@skyhook-io/radar-app 1.14.3 → 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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/api/client.ts +2 -0
  3. package/src/api/diagnose.ts +7 -1
  4. package/src/components/diagnose/AIMarkdown.tsx +8 -3
  5. package/src/components/diagnose/ActivityTurn.tsx +1 -1
  6. package/src/components/diagnose/AssessmentCard.tsx +269 -238
  7. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +47 -0
  8. package/src/components/diagnose/InvestigationEvidencePane.tsx +15 -2
  9. package/src/components/diagnose/InvestigationEvidenceSections.tsx +1 -1
  10. package/src/components/diagnose/InvestigationView.tsx +32 -24
  11. package/src/components/diagnose/investigationCase.test.tsx +83 -0
  12. package/src/components/diagnose/investigationCase.ts +3 -2
  13. package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +22 -0
  14. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +8 -2
  15. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +1 -1
  16. package/src/components/diagnose/investigationEvidence/adapters/listings.ts +2 -0
  17. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +60 -0
  18. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +7 -3
  19. package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +1 -1
  20. package/src/components/diagnose/investigationEvidence/observations.ts +10 -5
  21. package/src/components/diagnose/investigationEvidence/types.ts +1 -1
  22. package/src/components/diagnose/investigationState.test.ts +2 -2
  23. package/src/components/diagnose/investigationState.ts +2 -2
  24. package/src/components/diagnose/investigationStory.ts +1 -1
  25. package/src/components/diagnose/parts.test.tsx +153 -2
  26. package/src/components/home/mcpToolCatalog.ts +27 -0
  27. package/src/components/rightsizing/RightsizingScanView.tsx +194 -63
  28. package/src/components/rightsizing/empty-state.test.tsx +67 -0
  29. package/src/components/rightsizing/model.test.ts +118 -1
  30. package/src/components/rightsizing/model.ts +36 -13
  31. package/src/components/rightsizing/notices.test.tsx +175 -0
  32. package/src/components/ui/Markdown.tsx +33 -6
  33. package/src/components/ui/Omnibar.tsx +82 -69
  34. package/src/components/workload/WorkloadView.tsx +7 -1
@@ -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
  }
@@ -446,6 +444,7 @@ export function AssessmentSources({
446
444
  investigationCase,
447
445
  unlinkedEvidence = 0,
448
446
  evidenceMalformed = false,
447
+ omittedEntries = 0,
449
448
  readOnly = false,
450
449
  renderedGroupIds,
451
450
  onViewSource,
@@ -464,6 +463,12 @@ export function AssessmentSources({
464
463
  * and no count describes how many were lost.
465
464
  */
466
465
  evidenceMalformed?: boolean;
466
+ /**
467
+ * Next steps, open items and ruled-out hypotheses the server left out of
468
+ * the record because the list ran past the page's limit. They were valid;
469
+ * the reader learns the list was longer than what is shown.
470
+ */
471
+ omittedEntries?: number;
467
472
  readOnly?: boolean;
468
473
  /**
469
474
  * Groups the Evidence pane actually rendered. A card-placed note whose card
@@ -475,7 +480,12 @@ export function AssessmentSources({
475
480
  onViewSource: (sourceId: string) => void;
476
481
  }) {
477
482
  const rows = assessmentSourceRows(resolution, investigationCase);
478
- if (rows.length === 0 && unlinkedEvidence === 0 && !evidenceMalformed)
483
+ if (
484
+ rows.length === 0 &&
485
+ unlinkedEvidence === 0 &&
486
+ !evidenceMalformed &&
487
+ omittedEntries === 0
488
+ )
479
489
  return null;
480
490
  return (
481
491
  <div className="mt-3 border-t border-theme-border/60 pt-2">
@@ -566,6 +576,13 @@ export function AssessmentSources({
566
576
  : `${unlinkedEvidence} agent notes could not be linked to Radar results and are not shown.`}
567
577
  </p>
568
578
  ) : null}
579
+ {omittedEntries > 0 ? (
580
+ <p className="mt-2 text-[11px] text-theme-text-tertiary">
581
+ {omittedEntries === 1
582
+ ? "1 next step, open item or ruled-out hypothesis went past the page's limit and is not shown."
583
+ : `${omittedEntries} next steps, open items or ruled-out hypotheses went past the page's limits and are not shown.`}
584
+ </p>
585
+ ) : null}
569
586
  </div>
570
587
  );
571
588
  }
@@ -656,8 +673,201 @@ export type AssessmentExplanation = {
656
673
  openRequest?: number;
657
674
  };
658
675
 
659
- // The diagnosis result: likely cause + remediation + the
660
- // 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
+
661
871
  function DiagnosisResult({
662
872
  diagnosis,
663
873
  onApply,
@@ -700,37 +910,9 @@ function DiagnosisResult({
700
910
  ? storyPlainText(diagnosis.report)
701
911
  : diagnosis.report;
702
912
  const showAnalysisDisclosure = !storyShape || storyInline;
703
- const [detail, setDetail] = useState<"analysis" | "explanation" | null>(
704
- explanation?.status === "running" ? "explanation" : null,
705
- );
706
- const showAnalysis = detail === "analysis";
707
- const analysisReveal = useDisclosureReveal<HTMLDivElement>();
708
- const { elementRef: analysisElementRef, revealAfterToggle: revealAnalysis } =
709
- analysisReveal;
710
- useEffect(() => {
711
- if (explanation?.openRequest) {
712
- setDetail("explanation");
713
- revealAnalysis(true);
714
- }
715
- }, [explanation?.openRequest, revealAnalysis]);
716
- useEffect(() => {
717
- if (
718
- detail === "explanation" &&
719
- (explanation?.status === "done" || explanation?.status === "error")
720
- ) {
721
- const element = analysisElementRef.current;
722
- const scroller = element?.closest("[data-investigation-findings-scroll]");
723
- if (element && scroller) {
724
- const top = element.getBoundingClientRect().top;
725
- const viewport = scroller.getBoundingClientRect();
726
- if (top >= viewport.top && top < viewport.bottom) revealAnalysis(true);
727
- }
728
- }
729
- }, [explanation?.status, detail, analysisElementRef, revealAnalysis]);
730
913
  const [showAllSteps, setShowAllSteps] = useState(false);
731
914
  const stepsId = useId();
732
915
  const stepsReveal = useDisclosureReveal<HTMLDivElement>();
733
- const analysisId = useId();
734
916
  // Only a real structured cause anchors the amber card; the full prose lives in
735
917
  // "Full analysis" (never relabel the report as a causal assessment).
736
918
  const rootCause = diagnosis.rootCause;
@@ -765,6 +947,7 @@ function DiagnosisResult({
765
947
  }) => {
766
948
  const isRec = recValid && i === recIdx! - 1;
767
949
  const step = typedSteps ? steps[i] : undefined;
950
+ const commands = remediationCommands(r);
768
951
  return (
769
952
  <div
770
953
  key={i}
@@ -823,17 +1006,6 @@ function DiagnosisResult({
823
1006
  Apply…
824
1007
  </button>
825
1008
  )}
826
- {remediationCommands(r).map((command, c, all) => (
827
- <CopyButton
828
- key={c}
829
- text={command}
830
- label={
831
- all.length > 1
832
- ? `Copy command ${c + 1} of step ${i + 1}`
833
- : `Copy command from step ${i + 1}`
834
- }
835
- />
836
- ))}
837
1009
  </div>
838
1010
  </div>
839
1011
  {/* The condition and the reason are read before the command is
@@ -854,7 +1026,19 @@ function DiagnosisResult({
854
1026
  {diagnosis.recommendedReason}
855
1027
  </p>
856
1028
  )}
857
- <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
+ >
858
1042
  {r}
859
1043
  </AIMarkdown>
860
1044
  </div>
@@ -1015,152 +1199,17 @@ function DiagnosisResult({
1015
1199
  </div>
1016
1200
  )}
1017
1201
 
1018
- {/* Full analysis — the agent's detailed evidence, on demand. Under the
1019
- story contract Findings renders the story itself, so only the
1020
- explanation and sources remain here. */}
1021
- {showConclusion &&
1022
- ((showAnalysisDisclosure && diagnosis.report) ||
1023
- (showAnalysisDisclosure && diagnosis.confidence != null) ||
1024
- explanation ||
1025
- assessmentAction ||
1026
- assessmentSources) && (
1027
- <div>
1028
- <div
1029
- className="flex flex-wrap items-center gap-x-3 gap-y-1 pt-2"
1030
- data-assessment-actions
1031
- >
1032
- {((showAnalysisDisclosure && diagnosis.report) ||
1033
- (showAnalysisDisclosure && diagnosis.confidence != null) ||
1034
- assessmentSources) && (
1035
- <button
1036
- type="button"
1037
- aria-expanded={showAnalysis}
1038
- aria-controls={`${analysisId}-analysis`}
1039
- onClick={() => {
1040
- setDetail(showAnalysis ? null : "analysis");
1041
- analysisReveal.revealAfterToggle(!showAnalysis);
1042
- }}
1043
- className="flex items-center gap-1.5 rounded-md py-2 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary"
1044
- >
1045
- <CollapseChevron
1046
- open={showAnalysis}
1047
- className="h-3.5 w-3.5"
1048
- />
1049
- {showAnalysisDisclosure && diagnosis.report
1050
- ? "Full analysis"
1051
- : "Assessment details"}
1052
- </button>
1053
- )}
1054
- {explanation && (
1055
- <Tooltip
1056
- content={
1057
- explanation.status === "idle"
1058
- ? explanation.onGenerate
1059
- ? "Ask the agent to explain this assessment and its proposed next steps in plain language."
1060
- : "Wait for the current agent request to finish before requesting an explanation."
1061
- : explanation.status === "running"
1062
- ? "The agent is preparing an explanation. You can close this and return while it runs."
1063
- : explanation.status === "error"
1064
- ? "View the explanation error and retry when the agent is available."
1065
- : "Show the saved plain-language explanation. No new request is needed."
1066
- }
1067
- >
1068
- <button
1069
- type="button"
1070
- aria-expanded={detail === "explanation"}
1071
- aria-controls={`${analysisId}-explanation`}
1072
- disabled={
1073
- explanation.status === "idle" && !explanation.onGenerate
1074
- }
1075
- onClick={() => {
1076
- const open = detail !== "explanation";
1077
- setDetail(open ? "explanation" : null);
1078
- analysisReveal.revealAfterToggle(open);
1079
- if (open && explanation.status === "idle")
1080
- explanation.onGenerate?.();
1081
- }}
1082
- 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"
1083
- >
1084
- <HelpCircle className="h-3 w-3" />
1085
- Explain simply
1086
- </button>
1087
- </Tooltip>
1088
- )}
1089
- {assessmentAction && (
1090
- <div className="ml-auto">{assessmentAction}</div>
1091
- )}
1092
- </div>
1093
- <div id={analysisId} ref={analysisReveal.elementRef}>
1094
- <div id={`${analysisId}-analysis`}>
1095
- <Collapse open={detail === "analysis"} mountLazily>
1096
- <div className="border-t border-theme-border/60 px-3 py-2">
1097
- {showAnalysisDisclosure ? (
1098
- <>
1099
- <p className="mb-2 text-xs text-theme-text-tertiary">
1100
- Agent confidence:{" "}
1101
- {diagnosis.confidence != null
1102
- ? confidenceLabel(diagnosis.confidence)
1103
- : "not stated"}
1104
- {diagnosis.confidence != null
1105
- ? " · self-reported"
1106
- : ""}
1107
- </p>
1108
- <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">
1109
- {analysisText}
1110
- </AIMarkdown>
1111
- </>
1112
- ) : null}
1113
- {assessmentSources}
1114
- </div>
1115
- </Collapse>
1116
- </div>
1117
- <div id={`${analysisId}-explanation`}>
1118
- <Collapse open={detail === "explanation"} mountLazily>
1119
- <div className="border-t border-theme-border/60 px-3 py-2">
1120
- {explanation?.status === "running" ? (
1121
- <div
1122
- role="status"
1123
- className="flex items-center gap-2 py-2 text-sm text-theme-text-secondary"
1124
- >
1125
- <Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
1126
- Explaining this assessment…
1127
- </div>
1128
- ) : explanation?.status === "done" ? (
1129
- <div className="flex items-start gap-2">
1130
- <AIMarkdown className="min-w-0 flex-1 text-sm text-theme-text-secondary [overflow-wrap:anywhere]">
1131
- {explanation.text || ""}
1132
- </AIMarkdown>
1133
- <CopyButton
1134
- text={explanation.text || ""}
1135
- label="Copy explanation"
1136
- />
1137
- </div>
1138
- ) : explanation?.status === "error" ? (
1139
- <div
1140
- role="alert"
1141
- className="flex flex-wrap items-center gap-2 py-2 text-sm text-theme-text-secondary"
1142
- >
1143
- <span>
1144
- {explanation.error ||
1145
- "The agent did not return an explanation."}
1146
- </span>
1147
- {explanation.onGenerate && (
1148
- <button
1149
- type="button"
1150
- onClick={explanation.onGenerate}
1151
- className="rounded-md px-2 py-1 text-xs font-medium text-accent-text hover:bg-theme-hover"
1152
- >
1153
- Try again
1154
- </button>
1155
- )}
1156
- </div>
1157
- ) : null}
1158
- </div>
1159
- </Collapse>
1160
- </div>
1161
- </div>
1162
- </div>
1163
- )}
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
+ )}
1164
1213
 
1165
1214
  {showConclusion && showDisclaimer && (
1166
1215
  <div className="flex items-start gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
@@ -1185,6 +1234,7 @@ function healthFlagSentence(flag: {
1185
1234
 
1186
1235
  export function AllClearCard({
1187
1236
  diagnosis,
1237
+ explanation,
1188
1238
  animate,
1189
1239
  showDisclaimer,
1190
1240
  coverageLimited,
@@ -1203,6 +1253,7 @@ export function AllClearCard({
1203
1253
  animate: boolean;
1204
1254
  showDisclaimer: boolean;
1205
1255
  coverageLimited: boolean;
1256
+ explanation?: AssessmentExplanation;
1206
1257
  evidenceConflict: boolean;
1207
1258
  evidenceConflictExplainedBy?: string[];
1208
1259
  assessmentAction?: ReactNode;
@@ -1217,9 +1268,6 @@ export function AllClearCard({
1217
1268
  onRevealSource?: (sourceId: string) => void;
1218
1269
  }) {
1219
1270
  const storyShape = diagnosisHasStoryShape(diagnosis);
1220
- const [showAnalysis, setShowAnalysis] = useState(false);
1221
- const analysisReveal = useDisclosureReveal<HTMLDivElement>();
1222
- const analysisId = useId();
1223
1271
  const report =
1224
1272
  (storyShape && !storyInline ? "" : storyPlainText(diagnosis.report)) ||
1225
1273
  (storyShape
@@ -1264,43 +1312,14 @@ export function AllClearCard({
1264
1312
  // the disclaimer are the same in both shapes.
1265
1313
  const trailing = (
1266
1314
  <>
1267
- {detailed || assessmentAction || assessmentSources ? (
1268
- <div>
1269
- <div
1270
- className="flex flex-wrap items-center gap-3 pt-2"
1271
- data-assessment-actions
1272
- >
1273
- {(detailed || assessmentSources) && (
1274
- <button
1275
- type="button"
1276
- aria-expanded={showAnalysis}
1277
- aria-controls={analysisId}
1278
- onClick={() => {
1279
- setShowAnalysis(!showAnalysis);
1280
- analysisReveal.revealAfterToggle(!showAnalysis);
1281
- }}
1282
- className="flex items-center gap-1.5 rounded-md py-2 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary"
1283
- >
1284
- <CollapseChevron open={showAnalysis} className="h-3.5 w-3.5" />
1285
- {detailed ? "Full analysis" : "Assessment details"}
1286
- </button>
1287
- )}
1288
- {assessmentAction && (
1289
- <div className="ml-auto">{assessmentAction}</div>
1290
- )}
1291
- </div>
1292
- <div id={analysisId} ref={analysisReveal.elementRef}>
1293
- <Collapse open={showAnalysis}>
1294
- <div className="border-t border-theme-border/60 px-3 py-2">
1295
- <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">
1296
- {detailed ? report : ""}
1297
- </AIMarkdown>
1298
- {assessmentSources}
1299
- </div>
1300
- </Collapse>
1301
- </div>
1302
- </div>
1303
- ) : null}
1315
+ <AssessmentDetails
1316
+ diagnosis={diagnosis}
1317
+ explanation={explanation}
1318
+ assessmentAction={assessmentAction}
1319
+ assessmentSources={assessmentSources}
1320
+ showAnalysisDisclosure={detailed}
1321
+ analysisText={report}
1322
+ />
1304
1323
  {showDisclaimer ? (
1305
1324
  <div className="flex items-start gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
1306
1325
  <ShieldCheck className="mt-0.5 h-3 w-3 shrink-0" />
@@ -1423,6 +1442,9 @@ export function AllClearCard({
1423
1442
  // all-clear) — so "I couldn't tell" never reads as "you're fine."
1424
1443
  export function InconclusiveCard({
1425
1444
  diagnosis,
1445
+ explanation,
1446
+ assessmentSources,
1447
+ assessmentAction,
1426
1448
  animate,
1427
1449
  storyInline = false,
1428
1450
  revisedAfter,
@@ -1432,7 +1454,10 @@ export function InconclusiveCard({
1432
1454
  diagnosis: Diagnosis;
1433
1455
  animate: boolean;
1434
1456
  storyInline?: boolean;
1457
+ explanation?: AssessmentExplanation;
1435
1458
  revisedAfter?: string;
1459
+ assessmentSources?: ReactNode;
1460
+ assessmentAction?: ReactNode;
1436
1461
  /** Reads Radar could not complete for this assessment; listed under Still open. */
1437
1462
  assessmentLimits?: string[];
1438
1463
  assessmentCopy?: Pick<AssessmentCopyRadar, "context" | "receipts">;
@@ -1477,6 +1502,12 @@ export function InconclusiveCard({
1477
1502
  addressing any errors shown in Activity.
1478
1503
  </span>
1479
1504
  </div>
1505
+ <AssessmentDetails
1506
+ diagnosis={diagnosis}
1507
+ explanation={explanation}
1508
+ assessmentSources={assessmentSources}
1509
+ assessmentAction={assessmentAction}
1510
+ />
1480
1511
  </div>
1481
1512
  );
1482
1513
  }
@@ -1528,10 +1559,10 @@ export function remediationHeadline(step: string): string {
1528
1559
  */
1529
1560
  export function remediationCommands(step: string): string[] {
1530
1561
  const commands: string[] = [];
1531
- // One pass in reading order, so button N is the Nth command in the text.
1562
+ const normalized = tidyFences(step);
1532
1563
  const code = /```[a-zA-Z]*\n([\s\S]*?)```|`([^`\n]+)`/g;
1533
1564
  let match: RegExpExecArray | null;
1534
- while ((match = code.exec(step))) {
1565
+ while ((match = code.exec(normalized))) {
1535
1566
  if (match[1] !== undefined) {
1536
1567
  const body = match[1].trim();
1537
1568
  if (body) commands.push(body);