@skyhook-io/radar-app 1.16.1 → 1.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/App.tsx +5 -4
- package/src/RadarApp.tsx +10 -2
- package/src/api/client.ts +8 -2
- package/src/api/client.updateResource.test.tsx +58 -0
- package/src/api/diagnose.ts +26 -0
- package/src/api/httpErrors.ts +12 -0
- package/src/components/ConnectionErrorView.test.tsx +2 -0
- package/src/components/ConnectionErrorView.tsx +2 -1
- 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/logs/LogsViewer.tsx +3 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +3 -0
- package/src/components/settings/LocalConfigurationDetails.tsx +21 -15
- package/src/components/settings/LocalConnectionSettings.tsx +41 -10
- package/src/components/whats-new/releaseNotes.ts +1 -1
- package/src/context/DiagnoseCustomization.tsx +13 -3
- package/src/index.ts +4 -2
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderToString } from "react-dom/server";
|
|
3
|
+
import { DiagnoseError, investigationRefusal } from "../../api/diagnose";
|
|
4
|
+
import { DiagnoseCustomizationProvider } from "../../context/DiagnoseCustomization";
|
|
5
|
+
import {
|
|
6
|
+
InvestigationFailureNotice,
|
|
7
|
+
InvestigationStartErrorAlert,
|
|
8
|
+
InvestigationStatusCheckNotice,
|
|
9
|
+
} from "./InvestigationView";
|
|
10
|
+
|
|
11
|
+
const quota = new DiagnoseError(
|
|
12
|
+
402,
|
|
13
|
+
"Your organization has used this month's investigations.",
|
|
14
|
+
{
|
|
15
|
+
code: "ai_quota_exhausted",
|
|
16
|
+
reason: "allowance_used_free",
|
|
17
|
+
action: "upgrade",
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
describe("investigationRefusal", () => {
|
|
22
|
+
it("carries a host refusal's code, reason and action with its sentence", () => {
|
|
23
|
+
expect(investigationRefusal(quota)).toEqual({
|
|
24
|
+
status: 402,
|
|
25
|
+
message: "Your organization has used this month's investigations.",
|
|
26
|
+
code: "ai_quota_exhausted",
|
|
27
|
+
reason: "allowance_used_free",
|
|
28
|
+
action: "upgrade",
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("is null for Radar's own errors and for anything that isn't a DiagnoseError", () => {
|
|
33
|
+
expect(
|
|
34
|
+
investigationRefusal(
|
|
35
|
+
new DiagnoseError(409, "Too many investigations running."),
|
|
36
|
+
),
|
|
37
|
+
).toBeNull();
|
|
38
|
+
expect(investigationRefusal(new Error("network down"))).toBeNull();
|
|
39
|
+
expect(investigationRefusal(undefined)).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("InvestigationStartErrorAlert", () => {
|
|
44
|
+
const alert = (refusal = investigationRefusal(quota)) => (
|
|
45
|
+
<InvestigationStartErrorAlert
|
|
46
|
+
error={quota.message}
|
|
47
|
+
refusal={refusal}
|
|
48
|
+
onDismiss={() => {}}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
it("shows the host's action beside the refusal", () => {
|
|
53
|
+
const html = renderToString(
|
|
54
|
+
<DiagnoseCustomizationProvider
|
|
55
|
+
value={undefined}
|
|
56
|
+
renderRefusalAction={(r) => (
|
|
57
|
+
<a href="/plans">{`Upgrade (${r.reason})`}</a>
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
{alert()}
|
|
61
|
+
</DiagnoseCustomizationProvider>,
|
|
62
|
+
);
|
|
63
|
+
expect(html).toContain("this month's investigations");
|
|
64
|
+
expect(html).toContain("Upgrade (allowance_used_free)");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("shows only the sentence when the host offers no action", () => {
|
|
68
|
+
const html = renderToString(alert());
|
|
69
|
+
expect(html).toContain("this month's investigations");
|
|
70
|
+
expect(html).not.toContain("Upgrade");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("asks the host for nothing when the error carries no refusal", () => {
|
|
74
|
+
let asked = false;
|
|
75
|
+
renderToString(
|
|
76
|
+
<DiagnoseCustomizationProvider
|
|
77
|
+
value={undefined}
|
|
78
|
+
renderRefusalAction={() => {
|
|
79
|
+
asked = true;
|
|
80
|
+
return null;
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
{alert(null)}
|
|
84
|
+
</DiagnoseCustomizationProvider>,
|
|
85
|
+
);
|
|
86
|
+
expect(asked).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const withHost = (node: React.ReactNode) =>
|
|
91
|
+
renderToString(
|
|
92
|
+
<DiagnoseCustomizationProvider
|
|
93
|
+
value={undefined}
|
|
94
|
+
renderRefusalAction={(r) => (
|
|
95
|
+
<a href="/plans">{`Upgrade (${r.reason})`}</a>
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{node}
|
|
99
|
+
</DiagnoseCustomizationProvider>,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
describe("InvestigationStatusCheckNotice (Findings)", () => {
|
|
103
|
+
it("says a refused status check couldn't run, with the host's action", () => {
|
|
104
|
+
const html = withHost(
|
|
105
|
+
<InvestigationStatusCheckNotice
|
|
106
|
+
message={quota.message}
|
|
107
|
+
uncertainOnly={false}
|
|
108
|
+
refusal={investigationRefusal(quota)}
|
|
109
|
+
onCheck={() => {}}
|
|
110
|
+
disabled={false}
|
|
111
|
+
/>,
|
|
112
|
+
);
|
|
113
|
+
expect(html).toContain("Couldn't check status:");
|
|
114
|
+
expect(html).not.toContain("Verification did not complete");
|
|
115
|
+
expect(html).toContain("Upgrade (allowance_used_free)");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("keeps the verification wording, and no action, for a check that ran and failed", () => {
|
|
119
|
+
const html = withHost(
|
|
120
|
+
<InvestigationStatusCheckNotice
|
|
121
|
+
message="The agent stopped before checking."
|
|
122
|
+
uncertainOnly={false}
|
|
123
|
+
refusal={null}
|
|
124
|
+
onCheck={() => {}}
|
|
125
|
+
disabled={false}
|
|
126
|
+
/>,
|
|
127
|
+
);
|
|
128
|
+
expect(html).toContain("Verification did not complete:");
|
|
129
|
+
expect(html).not.toContain("Upgrade");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("InvestigationFailureNotice (Activity)", () => {
|
|
134
|
+
it("keeps a status warning first and shows a newer refused follow-up, with its action, beside it", () => {
|
|
135
|
+
const html = withHost(
|
|
136
|
+
<InvestigationFailureNotice
|
|
137
|
+
message="Radar couldn't confirm whether the apply request completed."
|
|
138
|
+
refusal={null}
|
|
139
|
+
statusCheck={{
|
|
140
|
+
label: "Check current status",
|
|
141
|
+
onCheck: () => {},
|
|
142
|
+
disabled: false,
|
|
143
|
+
}}
|
|
144
|
+
followUp={{
|
|
145
|
+
message: quota.message,
|
|
146
|
+
refusal: investigationRefusal(quota),
|
|
147
|
+
}}
|
|
148
|
+
/>,
|
|
149
|
+
);
|
|
150
|
+
expect(
|
|
151
|
+
html.indexOf("couldn't confirm whether the apply"),
|
|
152
|
+
).toBeLessThan(html.indexOf("this month's investigations"));
|
|
153
|
+
expect(html).toContain("Check current status");
|
|
154
|
+
expect(html).toContain("Upgrade (allowance_used_free)");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { InvestigationRefusal } from "../../api/diagnose";
|
|
2
|
+
import { useDiagnoseCustomization } from "../../context/DiagnoseCustomization";
|
|
3
|
+
|
|
4
|
+
/** The host's action for a refused start or follow-up, or nothing when the
|
|
5
|
+
* refusal carries no code, reason or action, or the host offers none. */
|
|
6
|
+
export function InvestigationRefusalAction({
|
|
7
|
+
refusal,
|
|
8
|
+
}: {
|
|
9
|
+
refusal: InvestigationRefusal | null;
|
|
10
|
+
}) {
|
|
11
|
+
const { renderRefusalAction } = useDiagnoseCustomization();
|
|
12
|
+
if (!refusal || !renderRefusalAction) return null;
|
|
13
|
+
return <>{renderRefusalAction(refusal)}</>;
|
|
14
|
+
}
|
|
@@ -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
|
|