@skyhook-io/radar-app 1.14.4 → 1.14.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/radar-app",
3
- "version": "1.14.4",
3
+ "version": "1.14.5",
4
4
  "description": "Radar's full web UI as a reusable React component. Used by Radar's own binary and by external consumers like Radar Cloud.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
1
+ import { useState, type ComponentProps } from "react";
2
2
  import { AlertTriangle, Copy, Check } from "lucide-react";
3
3
  import { Markdown } from "../ui/Markdown";
4
4
  import { Tooltip } from "../ui/Tooltip";
@@ -59,7 +59,7 @@ export function CopyButton({ text, label }: { text: string; label: string }) {
59
59
  // it as a fence — it leaks the literal ``` and renders an empty code box. Coerce
60
60
  // fence markers onto their own lines and push trailing content off the opener so
61
61
  // the block renders. (Well-formed markdown is unaffected.)
62
- function tidyFences(md: string): string {
62
+ export function tidyFences(md: string): string {
63
63
  if (!md || !md.includes("```")) return md;
64
64
  return md
65
65
  .replace(/([^\n])```/g, "$1\n\n```") // opener/closer must start a line
@@ -75,12 +75,17 @@ const SOFT_INLINE_CODE =
75
75
  export function AIMarkdown({
76
76
  className,
77
77
  children,
78
+ codeActions,
78
79
  }: {
79
80
  className?: string;
80
81
  children: string;
82
+ codeActions?: ComponentProps<typeof Markdown>["codeActions"];
81
83
  }) {
82
84
  return (
83
- <Markdown className={`${SOFT_INLINE_CODE} ${className ?? ""}`}>
85
+ <Markdown
86
+ className={`${SOFT_INLINE_CODE} ${className ?? ""}`}
87
+ codeActions={codeActions}
88
+ >
84
89
  {tidyFences(children)}
85
90
  </Markdown>
86
91
  );
@@ -35,7 +35,7 @@ import {
35
35
  STEP_KIND_LABEL,
36
36
  assessmentCopyText,
37
37
  } from "./assessmentCopy";
38
- import { AIMarkdown, CopyButton } from "./AIMarkdown";
38
+ import { AIMarkdown, CopyButton, tidyFences } from "./AIMarkdown";
39
39
  import { prettyTool } from "./toolCallLabel";
40
40
 
41
41
  export function ResultCard({
@@ -134,6 +134,7 @@ export function ResultCard({
134
134
  return section === "actions" ? null : (
135
135
  <AllClearCard
136
136
  diagnosis={diagnosis}
137
+ explanation={explanation}
137
138
  animate={animate}
138
139
  showDisclaimer={showDisclaimer}
139
140
  coverageLimited={coverageLimited}
@@ -170,6 +171,9 @@ export function ResultCard({
170
171
  <>
171
172
  <InconclusiveCard
172
173
  diagnosis={diagnosis}
174
+ explanation={explanation}
175
+ assessmentSources={assessmentSources}
176
+ assessmentAction={assessmentAction}
173
177
  animate={animate}
174
178
  storyInline={storyInline}
175
179
  revisedAfter={revisedAfter}
@@ -188,12 +192,6 @@ export function ResultCard({
188
192
  readOnlyAssessment={readOnlyAssessment}
189
193
  />
190
194
  ) : null}
191
- {assessmentSources ? (
192
- <AssessmentSourceDetails>{assessmentSources}</AssessmentSourceDetails>
193
- ) : null}
194
- {assessmentAction && (
195
- <div className="mt-2 flex justify-end">{assessmentAction}</div>
196
- )}
197
195
  </>
198
196
  );
199
197
  }
@@ -675,8 +673,201 @@ export type AssessmentExplanation = {
675
673
  openRequest?: number;
676
674
  };
677
675
 
678
- // The diagnosis result: likely cause + remediation + the
679
- // agent's full analysis on demand.
676
+ function AssessmentDetails({
677
+ diagnosis,
678
+ explanation,
679
+ assessmentAction,
680
+ assessmentSources,
681
+ showAnalysisDisclosure = false,
682
+ showConfidence = false,
683
+ analysisText = diagnosis.report,
684
+ }: {
685
+ diagnosis: Diagnosis;
686
+ explanation?: AssessmentExplanation;
687
+ assessmentAction?: ReactNode;
688
+ assessmentSources?: ReactNode;
689
+ showAnalysisDisclosure?: boolean;
690
+ showConfidence?: boolean;
691
+ analysisText?: string;
692
+ }) {
693
+ const [detail, setDetail] = useState<"analysis" | "explanation" | null>(
694
+ explanation?.status === "running" ? "explanation" : null,
695
+ );
696
+ const showAnalysis = detail === "analysis";
697
+ const analysisReveal = useDisclosureReveal<HTMLDivElement>();
698
+ const { elementRef: analysisElementRef, revealAfterToggle: revealAnalysis } =
699
+ analysisReveal;
700
+ useEffect(() => {
701
+ if (explanation?.openRequest) {
702
+ setDetail("explanation");
703
+ revealAnalysis(true);
704
+ }
705
+ }, [explanation?.openRequest, revealAnalysis]);
706
+ useEffect(() => {
707
+ if (
708
+ detail === "explanation" &&
709
+ (explanation?.status === "done" || explanation?.status === "error")
710
+ ) {
711
+ const element = analysisElementRef.current;
712
+ const scroller = element?.closest("[data-investigation-findings-scroll]");
713
+ if (element && scroller) {
714
+ const top = element.getBoundingClientRect().top;
715
+ const viewport = scroller.getBoundingClientRect();
716
+ if (top >= viewport.top && top < viewport.bottom) revealAnalysis(true);
717
+ }
718
+ }
719
+ }, [explanation?.status, detail, analysisElementRef, revealAnalysis]);
720
+ const analysisId = useId();
721
+ return (
722
+ <>
723
+ {/* Full analysis — the agent's detailed evidence, on demand. Under the
724
+ story contract Findings renders the story itself, so only the
725
+ explanation and sources remain here. */}
726
+ {((showAnalysisDisclosure && diagnosis.report) ||
727
+ (showAnalysisDisclosure && diagnosis.confidence != null) ||
728
+ explanation ||
729
+ assessmentAction ||
730
+ assessmentSources) && (
731
+ <div>
732
+ <div
733
+ className="flex flex-wrap items-center gap-x-3 gap-y-1 pt-2"
734
+ data-assessment-actions
735
+ >
736
+ {((showAnalysisDisclosure && diagnosis.report) ||
737
+ (showAnalysisDisclosure && diagnosis.confidence != null) ||
738
+ assessmentSources) && (
739
+ <button
740
+ type="button"
741
+ aria-expanded={showAnalysis}
742
+ aria-controls={`${analysisId}-analysis`}
743
+ onClick={() => {
744
+ setDetail(showAnalysis ? null : "analysis");
745
+ analysisReveal.revealAfterToggle(!showAnalysis);
746
+ }}
747
+ className="flex items-center gap-1.5 rounded-md py-2 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary"
748
+ >
749
+ <CollapseChevron open={showAnalysis} className="h-3.5 w-3.5" />
750
+ {showAnalysisDisclosure && diagnosis.report
751
+ ? "Full analysis"
752
+ : "Assessment details"}
753
+ </button>
754
+ )}
755
+ {explanation && (
756
+ <Tooltip
757
+ content={
758
+ explanation.status === "idle"
759
+ ? explanation.onGenerate
760
+ ? "Ask the agent to explain this assessment and its proposed next steps in plain language."
761
+ : "Wait for the current agent request to finish before requesting an explanation."
762
+ : explanation.status === "running"
763
+ ? "The agent is preparing an explanation. You can close this and return while it runs."
764
+ : explanation.status === "error"
765
+ ? "View the explanation error and retry when the agent is available."
766
+ : "Show the saved plain-language explanation. No new request is needed."
767
+ }
768
+ >
769
+ <button
770
+ type="button"
771
+ aria-expanded={detail === "explanation"}
772
+ aria-controls={`${analysisId}-explanation`}
773
+ disabled={
774
+ explanation.status === "idle" && !explanation.onGenerate
775
+ }
776
+ onClick={() => {
777
+ const open = detail !== "explanation";
778
+ setDetail(open ? "explanation" : null);
779
+ analysisReveal.revealAfterToggle(open);
780
+ if (open && explanation.status === "idle")
781
+ explanation.onGenerate?.();
782
+ }}
783
+ className="inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-xs font-medium text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary disabled:opacity-50"
784
+ >
785
+ <HelpCircle className="h-3 w-3" />
786
+ Explain simply
787
+ </button>
788
+ </Tooltip>
789
+ )}
790
+ {assessmentAction && (
791
+ <div className="ml-auto">{assessmentAction}</div>
792
+ )}
793
+ </div>
794
+ <div id={analysisId} ref={analysisReveal.elementRef}>
795
+ <div id={`${analysisId}-analysis`}>
796
+ <Collapse open={detail === "analysis"} mountLazily>
797
+ <div className="border-t border-theme-border/60 px-3 py-2">
798
+ {showAnalysisDisclosure ? (
799
+ <>
800
+ {showConfidence && (
801
+ <p className="mb-2 text-xs text-theme-text-tertiary">
802
+ Agent confidence:{" "}
803
+ {diagnosis.confidence != null
804
+ ? confidenceLabel(diagnosis.confidence)
805
+ : "not stated"}
806
+ {diagnosis.confidence != null
807
+ ? " · self-reported"
808
+ : ""}
809
+ </p>
810
+ )}
811
+ <AIMarkdown className="text-sm [overflow-wrap:anywhere] [&_h2:first-child]:mt-0 [&_h2]:mb-1.5 [&_h2]:mt-3 [&_h2]:text-xs [&_h2]:font-semibold [&_h2]:uppercase [&_h2]:tracking-wide [&_h2]:text-theme-text-tertiary [&_h3]:text-sm [&_li]:text-theme-text-secondary [&_p]:my-1.5 [&_p]:text-theme-text-secondary">
812
+ {analysisText}
813
+ </AIMarkdown>
814
+ </>
815
+ ) : null}
816
+ {assessmentSources}
817
+ </div>
818
+ </Collapse>
819
+ </div>
820
+ <div id={`${analysisId}-explanation`}>
821
+ <Collapse open={detail === "explanation"} mountLazily>
822
+ <div className="border-t border-theme-border/60 px-3 py-2">
823
+ {explanation?.status === "running" ? (
824
+ <div
825
+ role="status"
826
+ className="flex items-center gap-2 py-2 text-sm text-theme-text-secondary"
827
+ >
828
+ <Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
829
+ Explaining this assessment…
830
+ </div>
831
+ ) : explanation?.status === "done" ? (
832
+ <div className="flex items-start gap-2">
833
+ <AIMarkdown className="min-w-0 flex-1 text-sm text-theme-text-secondary [overflow-wrap:anywhere]">
834
+ {explanation.text || ""}
835
+ </AIMarkdown>
836
+ <CopyButton
837
+ text={explanation.text || ""}
838
+ label="Copy explanation"
839
+ />
840
+ </div>
841
+ ) : explanation?.status === "error" ? (
842
+ <div
843
+ role="alert"
844
+ className="flex flex-wrap items-center gap-2 py-2 text-sm text-theme-text-secondary"
845
+ >
846
+ <span>
847
+ {explanation.error ||
848
+ "The agent did not return an explanation."}
849
+ </span>
850
+ {explanation.onGenerate && (
851
+ <button
852
+ type="button"
853
+ onClick={explanation.onGenerate}
854
+ className="rounded-md px-2 py-1 text-xs font-medium text-accent-text hover:bg-theme-hover"
855
+ >
856
+ Try again
857
+ </button>
858
+ )}
859
+ </div>
860
+ ) : null}
861
+ </div>
862
+ </Collapse>
863
+ </div>
864
+ </div>
865
+ </div>
866
+ )}
867
+ </>
868
+ );
869
+ }
870
+
680
871
  function DiagnosisResult({
681
872
  diagnosis,
682
873
  onApply,
@@ -719,37 +910,9 @@ function DiagnosisResult({
719
910
  ? storyPlainText(diagnosis.report)
720
911
  : diagnosis.report;
721
912
  const showAnalysisDisclosure = !storyShape || storyInline;
722
- const [detail, setDetail] = useState<"analysis" | "explanation" | null>(
723
- explanation?.status === "running" ? "explanation" : null,
724
- );
725
- const showAnalysis = detail === "analysis";
726
- const analysisReveal = useDisclosureReveal<HTMLDivElement>();
727
- const { elementRef: analysisElementRef, revealAfterToggle: revealAnalysis } =
728
- analysisReveal;
729
- useEffect(() => {
730
- if (explanation?.openRequest) {
731
- setDetail("explanation");
732
- revealAnalysis(true);
733
- }
734
- }, [explanation?.openRequest, revealAnalysis]);
735
- useEffect(() => {
736
- if (
737
- detail === "explanation" &&
738
- (explanation?.status === "done" || explanation?.status === "error")
739
- ) {
740
- const element = analysisElementRef.current;
741
- const scroller = element?.closest("[data-investigation-findings-scroll]");
742
- if (element && scroller) {
743
- const top = element.getBoundingClientRect().top;
744
- const viewport = scroller.getBoundingClientRect();
745
- if (top >= viewport.top && top < viewport.bottom) revealAnalysis(true);
746
- }
747
- }
748
- }, [explanation?.status, detail, analysisElementRef, revealAnalysis]);
749
913
  const [showAllSteps, setShowAllSteps] = useState(false);
750
914
  const stepsId = useId();
751
915
  const stepsReveal = useDisclosureReveal<HTMLDivElement>();
752
- const analysisId = useId();
753
916
  // Only a real structured cause anchors the amber card; the full prose lives in
754
917
  // "Full analysis" (never relabel the report as a causal assessment).
755
918
  const rootCause = diagnosis.rootCause;
@@ -784,6 +947,7 @@ function DiagnosisResult({
784
947
  }) => {
785
948
  const isRec = recValid && i === recIdx! - 1;
786
949
  const step = typedSteps ? steps[i] : undefined;
950
+ const commands = remediationCommands(r);
787
951
  return (
788
952
  <div
789
953
  key={i}
@@ -842,17 +1006,6 @@ function DiagnosisResult({
842
1006
  Apply…
843
1007
  </button>
844
1008
  )}
845
- {remediationCommands(r).map((command, c, all) => (
846
- <CopyButton
847
- key={c}
848
- text={command}
849
- label={
850
- all.length > 1
851
- ? `Copy command ${c + 1} of step ${i + 1}`
852
- : `Copy command from step ${i + 1}`
853
- }
854
- />
855
- ))}
856
1009
  </div>
857
1010
  </div>
858
1011
  {/* The condition and the reason are read before the command is
@@ -873,7 +1026,19 @@ function DiagnosisResult({
873
1026
  {diagnosis.recommendedReason}
874
1027
  </p>
875
1028
  )}
876
- <AIMarkdown className="max-w-[100ch] text-sm [overflow-wrap:anywhere] [&_p]:my-0 [&_pre]:my-1.5">
1029
+ <AIMarkdown
1030
+ className="max-w-[100ch] text-sm [overflow-wrap:anywhere] [&_p]:my-0 [&_pre]:my-1.5"
1031
+ codeActions={(code) => {
1032
+ const command = code.trim();
1033
+ if (!commands.includes(command)) return null;
1034
+ return (
1035
+ <CopyButton
1036
+ text={command}
1037
+ label={`Copy command from step ${i + 1}`}
1038
+ />
1039
+ );
1040
+ }}
1041
+ >
877
1042
  {r}
878
1043
  </AIMarkdown>
879
1044
  </div>
@@ -1034,152 +1199,17 @@ function DiagnosisResult({
1034
1199
  </div>
1035
1200
  )}
1036
1201
 
1037
- {/* Full analysis — the agent's detailed evidence, on demand. Under the
1038
- story contract Findings renders the story itself, so only the
1039
- explanation and sources remain here. */}
1040
- {showConclusion &&
1041
- ((showAnalysisDisclosure && diagnosis.report) ||
1042
- (showAnalysisDisclosure && diagnosis.confidence != null) ||
1043
- explanation ||
1044
- assessmentAction ||
1045
- assessmentSources) && (
1046
- <div>
1047
- <div
1048
- className="flex flex-wrap items-center gap-x-3 gap-y-1 pt-2"
1049
- data-assessment-actions
1050
- >
1051
- {((showAnalysisDisclosure && diagnosis.report) ||
1052
- (showAnalysisDisclosure && diagnosis.confidence != null) ||
1053
- assessmentSources) && (
1054
- <button
1055
- type="button"
1056
- aria-expanded={showAnalysis}
1057
- aria-controls={`${analysisId}-analysis`}
1058
- onClick={() => {
1059
- setDetail(showAnalysis ? null : "analysis");
1060
- analysisReveal.revealAfterToggle(!showAnalysis);
1061
- }}
1062
- className="flex items-center gap-1.5 rounded-md py-2 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary"
1063
- >
1064
- <CollapseChevron
1065
- open={showAnalysis}
1066
- className="h-3.5 w-3.5"
1067
- />
1068
- {showAnalysisDisclosure && diagnosis.report
1069
- ? "Full analysis"
1070
- : "Assessment details"}
1071
- </button>
1072
- )}
1073
- {explanation && (
1074
- <Tooltip
1075
- content={
1076
- explanation.status === "idle"
1077
- ? explanation.onGenerate
1078
- ? "Ask the agent to explain this assessment and its proposed next steps in plain language."
1079
- : "Wait for the current agent request to finish before requesting an explanation."
1080
- : explanation.status === "running"
1081
- ? "The agent is preparing an explanation. You can close this and return while it runs."
1082
- : explanation.status === "error"
1083
- ? "View the explanation error and retry when the agent is available."
1084
- : "Show the saved plain-language explanation. No new request is needed."
1085
- }
1086
- >
1087
- <button
1088
- type="button"
1089
- aria-expanded={detail === "explanation"}
1090
- aria-controls={`${analysisId}-explanation`}
1091
- disabled={
1092
- explanation.status === "idle" && !explanation.onGenerate
1093
- }
1094
- onClick={() => {
1095
- const open = detail !== "explanation";
1096
- setDetail(open ? "explanation" : null);
1097
- analysisReveal.revealAfterToggle(open);
1098
- if (open && explanation.status === "idle")
1099
- explanation.onGenerate?.();
1100
- }}
1101
- className="inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-xs font-medium text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary disabled:opacity-50"
1102
- >
1103
- <HelpCircle className="h-3 w-3" />
1104
- Explain simply
1105
- </button>
1106
- </Tooltip>
1107
- )}
1108
- {assessmentAction && (
1109
- <div className="ml-auto">{assessmentAction}</div>
1110
- )}
1111
- </div>
1112
- <div id={analysisId} ref={analysisReveal.elementRef}>
1113
- <div id={`${analysisId}-analysis`}>
1114
- <Collapse open={detail === "analysis"} mountLazily>
1115
- <div className="border-t border-theme-border/60 px-3 py-2">
1116
- {showAnalysisDisclosure ? (
1117
- <>
1118
- <p className="mb-2 text-xs text-theme-text-tertiary">
1119
- Agent confidence:{" "}
1120
- {diagnosis.confidence != null
1121
- ? confidenceLabel(diagnosis.confidence)
1122
- : "not stated"}
1123
- {diagnosis.confidence != null
1124
- ? " · self-reported"
1125
- : ""}
1126
- </p>
1127
- <AIMarkdown className="text-sm [overflow-wrap:anywhere] [&_h2:first-child]:mt-0 [&_h2]:mb-1.5 [&_h2]:mt-3 [&_h2]:text-xs [&_h2]:font-semibold [&_h2]:uppercase [&_h2]:tracking-wide [&_h2]:text-theme-text-tertiary [&_h3]:text-sm [&_li]:text-theme-text-secondary [&_p]:my-1.5 [&_p]:text-theme-text-secondary">
1128
- {analysisText}
1129
- </AIMarkdown>
1130
- </>
1131
- ) : null}
1132
- {assessmentSources}
1133
- </div>
1134
- </Collapse>
1135
- </div>
1136
- <div id={`${analysisId}-explanation`}>
1137
- <Collapse open={detail === "explanation"} mountLazily>
1138
- <div className="border-t border-theme-border/60 px-3 py-2">
1139
- {explanation?.status === "running" ? (
1140
- <div
1141
- role="status"
1142
- className="flex items-center gap-2 py-2 text-sm text-theme-text-secondary"
1143
- >
1144
- <Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
1145
- Explaining this assessment…
1146
- </div>
1147
- ) : explanation?.status === "done" ? (
1148
- <div className="flex items-start gap-2">
1149
- <AIMarkdown className="min-w-0 flex-1 text-sm text-theme-text-secondary [overflow-wrap:anywhere]">
1150
- {explanation.text || ""}
1151
- </AIMarkdown>
1152
- <CopyButton
1153
- text={explanation.text || ""}
1154
- label="Copy explanation"
1155
- />
1156
- </div>
1157
- ) : explanation?.status === "error" ? (
1158
- <div
1159
- role="alert"
1160
- className="flex flex-wrap items-center gap-2 py-2 text-sm text-theme-text-secondary"
1161
- >
1162
- <span>
1163
- {explanation.error ||
1164
- "The agent did not return an explanation."}
1165
- </span>
1166
- {explanation.onGenerate && (
1167
- <button
1168
- type="button"
1169
- onClick={explanation.onGenerate}
1170
- className="rounded-md px-2 py-1 text-xs font-medium text-accent-text hover:bg-theme-hover"
1171
- >
1172
- Try again
1173
- </button>
1174
- )}
1175
- </div>
1176
- ) : null}
1177
- </div>
1178
- </Collapse>
1179
- </div>
1180
- </div>
1181
- </div>
1182
- )}
1202
+ {showConclusion && (
1203
+ <AssessmentDetails
1204
+ diagnosis={diagnosis}
1205
+ explanation={explanation}
1206
+ assessmentAction={assessmentAction}
1207
+ assessmentSources={assessmentSources}
1208
+ showAnalysisDisclosure={showAnalysisDisclosure}
1209
+ showConfidence
1210
+ analysisText={analysisText}
1211
+ />
1212
+ )}
1183
1213
 
1184
1214
  {showConclusion && showDisclaimer && (
1185
1215
  <div className="flex items-start gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
@@ -1204,6 +1234,7 @@ function healthFlagSentence(flag: {
1204
1234
 
1205
1235
  export function AllClearCard({
1206
1236
  diagnosis,
1237
+ explanation,
1207
1238
  animate,
1208
1239
  showDisclaimer,
1209
1240
  coverageLimited,
@@ -1222,6 +1253,7 @@ export function AllClearCard({
1222
1253
  animate: boolean;
1223
1254
  showDisclaimer: boolean;
1224
1255
  coverageLimited: boolean;
1256
+ explanation?: AssessmentExplanation;
1225
1257
  evidenceConflict: boolean;
1226
1258
  evidenceConflictExplainedBy?: string[];
1227
1259
  assessmentAction?: ReactNode;
@@ -1236,9 +1268,6 @@ export function AllClearCard({
1236
1268
  onRevealSource?: (sourceId: string) => void;
1237
1269
  }) {
1238
1270
  const storyShape = diagnosisHasStoryShape(diagnosis);
1239
- const [showAnalysis, setShowAnalysis] = useState(false);
1240
- const analysisReveal = useDisclosureReveal<HTMLDivElement>();
1241
- const analysisId = useId();
1242
1271
  const report =
1243
1272
  (storyShape && !storyInline ? "" : storyPlainText(diagnosis.report)) ||
1244
1273
  (storyShape
@@ -1283,43 +1312,14 @@ export function AllClearCard({
1283
1312
  // the disclaimer are the same in both shapes.
1284
1313
  const trailing = (
1285
1314
  <>
1286
- {detailed || assessmentAction || assessmentSources ? (
1287
- <div>
1288
- <div
1289
- className="flex flex-wrap items-center gap-3 pt-2"
1290
- data-assessment-actions
1291
- >
1292
- {(detailed || assessmentSources) && (
1293
- <button
1294
- type="button"
1295
- aria-expanded={showAnalysis}
1296
- aria-controls={analysisId}
1297
- onClick={() => {
1298
- setShowAnalysis(!showAnalysis);
1299
- analysisReveal.revealAfterToggle(!showAnalysis);
1300
- }}
1301
- className="flex items-center gap-1.5 rounded-md py-2 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary"
1302
- >
1303
- <CollapseChevron open={showAnalysis} className="h-3.5 w-3.5" />
1304
- {detailed ? "Full analysis" : "Assessment details"}
1305
- </button>
1306
- )}
1307
- {assessmentAction && (
1308
- <div className="ml-auto">{assessmentAction}</div>
1309
- )}
1310
- </div>
1311
- <div id={analysisId} ref={analysisReveal.elementRef}>
1312
- <Collapse open={showAnalysis}>
1313
- <div className="border-t border-theme-border/60 px-3 py-2">
1314
- <AIMarkdown className="text-sm [overflow-wrap:anywhere] [&_p]:my-1.5 [&_p]:text-theme-text-secondary [&_p:first-child]:mt-0 [&_p:last-child]:mb-0">
1315
- {detailed ? report : ""}
1316
- </AIMarkdown>
1317
- {assessmentSources}
1318
- </div>
1319
- </Collapse>
1320
- </div>
1321
- </div>
1322
- ) : null}
1315
+ <AssessmentDetails
1316
+ diagnosis={diagnosis}
1317
+ explanation={explanation}
1318
+ assessmentAction={assessmentAction}
1319
+ assessmentSources={assessmentSources}
1320
+ showAnalysisDisclosure={detailed}
1321
+ analysisText={report}
1322
+ />
1323
1323
  {showDisclaimer ? (
1324
1324
  <div className="flex items-start gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
1325
1325
  <ShieldCheck className="mt-0.5 h-3 w-3 shrink-0" />
@@ -1442,6 +1442,9 @@ export function AllClearCard({
1442
1442
  // all-clear) — so "I couldn't tell" never reads as "you're fine."
1443
1443
  export function InconclusiveCard({
1444
1444
  diagnosis,
1445
+ explanation,
1446
+ assessmentSources,
1447
+ assessmentAction,
1445
1448
  animate,
1446
1449
  storyInline = false,
1447
1450
  revisedAfter,
@@ -1451,7 +1454,10 @@ export function InconclusiveCard({
1451
1454
  diagnosis: Diagnosis;
1452
1455
  animate: boolean;
1453
1456
  storyInline?: boolean;
1457
+ explanation?: AssessmentExplanation;
1454
1458
  revisedAfter?: string;
1459
+ assessmentSources?: ReactNode;
1460
+ assessmentAction?: ReactNode;
1455
1461
  /** Reads Radar could not complete for this assessment; listed under Still open. */
1456
1462
  assessmentLimits?: string[];
1457
1463
  assessmentCopy?: Pick<AssessmentCopyRadar, "context" | "receipts">;
@@ -1496,6 +1502,12 @@ export function InconclusiveCard({
1496
1502
  addressing any errors shown in Activity.
1497
1503
  </span>
1498
1504
  </div>
1505
+ <AssessmentDetails
1506
+ diagnosis={diagnosis}
1507
+ explanation={explanation}
1508
+ assessmentSources={assessmentSources}
1509
+ assessmentAction={assessmentAction}
1510
+ />
1499
1511
  </div>
1500
1512
  );
1501
1513
  }
@@ -1547,10 +1559,10 @@ export function remediationHeadline(step: string): string {
1547
1559
  */
1548
1560
  export function remediationCommands(step: string): string[] {
1549
1561
  const commands: string[] = [];
1550
- // One pass in reading order, so button N is the Nth command in the text.
1562
+ const normalized = tidyFences(step);
1551
1563
  const code = /```[a-zA-Z]*\n([\s\S]*?)```|`([^`\n]+)`/g;
1552
1564
  let match: RegExpExecArray | null;
1553
- while ((match = code.exec(step))) {
1565
+ while ((match = code.exec(normalized))) {
1554
1566
  if (match[1] !== undefined) {
1555
1567
  const body = match[1].trim();
1556
1568
  if (body) commands.push(body);