@skyhook-io/radar-app 1.14.0 → 1.14.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 +2 -2
- package/src/App.tsx +6 -4
- package/src/api/client.ts +3 -0
- package/src/api/diagnose.ts +31 -1
- package/src/components/CloudConnectFlow.tsx +173 -13
- package/src/components/CloudFunnelButton.tsx +4 -2
- package/src/components/ConnectionErrorView.tsx +9 -10
- package/src/components/DebugOverlay.tsx +10 -6
- package/src/components/cloudConnectHandoff.test.ts +109 -4
- package/src/components/cloudConnectHandoff.ts +154 -2
- package/src/components/curl/ServiceCurlButton.tsx +19 -26
- package/src/components/diagnose/AgentCase.tsx +18 -5
- package/src/components/diagnose/AnalysisStory.test.tsx +308 -0
- package/src/components/diagnose/AnalysisStory.tsx +564 -0
- package/src/components/diagnose/DiagnoseContext.test.ts +10 -0
- package/src/components/diagnose/DiagnoseContext.tsx +25 -8
- package/src/components/diagnose/DiagnoseSurface.tsx +20 -14
- package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +771 -0
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +8 -33
- package/src/components/diagnose/InvestigationEvidencePane.tsx +809 -168
- package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +30 -0
- package/src/components/diagnose/InvestigationResourceEvidence.tsx +20 -0
- package/src/components/diagnose/InvestigationView.tsx +493 -529
- package/src/components/diagnose/investigationCase.test.tsx +267 -129
- package/src/components/diagnose/investigationCase.ts +122 -77
- package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +27 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +28 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +11 -2
- package/src/components/diagnose/investigationEvidence/identity.test.ts +25 -7
- package/src/components/diagnose/investigationEvidence/identity.ts +4 -4
- package/src/components/diagnose/investigationEvidence/observations.ts +24 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +36 -3
- package/src/components/diagnose/investigationEvidence/types.ts +2 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +2 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +3 -0
- package/src/components/diagnose/investigationState.test.ts +316 -58
- package/src/components/diagnose/investigationState.ts +257 -44
- package/src/components/diagnose/investigationStory.test.ts +161 -0
- package/src/components/diagnose/investigationStory.ts +207 -0
- package/src/components/diagnose/parts.test.tsx +9 -6
- package/src/components/diagnose/parts.tsx +977 -193
- package/src/components/diagnose/storyParts.test.tsx +426 -0
- package/src/components/diagnose/toolCallLabel.test.ts +51 -0
- package/src/components/diagnose/toolCallLabel.ts +135 -0
- package/src/components/diagnose/useDisclosureReveal.ts +10 -11
- package/src/components/helm/HelmCompareRoute.tsx +18 -4
- package/src/components/helm/HelmReleaseDrawer.tsx +11 -26
- package/src/components/helm/InstallWizard.tsx +8 -3
- package/src/components/home/MCPSetupDialog.tsx +15 -9
- package/src/components/portforward/PortForwardManager.tsx +6 -13
- package/src/components/resource/PrometheusChartsGrid.tsx +3 -3
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +3 -3
- package/src/components/resource/WorkloadMetricsSection.tsx +15 -21
- package/src/components/resources/PodFilePreview.tsx +3 -3
- package/src/components/resources/renderers/ServiceRenderer.tsx +2 -1
- package/src/components/settings/SettingsDialog.tsx +4 -2
- package/src/components/ui/CommandPalette.tsx +10 -6
- package/src/components/ui/DiagnosticsOverlay.tsx +10 -6
- package/src/components/ui/Disclosure.tsx +47 -0
- package/src/components/ui/Markdown.tsx +23 -11
- package/src/components/ui/ShortcutHelpOverlay.tsx +3 -1
- package/src/components/ui/UpdateNotification.tsx +18 -2
- package/src/index.css +19 -6
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
1
|
// A view over one durable, server-side investigation run. It SUBSCRIBES to the
|
|
3
2
|
// run's event stream (replay + live) and reconstructs the transcript; it does not
|
|
4
3
|
// own the run's lifetime — the server does. So closing the panel or navigating
|
|
@@ -6,7 +5,6 @@ import type { ReactNode } from "react";
|
|
|
6
5
|
import {
|
|
7
6
|
investigationDisclosureSettleDelay,
|
|
8
7
|
prefersReducedMotion,
|
|
9
|
-
useDisclosureReveal,
|
|
10
8
|
} from "./useDisclosureReveal";
|
|
11
9
|
import {
|
|
12
10
|
investigationExplanation,
|
|
@@ -31,7 +29,11 @@ import {
|
|
|
31
29
|
investigationEvidenceCoverageLimited,
|
|
32
30
|
investigationEvidenceConflictsWithHealthy,
|
|
33
31
|
investigationHealthConflictExplainedBy,
|
|
34
|
-
|
|
32
|
+
investigationAssessmentTurnIndexes,
|
|
33
|
+
investigationEvidenceCoverageGaps,
|
|
34
|
+
investigationHealthSignals,
|
|
35
|
+
investigationIsAssessmentTurn,
|
|
36
|
+
investigationSettledAnswerTurnIndexes,
|
|
35
37
|
investigationEndedBeforeConclusion,
|
|
36
38
|
type InvestigationHistoryUnavailableState,
|
|
37
39
|
investigationHistoryUnavailablePresentation,
|
|
@@ -41,6 +43,8 @@ import {
|
|
|
41
43
|
canInvestigateFurther,
|
|
42
44
|
} from "./investigationState";
|
|
43
45
|
import type { AssessmentExplanation } from "./parts";
|
|
46
|
+
import { describeToolCall } from "./toolCallLabel";
|
|
47
|
+
import { AGENT_ROLE_LABELS } from "./AgentCase";
|
|
44
48
|
import {
|
|
45
49
|
Fragment,
|
|
46
50
|
useCallback,
|
|
@@ -53,7 +57,7 @@ import {
|
|
|
53
57
|
type KeyboardEvent,
|
|
54
58
|
} from "react";
|
|
55
59
|
import { useQueryClient } from "@tanstack/react-query";
|
|
56
|
-
import { Badge
|
|
60
|
+
import { Badge } from "@skyhook-io/k8s-ui";
|
|
57
61
|
import {
|
|
58
62
|
Send,
|
|
59
63
|
AlertTriangle,
|
|
@@ -61,6 +65,7 @@ import {
|
|
|
61
65
|
ArrowRight,
|
|
62
66
|
Activity,
|
|
63
67
|
CheckCircle2,
|
|
68
|
+
FileClock,
|
|
64
69
|
Files,
|
|
65
70
|
Loader2,
|
|
66
71
|
} from "lucide-react";
|
|
@@ -69,7 +74,6 @@ import {
|
|
|
69
74
|
addTurn,
|
|
70
75
|
stopRun,
|
|
71
76
|
DiagnoseError,
|
|
72
|
-
type Diagnosis,
|
|
73
77
|
type DiagnoseStreamEvent,
|
|
74
78
|
type RunSummary,
|
|
75
79
|
} from "../../api/diagnose";
|
|
@@ -83,6 +87,7 @@ import {
|
|
|
83
87
|
mergeStartupSignal,
|
|
84
88
|
upsertTool,
|
|
85
89
|
type Turn,
|
|
90
|
+
remediationHeadline,
|
|
86
91
|
} from "./parts";
|
|
87
92
|
import {
|
|
88
93
|
investigationActivitySourceDomId,
|
|
@@ -91,12 +96,7 @@ import {
|
|
|
91
96
|
projectInvestigationEvidence,
|
|
92
97
|
resolveInvestigationRootCauseEvidence,
|
|
93
98
|
} from "./investigationEvidence";
|
|
94
|
-
import {
|
|
95
|
-
resolveInvestigationCase,
|
|
96
|
-
investigationCaseItemsStillRendered,
|
|
97
|
-
mergeInvestigationCases,
|
|
98
|
-
type InvestigationCaseResolution,
|
|
99
|
-
} from "./investigationCase";
|
|
99
|
+
import { resolveInvestigationCase } from "./investigationCase";
|
|
100
100
|
import {
|
|
101
101
|
InvestigationEvidencePane,
|
|
102
102
|
partitionInvestigationEvidence,
|
|
@@ -106,6 +106,8 @@ import type { DiagnosisResourceRef } from "./diagnoseEvidenceTypes";
|
|
|
106
106
|
import { formatInvestigationTarget } from "./target";
|
|
107
107
|
import { parseContextName } from "../../utils/context-name";
|
|
108
108
|
import type { InvestigationSourceExcerpt } from "./investigationSourceFocus";
|
|
109
|
+
import { diagnosisHasStoryShape } from "./investigationStory";
|
|
110
|
+
import { groupEvidenceCoverage } from "./investigationEvidencePresentation";
|
|
109
111
|
|
|
110
112
|
const RECHECK_QUESTION =
|
|
111
113
|
"Did the fix resolve the issue? Re-check the resource's current status and health now, and say whether it's healthy.";
|
|
@@ -887,11 +889,10 @@ export function InvestigationView({
|
|
|
887
889
|
let lastApplyAttemptIdx = -1;
|
|
888
890
|
let lastApplyOutcome: Turn["applyOutcome"];
|
|
889
891
|
turns.forEach((t, i) => {
|
|
892
|
+
// A revising follow-up is an assessment turn too; its steps are the ones
|
|
893
|
+
// Findings offers.
|
|
890
894
|
if (
|
|
891
|
-
t
|
|
892
|
-
!t.apply &&
|
|
893
|
-
!t.explainAssessment &&
|
|
894
|
-
(!t.question || t.verify) &&
|
|
895
|
+
investigationIsAssessmentTurn(t) &&
|
|
895
896
|
(t.diagnosis?.remediation?.length ?? 0) > 0
|
|
896
897
|
)
|
|
897
898
|
lastRemediationIdx = i;
|
|
@@ -901,26 +902,10 @@ export function InvestigationView({
|
|
|
901
902
|
}
|
|
902
903
|
});
|
|
903
904
|
|
|
904
|
-
// Initial and explicit verification turns update the
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
const dx = t.diagnosis;
|
|
909
|
-
const structured =
|
|
910
|
-
!!dx &&
|
|
911
|
-
(!!dx.rootCause ||
|
|
912
|
-
(dx.remediation?.length ?? 0) > 0 ||
|
|
913
|
-
dx.healthy ||
|
|
914
|
-
dx.inconclusive);
|
|
915
|
-
if (
|
|
916
|
-
t.status === "done" &&
|
|
917
|
-
!t.apply &&
|
|
918
|
-
!t.explainAssessment &&
|
|
919
|
-
(!t.question || t.verify) &&
|
|
920
|
-
structured
|
|
921
|
-
)
|
|
922
|
-
assessmentIndexes.push(i);
|
|
923
|
-
});
|
|
905
|
+
// Initial and explicit verification turns update the Findings assessment,
|
|
906
|
+
// and so does a question whose verdict revises it and is complete. Other
|
|
907
|
+
// questions remain conversational answers in Activity.
|
|
908
|
+
const assessmentIndexes = investigationAssessmentTurnIndexes(turns);
|
|
924
909
|
const currentAssessmentIdx = assessmentIndexes.at(-1) ?? -1;
|
|
925
910
|
const initialAssessmentIdx = assessmentIndexes[0] ?? -1;
|
|
926
911
|
const hasMultipleAssessments = assessmentIndexes.length > 1;
|
|
@@ -997,101 +982,22 @@ export function InvestigationView({
|
|
|
997
982
|
: undefined,
|
|
998
983
|
[currentAssessment, currentAssessmentIdx, projection],
|
|
999
984
|
);
|
|
1000
|
-
//
|
|
1001
|
-
//
|
|
1002
|
-
|
|
1003
|
-
turns,
|
|
1004
|
-
currentAssessmentIdx,
|
|
1005
|
-
);
|
|
1006
|
-
const liveCaseIsCurrentAssessment = liveCaseTurnIdx === currentAssessmentIdx;
|
|
1007
|
-
// The qualification banner and the "Used for assessment" markers describe the
|
|
1008
|
-
// assessment on screen, so they always read its own resolution. A later turn's
|
|
1009
|
-
// citations only widen the selection (below): replacing the resolution would
|
|
1010
|
-
// drop the assessment's qualification and push its cited evidence back into
|
|
1011
|
-
// the withheld set.
|
|
985
|
+
// Findings is the newest assessment and nothing else: its own citations,
|
|
986
|
+
// its own case, its own story. Earlier assessments stay complete in
|
|
987
|
+
// Activity; answers that did not revise the assessment stay there too.
|
|
1012
988
|
const paneResolution = rootCauseEvidenceResolution;
|
|
1013
|
-
|
|
1014
|
-
// receipt: its sources are its, not the assessment's.
|
|
1015
|
-
const liveTurnResolution = useMemo(() => {
|
|
1016
|
-
if (liveCaseIsCurrentAssessment) return rootCauseEvidenceResolution;
|
|
1017
|
-
const diagnosis = turns[liveCaseTurnIdx]?.diagnosis;
|
|
1018
|
-
return diagnosis?.rootCause
|
|
1019
|
-
? resolveInvestigationRootCauseEvidence(
|
|
1020
|
-
projection,
|
|
1021
|
-
diagnosis.rootCauseEvidence,
|
|
1022
|
-
liveCaseTurnIdx,
|
|
1023
|
-
)
|
|
1024
|
-
: undefined;
|
|
1025
|
-
}, [
|
|
1026
|
-
liveCaseIsCurrentAssessment,
|
|
1027
|
-
rootCauseEvidenceResolution,
|
|
1028
|
-
turns,
|
|
1029
|
-
liveCaseTurnIdx,
|
|
1030
|
-
projection,
|
|
1031
|
-
]);
|
|
1032
|
-
const followUpSelectedGroupIds = useMemo(() => {
|
|
1033
|
-
if (liveCaseIsCurrentAssessment) return undefined;
|
|
1034
|
-
if (liveTurnResolution?.status !== "linked") return undefined;
|
|
1035
|
-
return liveTurnResolution.links.flatMap((link) =>
|
|
1036
|
-
link.originalGroupId
|
|
1037
|
-
? [{ groupId: link.originalGroupId, source: link.source }]
|
|
1038
|
-
: [],
|
|
1039
|
-
);
|
|
1040
|
-
}, [liveCaseIsCurrentAssessment, liveTurnResolution]);
|
|
1041
|
-
// The live turn's own case, before anything is carried onto it. Its source
|
|
1042
|
-
// receipt must list what IT cited, not what the merge brought along.
|
|
1043
|
-
const liveTurnCase = useMemo(
|
|
1044
|
-
() =>
|
|
1045
|
-
liveCaseIsCurrentAssessment
|
|
1046
|
-
? investigationCase
|
|
1047
|
-
: resolveInvestigationCase(
|
|
1048
|
-
projection,
|
|
1049
|
-
turns[liveCaseTurnIdx]?.diagnosis,
|
|
1050
|
-
liveCaseTurnIdx,
|
|
1051
|
-
),
|
|
1052
|
-
[
|
|
1053
|
-
liveCaseIsCurrentAssessment,
|
|
1054
|
-
investigationCase,
|
|
1055
|
-
projection,
|
|
1056
|
-
turns,
|
|
1057
|
-
liveCaseTurnIdx,
|
|
1058
|
-
],
|
|
1059
|
-
);
|
|
1060
|
-
const paneCase = useMemo(() => {
|
|
1061
|
-
const live = liveTurnCase;
|
|
1062
|
-
const earlier: InvestigationCaseResolution[] = [];
|
|
1063
|
-
for (let i = turns.length - 1; i >= 0; i -= 1) {
|
|
1064
|
-
const diagnosis = turns[i]?.diagnosis;
|
|
1065
|
-
if (i === liveCaseTurnIdx || !diagnosis) continue;
|
|
1066
|
-
// Reuse the assessment's own resolution rather than resolving it a
|
|
1067
|
-
// second time: same projection, same result, and it keeps the items the
|
|
1068
|
-
// conflict banner reasons about the ones the merge received.
|
|
1069
|
-
earlier.push(
|
|
1070
|
-
i === currentAssessmentIdx && investigationCase
|
|
1071
|
-
? investigationCase
|
|
1072
|
-
: resolveInvestigationCase(projection, diagnosis, i),
|
|
1073
|
-
);
|
|
1074
|
-
}
|
|
1075
|
-
return mergeInvestigationCases(live, earlier);
|
|
1076
|
-
}, [
|
|
1077
|
-
liveTurnCase,
|
|
1078
|
-
investigationCase,
|
|
1079
|
-
currentAssessmentIdx,
|
|
1080
|
-
projection,
|
|
1081
|
-
turns,
|
|
1082
|
-
liveCaseTurnIdx,
|
|
1083
|
-
]);
|
|
989
|
+
const storyShape = diagnosisHasStoryShape(currentAssessment?.diagnosis);
|
|
1084
990
|
const visibleEvidenceGroupIds = useMemo(
|
|
1085
991
|
() =>
|
|
1086
992
|
new Set(
|
|
1087
993
|
partitionInvestigationEvidence(
|
|
1088
994
|
projection.groups,
|
|
1089
995
|
paneResolution,
|
|
1090
|
-
|
|
1091
|
-
|
|
996
|
+
investigationCase,
|
|
997
|
+
storyShape,
|
|
1092
998
|
).collectionByGroup.keys(),
|
|
1093
999
|
),
|
|
1094
|
-
[projection.groups, paneResolution,
|
|
1000
|
+
[projection.groups, paneResolution, investigationCase, storyShape],
|
|
1095
1001
|
);
|
|
1096
1002
|
const evidenceStepIdsByTurn = useMemo(
|
|
1097
1003
|
() =>
|
|
@@ -1378,47 +1284,93 @@ export function InvestigationView({
|
|
|
1378
1284
|
: verificationError;
|
|
1379
1285
|
const displayedStatusCheckError =
|
|
1380
1286
|
displayedVerificationError || applyOutcomeUncertain;
|
|
1381
|
-
const currentKeyFindingCount = projection.groups.filter(
|
|
1382
|
-
(group) => !group.historical && group.latest.tier === "key",
|
|
1383
|
-
).length;
|
|
1384
1287
|
const findingsTabAccessibleLabel =
|
|
1385
1288
|
"Findings: current assessment, Radar evidence, and next steps";
|
|
1386
1289
|
const currentAssessmentCoverageLimited = investigationEvidenceCoverageLimited(
|
|
1387
1290
|
currentAssessmentProjection,
|
|
1291
|
+
kind,
|
|
1388
1292
|
);
|
|
1293
|
+
// The agent's notes per source. In the legacy shape this is Assessment
|
|
1294
|
+
// details; in the story shape the notes sit on the cards, so it appears
|
|
1295
|
+
// only inside Captured results and only when something has no card: a
|
|
1296
|
+
// note that pins to no single observation, or a citation Radar rejected.
|
|
1297
|
+
const assessmentSourcesNode =
|
|
1298
|
+
currentAssessment?.diagnosis &&
|
|
1299
|
+
(rootCauseEvidenceResolution?.links.length ||
|
|
1300
|
+
investigationCase?.items.length ||
|
|
1301
|
+
currentAssessment.diagnosis.unlinkedEvidence ||
|
|
1302
|
+
currentAssessment.diagnosis.evidenceMalformed) ? (
|
|
1303
|
+
<AssessmentSources
|
|
1304
|
+
renderedGroupIds={visibleEvidenceGroupIds}
|
|
1305
|
+
resolution={rootCauseEvidenceResolution}
|
|
1306
|
+
investigationCase={investigationCase}
|
|
1307
|
+
unlinkedEvidence={currentAssessment.diagnosis.unlinkedEvidence}
|
|
1308
|
+
evidenceMalformed={currentAssessment.diagnosis.evidenceMalformed}
|
|
1309
|
+
onViewSource={viewActivitySource}
|
|
1310
|
+
/>
|
|
1311
|
+
) : undefined;
|
|
1312
|
+
const recordNotes =
|
|
1313
|
+
storyShape &&
|
|
1314
|
+
(investigationCase?.items.some((item) => item.placement === "source") ||
|
|
1315
|
+
currentAssessment?.diagnosis?.unlinkedEvidence ||
|
|
1316
|
+
currentAssessment?.diagnosis?.evidenceMalformed)
|
|
1317
|
+
? assessmentSourcesNode
|
|
1318
|
+
: undefined;
|
|
1319
|
+
const assessmentLimits = useMemo(() => {
|
|
1320
|
+
const lines = groupEvidenceCoverage(currentAssessmentProjection.limitations)
|
|
1321
|
+
// A capped preview or a history bound is bookkeeping for the record; Still
|
|
1322
|
+
// open keeps the reads Radar could not complete.
|
|
1323
|
+
.filter((group) => !group.historyOnly && !group.bookkeepingOnly)
|
|
1324
|
+
.map((group) => `${group.label}: ${group.summary}`);
|
|
1325
|
+
// Two coverage gaps carry no limitation of their own; name the one that
|
|
1326
|
+
// applies.
|
|
1327
|
+
const gaps = investigationEvidenceCoverageGaps(
|
|
1328
|
+
currentAssessmentProjection,
|
|
1329
|
+
kind,
|
|
1330
|
+
);
|
|
1331
|
+
if (gaps.noEvidence)
|
|
1332
|
+
lines.push("No evidence was recorded for this assessment");
|
|
1333
|
+
else if (gaps.noTargetDiagnosis)
|
|
1334
|
+
lines.push(
|
|
1335
|
+
"Radar's full diagnose of this workload was not collected, so this check is partial",
|
|
1336
|
+
);
|
|
1337
|
+
return lines;
|
|
1338
|
+
}, [currentAssessmentProjection, kind]);
|
|
1339
|
+
const healthSignals = useMemo(
|
|
1340
|
+
() =>
|
|
1341
|
+
currentAssessment?.diagnosis?.healthy
|
|
1342
|
+
? investigationHealthSignals(projection, investigationCase?.items)
|
|
1343
|
+
: [],
|
|
1344
|
+
[currentAssessment, projection, investigationCase],
|
|
1345
|
+
);
|
|
1346
|
+
// While the first assessment is still running the pane keeps the story
|
|
1347
|
+
// shape, so the page fills in rather than rearranging when the verdict lands.
|
|
1348
|
+
const storyShell =
|
|
1349
|
+
!hosted && !currentAssessment && lastTurn?.status === "running";
|
|
1389
1350
|
const currentAssessmentEvidenceConflict =
|
|
1390
1351
|
currentAssessment?.diagnosis?.healthy === true &&
|
|
1391
1352
|
investigationEvidenceConflictsWithHealthy(projection);
|
|
1392
1353
|
// The banner qualifies THIS assessment, so only this assessment's own case
|
|
1393
|
-
// may reframe it
|
|
1394
|
-
// inherited from superseded assessments; neither of those spoke about this
|
|
1395
|
-
// verdict, and letting them soften it would let an unrelated follow-up
|
|
1396
|
-
// retire a warning the reader still needs.
|
|
1354
|
+
// may reframe it — and that is the only case the pane renders.
|
|
1397
1355
|
const currentAssessmentEvidenceConflictExplainedBy = useMemo(() => {
|
|
1398
1356
|
if (!currentAssessmentEvidenceConflict) return undefined;
|
|
1399
|
-
// Two conditions, and both are required. The note must belong to THIS
|
|
1400
|
-
// assessment — a later answer or a superseded assessment did not speak
|
|
1401
|
-
// about this verdict. And it must survive into the case the pane actually
|
|
1402
|
-
// renders: the merge lets a later turn take over a group, and a banner
|
|
1403
|
-
// that points at a note the reader cannot find is worse than no banner.
|
|
1404
|
-
const qualifying = investigationCaseItemsStillRendered(
|
|
1405
|
-
investigationCase?.items,
|
|
1406
|
-
paneCase?.items,
|
|
1407
|
-
);
|
|
1408
1357
|
return (
|
|
1409
|
-
investigationHealthConflictExplainedBy(
|
|
1410
|
-
|
|
1358
|
+
investigationHealthConflictExplainedBy(
|
|
1359
|
+
projection,
|
|
1360
|
+
investigationCase?.items,
|
|
1361
|
+
) ?? undefined
|
|
1411
1362
|
);
|
|
1412
|
-
}, [
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
]);
|
|
1363
|
+
}, [currentAssessmentEvidenceConflict, projection, investigationCase]);
|
|
1364
|
+
const settledAnswerTurns = useMemo(
|
|
1365
|
+
() => investigationSettledAnswerTurnIndexes(turns, currentAssessmentIdx),
|
|
1366
|
+
[turns, currentAssessmentIdx],
|
|
1367
|
+
);
|
|
1418
1368
|
const hasEvidenceCollectedAfterAssessment =
|
|
1419
1369
|
currentAssessmentIdx >= 0 &&
|
|
1420
1370
|
projection.sources.some(
|
|
1421
|
-
(source) =>
|
|
1371
|
+
(source) =>
|
|
1372
|
+
source.turnIndex > currentAssessmentIdx &&
|
|
1373
|
+
!settledAnswerTurns.has(source.turnIndex),
|
|
1422
1374
|
);
|
|
1423
1375
|
const assessmentNeedsCurrentStateVerification =
|
|
1424
1376
|
investigationAssessmentNeedsCurrentStateVerification({
|
|
@@ -1434,9 +1386,112 @@ export function InvestigationView({
|
|
|
1434
1386
|
const earlierPlan =
|
|
1435
1387
|
assessmentNeedsCurrentStateVerification ||
|
|
1436
1388
|
hasEvidenceCollectedAfterAssessment;
|
|
1389
|
+
// The jump link names the decision it leads to, not the section.
|
|
1390
|
+
const nextStepLabel = (() => {
|
|
1391
|
+
const dx = currentAssessment?.diagnosis;
|
|
1392
|
+
if (!dx) return "Next steps ↓";
|
|
1393
|
+
const index = dx.recommendedIndex ? dx.recommendedIndex - 1 : 0;
|
|
1394
|
+
const text =
|
|
1395
|
+
dx.steps?.[index]?.text ?? dx.remediation?.[index] ?? dx.remediation?.[0];
|
|
1396
|
+
if (!text) return "Next steps ↓";
|
|
1397
|
+
const headline = remediationHeadline(text);
|
|
1398
|
+
return `Next: ${headline.length > 56 ? `${headline.slice(0, 56).trimEnd()}…` : headline} ↓`;
|
|
1399
|
+
})();
|
|
1400
|
+
// What a reader in a channel needs with the verdict: where, when, and the
|
|
1401
|
+
// receipts behind it, cause first, at most three.
|
|
1402
|
+
const assessmentCopy = useMemo(() => {
|
|
1403
|
+
const items = [...(investigationCase?.items ?? [])]
|
|
1404
|
+
.filter((item) => item.placement === "card" && item.observation)
|
|
1405
|
+
.sort((a, b) => Number(b.role === "cause") - Number(a.role === "cause"))
|
|
1406
|
+
.slice(0, 3);
|
|
1407
|
+
return {
|
|
1408
|
+
context: {
|
|
1409
|
+
target: formatInvestigationTarget(run),
|
|
1410
|
+
cluster: run.context,
|
|
1411
|
+
startedAt: run.createdAt,
|
|
1412
|
+
agent: agentLabel,
|
|
1413
|
+
},
|
|
1414
|
+
receipts: items.map((item) => {
|
|
1415
|
+
const observation = item.observation!;
|
|
1416
|
+
const lines =
|
|
1417
|
+
observation.data.type === "logs"
|
|
1418
|
+
? (observation.data.logs?.lines ?? []).slice(-2)
|
|
1419
|
+
: observation.summary
|
|
1420
|
+
? [observation.summary]
|
|
1421
|
+
: [];
|
|
1422
|
+
return {
|
|
1423
|
+
title: observation.title,
|
|
1424
|
+
role: item.role ? AGENT_ROLE_LABELS[item.role] : undefined,
|
|
1425
|
+
lines,
|
|
1426
|
+
gap: item.gap || undefined,
|
|
1427
|
+
};
|
|
1428
|
+
}),
|
|
1429
|
+
};
|
|
1430
|
+
}, [investigationCase, run, agentLabel]);
|
|
1431
|
+
// While the agent works, name the read in flight instead of describing the
|
|
1432
|
+
// pane's mechanics, with how many reads so far and how long it has been.
|
|
1433
|
+
// The clock starts when this client sees the turn running; a replay of a
|
|
1434
|
+
// finished turn never shows it.
|
|
1435
|
+
const turnRunning = lastTurn?.status === "running";
|
|
1436
|
+
const runningSinceRef = useRef<number | undefined>(undefined);
|
|
1437
|
+
const [, setRunningTick] = useState(0);
|
|
1438
|
+
useEffect(() => {
|
|
1439
|
+
if (!turnRunning) {
|
|
1440
|
+
runningSinceRef.current = undefined;
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
runningSinceRef.current ??= Date.now();
|
|
1444
|
+
const id = window.setInterval(() => setRunningTick((t) => t + 1), 1000);
|
|
1445
|
+
return () => window.clearInterval(id);
|
|
1446
|
+
}, [turnRunning]);
|
|
1447
|
+
// A finished read is not what the agent is doing eight seconds later; after
|
|
1448
|
+
// a quiet spell the label says it is thinking rather than naming a stale read.
|
|
1449
|
+
const lastTool = [...(lastTurn?.timeline ?? [])]
|
|
1450
|
+
.reverse()
|
|
1451
|
+
.find((item) => item.kind === "tool");
|
|
1452
|
+
const toolSignature = lastTool
|
|
1453
|
+
? `${lastTool.id}:${lastTool.status}`
|
|
1454
|
+
: undefined;
|
|
1455
|
+
const toolChangedAtRef = useRef<number>(Date.now());
|
|
1456
|
+
const toolSeenRef = useRef(toolSignature);
|
|
1457
|
+
useEffect(() => {
|
|
1458
|
+
toolSeenRef.current = toolSignature;
|
|
1459
|
+
toolChangedAtRef.current = Date.now();
|
|
1460
|
+
}, [toolSignature]);
|
|
1461
|
+
const investigatingLabel = (() => {
|
|
1462
|
+
const items = lastTurn?.timeline ?? [];
|
|
1463
|
+
const reads = items.filter((item) => item.kind === "tool").length;
|
|
1464
|
+
// The render that first shows a changed tool is not quiet yet; the effect
|
|
1465
|
+
// that stamps the change runs after it.
|
|
1466
|
+
const quietFor =
|
|
1467
|
+
toolSeenRef.current === toolSignature
|
|
1468
|
+
? Date.now() - toolChangedAtRef.current
|
|
1469
|
+
: 0;
|
|
1470
|
+
let current = "Investigating";
|
|
1471
|
+
if (lastTool && lastTool.kind === "tool" && lastTool.tool) {
|
|
1472
|
+
current =
|
|
1473
|
+
lastTool.status === "running" || quietFor < 8000
|
|
1474
|
+
? describeToolCall(lastTool.tool, lastTool.summary)
|
|
1475
|
+
: "Thinking";
|
|
1476
|
+
}
|
|
1477
|
+
const elapsed = runningSinceRef.current
|
|
1478
|
+
? Math.round((Date.now() - runningSinceRef.current) / 1000)
|
|
1479
|
+
: 0;
|
|
1480
|
+
// The tool text changes width as it changes; the counters sit apart on
|
|
1481
|
+
// the right so they never move with it.
|
|
1482
|
+
return {
|
|
1483
|
+
current: `${current}…`,
|
|
1484
|
+
meta: [
|
|
1485
|
+
reads > 0 ? `${reads} ${reads === 1 ? "read" : "reads"}` : undefined,
|
|
1486
|
+
elapsed >= 2 ? `${elapsed} s` : undefined,
|
|
1487
|
+
]
|
|
1488
|
+
.filter(Boolean)
|
|
1489
|
+
.join(" · "),
|
|
1490
|
+
};
|
|
1491
|
+
})();
|
|
1437
1492
|
const showSplitWorkspace = maximized;
|
|
1438
1493
|
const splitGridClass = showSplitWorkspace
|
|
1439
|
-
? "@min-[1000px]/investigation:grid-cols-[minmax(
|
|
1494
|
+
? "@min-[1000px]/investigation:grid-cols-[minmax(320px,min(30%,520px))_minmax(0,1fr)]"
|
|
1440
1495
|
: "";
|
|
1441
1496
|
const splitTabClass = showSplitWorkspace
|
|
1442
1497
|
? "@min-[1000px]/investigation:hidden"
|
|
@@ -1590,6 +1645,50 @@ export function InvestigationView({
|
|
|
1590
1645
|
</div>
|
|
1591
1646
|
) : null;
|
|
1592
1647
|
|
|
1648
|
+
const nextStepsSection =
|
|
1649
|
+
hasNextSteps && currentAssessment?.diagnosis ? (
|
|
1650
|
+
<section
|
|
1651
|
+
ref={nextStepsRef}
|
|
1652
|
+
tabIndex={-1}
|
|
1653
|
+
aria-labelledby={`${workspaceId}-next-steps`}
|
|
1654
|
+
className="investigation-next-steps rounded-xl border p-4 outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
|
|
1655
|
+
>
|
|
1656
|
+
<h2
|
|
1657
|
+
id={`${workspaceId}-next-steps`}
|
|
1658
|
+
className="text-lg font-semibold text-theme-text-primary"
|
|
1659
|
+
>
|
|
1660
|
+
{earlierPlan ? "Earlier proposed steps" : "Next steps"}
|
|
1661
|
+
</h2>
|
|
1662
|
+
<ResultCard
|
|
1663
|
+
diagnosis={currentAssessment.diagnosis}
|
|
1664
|
+
section="actions"
|
|
1665
|
+
compactActions
|
|
1666
|
+
actionNotice={
|
|
1667
|
+
earlierPlan
|
|
1668
|
+
? assessmentNeedsCurrentStateVerification
|
|
1669
|
+
? "Proposed before the apply attempt. Current state has not been verified."
|
|
1670
|
+
: "Proposed before the latest evidence. Reassess before applying."
|
|
1671
|
+
: undefined
|
|
1672
|
+
}
|
|
1673
|
+
onApply={
|
|
1674
|
+
canOfferInvestigationApply({
|
|
1675
|
+
currentAssessmentIdx,
|
|
1676
|
+
lastRemediationIdx,
|
|
1677
|
+
lastApplyAttemptIdx,
|
|
1678
|
+
localApplyAttemptAssessmentIdx,
|
|
1679
|
+
interactionsBlocked,
|
|
1680
|
+
hosted,
|
|
1681
|
+
hasNewerEvidence: hasEvidenceCollectedAfterAssessment,
|
|
1682
|
+
})
|
|
1683
|
+
? requestApply
|
|
1684
|
+
: undefined
|
|
1685
|
+
}
|
|
1686
|
+
animate={currentAssessment.animateResult !== false}
|
|
1687
|
+
showDisclaimer={false}
|
|
1688
|
+
/>
|
|
1689
|
+
</section>
|
|
1690
|
+
) : undefined;
|
|
1691
|
+
|
|
1593
1692
|
return (
|
|
1594
1693
|
<div
|
|
1595
1694
|
data-investigation-workspace
|
|
@@ -1846,46 +1945,56 @@ export function InvestigationView({
|
|
|
1846
1945
|
? retryDiagnosis
|
|
1847
1946
|
: undefined
|
|
1848
1947
|
}
|
|
1849
|
-
|
|
1948
|
+
assessment={assessmentIndexes.includes(index)}
|
|
1949
|
+
// The current assessment is Findings; Activity
|
|
1950
|
+
// keeps a pointer to it and the full verdict of
|
|
1951
|
+
// every earlier one, so a revision never erases
|
|
1952
|
+
// what was said before.
|
|
1953
|
+
hideConclusion={index === currentAssessmentIdx}
|
|
1954
|
+
explanation={
|
|
1955
|
+
assessmentIndexes.includes(index) &&
|
|
1956
|
+
index !== currentAssessmentIdx
|
|
1957
|
+
? explanationFor(turn)
|
|
1958
|
+
: undefined
|
|
1959
|
+
}
|
|
1850
1960
|
assessmentSources={(() => {
|
|
1851
|
-
//
|
|
1852
|
-
//
|
|
1853
|
-
// read-only.
|
|
1961
|
+
// Every turn with a case shows what it cited;
|
|
1962
|
+
// only the current assessment's notes annotate
|
|
1963
|
+
// Findings, the rest are read-only here.
|
|
1854
1964
|
if (
|
|
1855
|
-
!turn.question ||
|
|
1856
|
-
turn.verify ||
|
|
1857
1965
|
turn.apply ||
|
|
1858
1966
|
turn.status !== "done" ||
|
|
1859
|
-
!turn.diagnosis
|
|
1967
|
+
!turn.diagnosis ||
|
|
1968
|
+
index === currentAssessmentIdx
|
|
1860
1969
|
)
|
|
1861
1970
|
return undefined;
|
|
1862
|
-
const
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
return
|
|
1875
|
-
|
|
1876
|
-
turn.diagnosis
|
|
1877
|
-
turn.diagnosis
|
|
1971
|
+
const turnCase = resolveInvestigationCase(
|
|
1972
|
+
projection,
|
|
1973
|
+
turn.diagnosis,
|
|
1974
|
+
index,
|
|
1975
|
+
);
|
|
1976
|
+
const turnResolution = turn.diagnosis.rootCause
|
|
1977
|
+
? resolveInvestigationRootCauseEvidence(
|
|
1978
|
+
projection,
|
|
1979
|
+
turn.diagnosis.rootCauseEvidence,
|
|
1980
|
+
index,
|
|
1981
|
+
)
|
|
1982
|
+
: undefined;
|
|
1983
|
+
return turnCase.items.length ||
|
|
1984
|
+
turnResolution?.links.length ||
|
|
1985
|
+
turn.diagnosis.unlinkedEvidence ||
|
|
1986
|
+
turn.diagnosis.evidenceMalformed ? (
|
|
1878
1987
|
<AssessmentSources
|
|
1879
1988
|
renderedGroupIds={visibleEvidenceGroupIds}
|
|
1880
|
-
resolution={
|
|
1881
|
-
investigationCase={
|
|
1989
|
+
resolution={turnResolution}
|
|
1990
|
+
investigationCase={turnCase}
|
|
1882
1991
|
unlinkedEvidence={
|
|
1883
|
-
turn.diagnosis
|
|
1992
|
+
turn.diagnosis.unlinkedEvidence
|
|
1884
1993
|
}
|
|
1885
1994
|
evidenceMalformed={
|
|
1886
|
-
turn.diagnosis
|
|
1995
|
+
turn.diagnosis.evidenceMalformed
|
|
1887
1996
|
}
|
|
1888
|
-
readOnly
|
|
1997
|
+
readOnly
|
|
1889
1998
|
onViewSource={viewActivitySource}
|
|
1890
1999
|
/>
|
|
1891
2000
|
) : undefined;
|
|
@@ -1929,9 +2038,7 @@ export function InvestigationView({
|
|
|
1929
2038
|
: "Assessment ready"}
|
|
1930
2039
|
</span>
|
|
1931
2040
|
<span className="block text-[11px] text-theme-text-tertiary">
|
|
1932
|
-
|
|
1933
|
-
? `${currentKeyFindingCount} ${currentKeyFindingCount === 1 ? "key finding" : "key findings"} ready to review`
|
|
1934
|
-
: "Findings compiled from Radar results"}
|
|
2041
|
+
Assessment, evidence and next steps
|
|
1935
2042
|
</span>
|
|
1936
2043
|
</span>
|
|
1937
2044
|
<span className="inline-flex shrink-0 items-center gap-1 text-xs font-medium text-accent-text">
|
|
@@ -2042,7 +2149,7 @@ export function InvestigationView({
|
|
|
2042
2149
|
}}
|
|
2043
2150
|
className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-3 py-3 [overflow-anchor:none] [scrollbar-gutter:stable]"
|
|
2044
2151
|
>
|
|
2045
|
-
<div ref={evidenceContentRef} className="min-w-0 space-y-
|
|
2152
|
+
<div ref={evidenceContentRef} className="min-w-0 space-y-4">
|
|
2046
2153
|
{rebuildingReplay ? (
|
|
2047
2154
|
<div className="flex min-h-28 flex-col items-center justify-center rounded-lg border border-dashed border-theme-border px-4 text-center">
|
|
2048
2155
|
{historyUnavailablePresentation &&
|
|
@@ -2067,221 +2174,211 @@ export function InvestigationView({
|
|
|
2067
2174
|
</p>
|
|
2068
2175
|
</div>
|
|
2069
2176
|
) : (
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
>
|
|
2075
|
-
<
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
>
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
? "
|
|
2092
|
-
:
|
|
2093
|
-
|
|
2177
|
+
<div className="space-y-4">
|
|
2178
|
+
{/* One column, one reading order: assessment, analysis, next
|
|
2179
|
+
steps. The page already holds four columns; Findings is
|
|
2180
|
+
not a fifth, and it fills the pane it is given. */}
|
|
2181
|
+
<div className="space-y-3">
|
|
2182
|
+
<section
|
|
2183
|
+
aria-labelledby={`${workspaceId}-assessment-heading`}
|
|
2184
|
+
className="investigation-assessment rounded-xl border p-3"
|
|
2185
|
+
>
|
|
2186
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
2187
|
+
<h2
|
|
2188
|
+
id={`${workspaceId}-assessment-heading`}
|
|
2189
|
+
className="text-lg font-semibold text-theme-text-primary"
|
|
2190
|
+
>
|
|
2191
|
+
{assessmentNeedsCurrentStateVerification
|
|
2192
|
+
? "Assessment before apply"
|
|
2193
|
+
: hasEvidenceCollectedAfterAssessment
|
|
2194
|
+
? "Earlier assessment"
|
|
2195
|
+
: !currentAssessment
|
|
2196
|
+
? "Assessment"
|
|
2197
|
+
: currentAssessment.verify
|
|
2198
|
+
? "Verification result"
|
|
2199
|
+
: currentAssessmentIdx ===
|
|
2200
|
+
initialAssessmentIdx &&
|
|
2201
|
+
hasMultipleAssessments
|
|
2202
|
+
? "Initial assessment"
|
|
2203
|
+
: "Assessment"}
|
|
2204
|
+
</h2>
|
|
2205
|
+
{assessmentNeedsCurrentStateVerification ? (
|
|
2206
|
+
<Badge severity="warning" size="sm">
|
|
2207
|
+
Current state unverified
|
|
2208
|
+
</Badge>
|
|
2209
|
+
) : null}
|
|
2210
|
+
{hasEvidenceCollectedAfterAssessment &&
|
|
2211
|
+
!assessmentNeedsCurrentStateVerification ? (
|
|
2212
|
+
<Badge severity="info" size="sm">
|
|
2213
|
+
Newer evidence below
|
|
2214
|
+
</Badge>
|
|
2215
|
+
) : null}
|
|
2216
|
+
{verificationRunning || verificationPending ? (
|
|
2217
|
+
<Badge severity="info" size="sm">
|
|
2218
|
+
Verifying…
|
|
2219
|
+
</Badge>
|
|
2220
|
+
) : null}
|
|
2221
|
+
</div>
|
|
2094
2222
|
{assessmentNeedsCurrentStateVerification ? (
|
|
2095
|
-
<
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
<
|
|
2102
|
-
|
|
2103
|
-
|
|
2223
|
+
<p className="mt-0.5 text-xs text-theme-text-tertiary">
|
|
2224
|
+
{verificationRunning || verificationPending
|
|
2225
|
+
? "This assessment predates the apply attempt. Radar is checking the current state now."
|
|
2226
|
+
: "This assessment predates the apply attempt; cluster state after it has not been verified."}
|
|
2227
|
+
</p>
|
|
2228
|
+
) : hasEvidenceCollectedAfterAssessment ? (
|
|
2229
|
+
<p className="mt-0.5 text-xs text-theme-text-tertiary">
|
|
2230
|
+
Some evidence below was collected after this
|
|
2231
|
+
assessment. Validate the conclusion against it before
|
|
2232
|
+
acting.
|
|
2233
|
+
</p>
|
|
2104
2234
|
) : null}
|
|
2105
|
-
{
|
|
2106
|
-
<
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2235
|
+
{currentAssessment?.diagnosis ? (
|
|
2236
|
+
<ResultCard
|
|
2237
|
+
key={
|
|
2238
|
+
currentAssessment.resultSequence ??
|
|
2239
|
+
currentAssessmentIdx
|
|
2240
|
+
}
|
|
2241
|
+
diagnosis={currentAssessment.diagnosis}
|
|
2242
|
+
assessmentLimits={
|
|
2243
|
+
storyShape ? assessmentLimits : undefined
|
|
2244
|
+
}
|
|
2245
|
+
assessmentCopy={assessmentCopy}
|
|
2246
|
+
healthSignals={storyShape ? healthSignals : undefined}
|
|
2247
|
+
onRevealSource={viewEvidenceSource}
|
|
2248
|
+
assessmentSources={
|
|
2249
|
+
storyShape ? undefined : assessmentSourcesNode
|
|
2250
|
+
}
|
|
2251
|
+
assessmentAction={
|
|
2252
|
+
hasNextSteps ? (
|
|
2253
|
+
<button
|
|
2254
|
+
type="button"
|
|
2255
|
+
onClick={() => {
|
|
2256
|
+
const section = nextStepsRef.current;
|
|
2257
|
+
const scroller = evidenceScrollRef.current;
|
|
2258
|
+
if (!section || !scroller) return;
|
|
2259
|
+
section.focus({ preventScroll: true });
|
|
2260
|
+
scroller.scrollTo({
|
|
2261
|
+
top:
|
|
2262
|
+
scroller.scrollTop +
|
|
2263
|
+
section.getBoundingClientRect().top -
|
|
2264
|
+
scroller.getBoundingClientRect().top -
|
|
2265
|
+
12,
|
|
2266
|
+
behavior: prefersReducedMotion()
|
|
2267
|
+
? "auto"
|
|
2268
|
+
: "smooth",
|
|
2269
|
+
});
|
|
2270
|
+
}}
|
|
2271
|
+
className="ml-auto rounded-md px-2 py-1 text-xs font-medium text-accent-text hover:bg-theme-hover"
|
|
2272
|
+
>
|
|
2273
|
+
{earlierPlan
|
|
2274
|
+
? "Earlier proposed steps ↓"
|
|
2275
|
+
: nextStepLabel}
|
|
2276
|
+
</button>
|
|
2277
|
+
) : null
|
|
2278
|
+
}
|
|
2279
|
+
explanation={explanationFor(currentAssessment)}
|
|
2280
|
+
section="conclusion"
|
|
2281
|
+
animate={currentAssessment.animateResult !== false}
|
|
2282
|
+
showDisclaimer={false}
|
|
2283
|
+
revisedAfter={
|
|
2284
|
+
currentAssessment.question &&
|
|
2285
|
+
!currentAssessment.verify &&
|
|
2286
|
+
hasMultipleAssessments
|
|
2287
|
+
? currentAssessment.question
|
|
2288
|
+
: undefined
|
|
2289
|
+
}
|
|
2290
|
+
coverageLimited={currentAssessmentCoverageLimited}
|
|
2291
|
+
evidenceConflict={currentAssessmentEvidenceConflict}
|
|
2292
|
+
evidenceConflictExplainedBy={
|
|
2293
|
+
currentAssessmentEvidenceConflictExplainedBy
|
|
2294
|
+
}
|
|
2295
|
+
/>
|
|
2296
|
+
) : (
|
|
2297
|
+
<div className="mt-2 flex items-center gap-2 rounded-md bg-theme-surface/60 px-2.5 py-2 text-xs text-theme-text-tertiary">
|
|
2298
|
+
{busy || requestPending ? (
|
|
2299
|
+
<span
|
|
2300
|
+
className="h-1.5 w-1.5 shrink-0 animate-pulse rounded-full bg-accent"
|
|
2301
|
+
aria-hidden
|
|
2150
2302
|
/>
|
|
2151
|
-
) :
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
<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">
|
|
2213
|
-
<AlertTriangle className="h-3.5 w-3.5 shrink-0 text-red-400" />
|
|
2214
|
-
<span className="min-w-0 flex-1">
|
|
2215
|
-
{applyOutcomeUncertain && !displayedVerificationError
|
|
2216
|
-
? displayedStatusCheckError
|
|
2217
|
-
: `Verification did not complete: ${displayedStatusCheckError}`}
|
|
2303
|
+
) : (
|
|
2304
|
+
<Activity
|
|
2305
|
+
className="h-3.5 w-3.5 shrink-0"
|
|
2306
|
+
aria-hidden
|
|
2307
|
+
/>
|
|
2308
|
+
)}
|
|
2309
|
+
{busy || requestPending ? (
|
|
2310
|
+
<>
|
|
2311
|
+
<span className="min-w-0 flex-1 truncate">
|
|
2312
|
+
{investigatingLabel.current}
|
|
2313
|
+
</span>
|
|
2314
|
+
{investigatingLabel.meta ? (
|
|
2315
|
+
<span className="shrink-0 tabular-nums text-theme-text-tertiary/80">
|
|
2316
|
+
{investigatingLabel.meta}
|
|
2317
|
+
</span>
|
|
2318
|
+
) : null}
|
|
2319
|
+
</>
|
|
2320
|
+
) : (
|
|
2321
|
+
<span>
|
|
2322
|
+
The agent did not provide a final assessment.
|
|
2323
|
+
</span>
|
|
2324
|
+
)}
|
|
2325
|
+
</div>
|
|
2326
|
+
)}
|
|
2327
|
+
{displayedStatusCheckError ? (
|
|
2328
|
+
<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">
|
|
2329
|
+
<AlertTriangle className="h-3.5 w-3.5 shrink-0 text-red-400" />
|
|
2330
|
+
<span className="min-w-0 flex-1">
|
|
2331
|
+
{applyOutcomeUncertain &&
|
|
2332
|
+
!displayedVerificationError
|
|
2333
|
+
? displayedStatusCheckError
|
|
2334
|
+
: `Verification did not complete: ${displayedStatusCheckError}`}
|
|
2335
|
+
</span>
|
|
2336
|
+
<button
|
|
2337
|
+
type="button"
|
|
2338
|
+
onClick={checkStatus}
|
|
2339
|
+
disabled={interactionsBlocked}
|
|
2340
|
+
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"
|
|
2341
|
+
>
|
|
2342
|
+
{applyOutcomeUncertain &&
|
|
2343
|
+
!displayedVerificationError
|
|
2344
|
+
? "Check current status"
|
|
2345
|
+
: "Check current status again"}
|
|
2346
|
+
</button>
|
|
2347
|
+
</div>
|
|
2348
|
+
) : null}
|
|
2349
|
+
</section>
|
|
2350
|
+
|
|
2351
|
+
{hasMultipleAssessments ? (
|
|
2352
|
+
<button
|
|
2353
|
+
type="button"
|
|
2354
|
+
data-investigation-earlier-assessments
|
|
2355
|
+
onClick={viewActivity}
|
|
2356
|
+
className="flex items-center gap-1.5 self-start rounded-md px-1.5 py-1 text-xs text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
2357
|
+
>
|
|
2358
|
+
<FileClock className="h-3.5 w-3.5" aria-hidden />
|
|
2359
|
+
{assessmentIndexes.length - 1 === 1
|
|
2360
|
+
? "1 earlier assessment"
|
|
2361
|
+
: `${assessmentIndexes.length - 1} earlier assessments`}
|
|
2362
|
+
<span className="text-theme-text-tertiary">
|
|
2363
|
+
· in Activity
|
|
2218
2364
|
</span>
|
|
2219
|
-
|
|
2220
|
-
type="button"
|
|
2221
|
-
onClick={checkStatus}
|
|
2222
|
-
disabled={interactionsBlocked}
|
|
2223
|
-
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"
|
|
2224
|
-
>
|
|
2225
|
-
{applyOutcomeUncertain && !displayedVerificationError
|
|
2226
|
-
? "Check current status"
|
|
2227
|
-
: "Check current status again"}
|
|
2228
|
-
</button>
|
|
2229
|
-
</div>
|
|
2365
|
+
</button>
|
|
2230
2366
|
) : null}
|
|
2231
|
-
</
|
|
2232
|
-
|
|
2233
|
-
<AssessmentHistory
|
|
2234
|
-
assessments={assessmentIndexes
|
|
2235
|
-
.filter(
|
|
2236
|
-
(index) =>
|
|
2237
|
-
index !== currentAssessmentIdx &&
|
|
2238
|
-
(index === initialAssessmentIdx ||
|
|
2239
|
-
(turns[index].diagnosis?.remediation?.length ?? 0) >
|
|
2240
|
-
0 ||
|
|
2241
|
-
turns.some(
|
|
2242
|
-
(turn) =>
|
|
2243
|
-
turn.explainAssessment ===
|
|
2244
|
-
turns[index].resultSequence,
|
|
2245
|
-
)),
|
|
2246
|
-
)
|
|
2247
|
-
.map((index) => ({
|
|
2248
|
-
sequence: turns[index].resultSequence ?? index,
|
|
2249
|
-
initial: index === initialAssessmentIdx,
|
|
2250
|
-
diagnosis: turns[index].diagnosis!,
|
|
2251
|
-
explanation: explanationFor(turns[index]),
|
|
2252
|
-
sources: (() => {
|
|
2253
|
-
// Each earlier assessment owns its own items; they
|
|
2254
|
-
// render read-only here and never annotate cards.
|
|
2255
|
-
const resolution =
|
|
2256
|
-
resolveInvestigationRootCauseEvidence(
|
|
2257
|
-
projection,
|
|
2258
|
-
turns[index].diagnosis!.rootCauseEvidence,
|
|
2259
|
-
index,
|
|
2260
|
-
);
|
|
2261
|
-
const earlierCase = resolveInvestigationCase(
|
|
2262
|
-
projection,
|
|
2263
|
-
turns[index].diagnosis,
|
|
2264
|
-
index,
|
|
2265
|
-
);
|
|
2266
|
-
return resolution.links.length ||
|
|
2267
|
-
earlierCase.items.length ? (
|
|
2268
|
-
<AssessmentSources
|
|
2269
|
-
renderedGroupIds={visibleEvidenceGroupIds}
|
|
2270
|
-
resolution={resolution}
|
|
2271
|
-
investigationCase={earlierCase}
|
|
2272
|
-
readOnly
|
|
2273
|
-
onViewSource={viewActivitySource}
|
|
2274
|
-
/>
|
|
2275
|
-
) : undefined;
|
|
2276
|
-
})(),
|
|
2277
|
-
}))}
|
|
2278
|
-
/>
|
|
2367
|
+
</div>
|
|
2279
2368
|
|
|
2280
2369
|
<InvestigationEvidencePane
|
|
2281
2370
|
projection={projection}
|
|
2282
2371
|
rootCauseEvidence={paneResolution}
|
|
2283
|
-
|
|
2284
|
-
|
|
2372
|
+
investigationCase={investigationCase}
|
|
2373
|
+
story={
|
|
2374
|
+
storyShape && currentAssessment?.diagnosis
|
|
2375
|
+
? {
|
|
2376
|
+
report: currentAssessment.diagnosis.report,
|
|
2377
|
+
evidence: currentAssessment.diagnosis.evidence,
|
|
2378
|
+
}
|
|
2379
|
+
: undefined
|
|
2380
|
+
}
|
|
2381
|
+
storyShell={storyShell}
|
|
2285
2382
|
collecting={
|
|
2286
2383
|
explanationRequest?.status !== "running" &&
|
|
2287
2384
|
(requestPending ||
|
|
@@ -2298,55 +2395,10 @@ export function InvestigationView({
|
|
|
2298
2395
|
onOpenTimeline={stale ? undefined : onOpenTimeline}
|
|
2299
2396
|
revealRequest={evidenceRevealRequest}
|
|
2300
2397
|
onRevealReady={revealEvidenceSource}
|
|
2301
|
-
afterEvidence={
|
|
2302
|
-
|
|
2303
|
-
<section
|
|
2304
|
-
ref={nextStepsRef}
|
|
2305
|
-
tabIndex={-1}
|
|
2306
|
-
aria-labelledby={`${workspaceId}-next-steps`}
|
|
2307
|
-
className="investigation-next-steps rounded-xl border p-4 outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
|
|
2308
|
-
>
|
|
2309
|
-
<h2
|
|
2310
|
-
id={`${workspaceId}-next-steps`}
|
|
2311
|
-
className="text-lg font-semibold text-theme-text-primary"
|
|
2312
|
-
>
|
|
2313
|
-
{earlierPlan
|
|
2314
|
-
? "Earlier proposed steps"
|
|
2315
|
-
: "Next steps"}
|
|
2316
|
-
</h2>
|
|
2317
|
-
<ResultCard
|
|
2318
|
-
diagnosis={currentAssessment.diagnosis}
|
|
2319
|
-
section="actions"
|
|
2320
|
-
compactActions
|
|
2321
|
-
actionNotice={
|
|
2322
|
-
earlierPlan
|
|
2323
|
-
? assessmentNeedsCurrentStateVerification
|
|
2324
|
-
? "Proposed before the apply attempt. Current state has not been verified."
|
|
2325
|
-
: "Proposed before the latest evidence. Reassess before applying."
|
|
2326
|
-
: undefined
|
|
2327
|
-
}
|
|
2328
|
-
onApply={
|
|
2329
|
-
canOfferInvestigationApply({
|
|
2330
|
-
currentAssessmentIdx,
|
|
2331
|
-
lastRemediationIdx,
|
|
2332
|
-
lastApplyAttemptIdx,
|
|
2333
|
-
localApplyAttemptAssessmentIdx,
|
|
2334
|
-
interactionsBlocked,
|
|
2335
|
-
hosted,
|
|
2336
|
-
hasNewerEvidence:
|
|
2337
|
-
hasEvidenceCollectedAfterAssessment,
|
|
2338
|
-
})
|
|
2339
|
-
? requestApply
|
|
2340
|
-
: undefined
|
|
2341
|
-
}
|
|
2342
|
-
animate={currentAssessment.animateResult !== false}
|
|
2343
|
-
showDisclaimer={false}
|
|
2344
|
-
/>
|
|
2345
|
-
</section>
|
|
2346
|
-
) : undefined
|
|
2347
|
-
}
|
|
2398
|
+
afterEvidence={nextStepsSection}
|
|
2399
|
+
recordNotes={recordNotes}
|
|
2348
2400
|
/>
|
|
2349
|
-
|
|
2401
|
+
</div>
|
|
2350
2402
|
)}
|
|
2351
2403
|
</div>
|
|
2352
2404
|
</div>
|
|
@@ -2361,105 +2413,17 @@ export function InvestigationView({
|
|
|
2361
2413
|
resourceLabel={formatInvestigationTarget(run)}
|
|
2362
2414
|
context={run.context}
|
|
2363
2415
|
fix={pendingFix}
|
|
2416
|
+
reason={currentAssessment?.diagnosis?.recommendedReason}
|
|
2417
|
+
precondition={
|
|
2418
|
+
currentAssessment?.diagnosis?.recommendedIndex
|
|
2419
|
+
? currentAssessment.diagnosis.steps?.[
|
|
2420
|
+
currentAssessment.diagnosis.recommendedIndex - 1
|
|
2421
|
+
]?.precondition
|
|
2422
|
+
: undefined
|
|
2423
|
+
}
|
|
2364
2424
|
managedBy={run.managedBy}
|
|
2365
2425
|
confidence={turns[lastRemediationIdx]?.diagnosis?.confidence}
|
|
2366
2426
|
/>
|
|
2367
2427
|
</div>
|
|
2368
2428
|
);
|
|
2369
2429
|
}
|
|
2370
|
-
|
|
2371
|
-
function AssessmentHistory({
|
|
2372
|
-
assessments,
|
|
2373
|
-
}: {
|
|
2374
|
-
assessments: {
|
|
2375
|
-
sequence: number;
|
|
2376
|
-
diagnosis: Diagnosis;
|
|
2377
|
-
explanation?: AssessmentExplanation;
|
|
2378
|
-
initial: boolean;
|
|
2379
|
-
sources?: ReactNode;
|
|
2380
|
-
}[];
|
|
2381
|
-
}) {
|
|
2382
|
-
const reveal = useDisclosureReveal<HTMLDivElement>();
|
|
2383
|
-
const { revealAfterToggle } = reveal;
|
|
2384
|
-
const [open, setOpen] = useState(false);
|
|
2385
|
-
const regionId = useId();
|
|
2386
|
-
const openRequest = assessments.find((item) => item.explanation?.openRequest)
|
|
2387
|
-
?.explanation?.openRequest;
|
|
2388
|
-
useEffect(() => {
|
|
2389
|
-
if (openRequest) {
|
|
2390
|
-
setOpen(true);
|
|
2391
|
-
revealAfterToggle(true);
|
|
2392
|
-
}
|
|
2393
|
-
}, [openRequest, revealAfterToggle]);
|
|
2394
|
-
if (assessments.length === 0) return null;
|
|
2395
|
-
return (
|
|
2396
|
-
<div data-investigation-assessment-history className="overflow-hidden">
|
|
2397
|
-
<button
|
|
2398
|
-
type="button"
|
|
2399
|
-
aria-expanded={open}
|
|
2400
|
-
aria-controls={regionId}
|
|
2401
|
-
onClick={() => {
|
|
2402
|
-
setOpen(!open);
|
|
2403
|
-
reveal.revealAfterToggle(!open);
|
|
2404
|
-
}}
|
|
2405
|
-
className="flex items-center gap-2 rounded-md px-2 py-1 text-left text-xs text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-secondary"
|
|
2406
|
-
>
|
|
2407
|
-
<CollapseChevron open={open} className="h-3.5 w-3.5" />
|
|
2408
|
-
<span className="font-medium">
|
|
2409
|
-
Previous assessments · {assessments.length}
|
|
2410
|
-
</span>
|
|
2411
|
-
</button>
|
|
2412
|
-
<div id={regionId} ref={reveal.elementRef}>
|
|
2413
|
-
<Collapse open={open}>
|
|
2414
|
-
<div className="mt-2 space-y-4 border-l border-theme-border pl-3 pb-2">
|
|
2415
|
-
{assessments.map(
|
|
2416
|
-
(
|
|
2417
|
-
{ sequence, diagnosis, explanation, initial, sources },
|
|
2418
|
-
index,
|
|
2419
|
-
) => (
|
|
2420
|
-
<section
|
|
2421
|
-
key={sequence}
|
|
2422
|
-
aria-label={
|
|
2423
|
-
initial
|
|
2424
|
-
? "Initial assessment"
|
|
2425
|
-
: `Earlier assessment ${index + 1}`
|
|
2426
|
-
}
|
|
2427
|
-
>
|
|
2428
|
-
<h3 className="text-sm font-medium text-theme-text-secondary">
|
|
2429
|
-
{initial
|
|
2430
|
-
? "Initial assessment"
|
|
2431
|
-
: `Earlier assessment ${index + 1}`}
|
|
2432
|
-
</h3>
|
|
2433
|
-
<ResultCard
|
|
2434
|
-
diagnosis={diagnosis}
|
|
2435
|
-
explanation={explanation}
|
|
2436
|
-
assessmentSources={sources}
|
|
2437
|
-
section="conclusion"
|
|
2438
|
-
showDisclaimer={false}
|
|
2439
|
-
/>
|
|
2440
|
-
{(diagnosis.remediation?.length ?? 0) > 0 && (
|
|
2441
|
-
<section
|
|
2442
|
-
aria-label="Earlier proposed steps"
|
|
2443
|
-
className="mt-3 border-t border-theme-border/60 pt-3"
|
|
2444
|
-
>
|
|
2445
|
-
<h3 className="text-sm font-medium text-theme-text-primary">
|
|
2446
|
-
Earlier proposed steps
|
|
2447
|
-
</h3>
|
|
2448
|
-
<ResultCard
|
|
2449
|
-
diagnosis={diagnosis}
|
|
2450
|
-
section="actions"
|
|
2451
|
-
compactActions
|
|
2452
|
-
actionNotice="From an earlier assessment, not the current recommendation."
|
|
2453
|
-
showDisclaimer={false}
|
|
2454
|
-
/>
|
|
2455
|
-
</section>
|
|
2456
|
-
)}
|
|
2457
|
-
</section>
|
|
2458
|
-
),
|
|
2459
|
-
)}
|
|
2460
|
-
</div>
|
|
2461
|
-
</Collapse>
|
|
2462
|
-
</div>
|
|
2463
|
-
</div>
|
|
2464
|
-
);
|
|
2465
|
-
}
|