@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.
Files changed (63) hide show
  1. package/package.json +2 -2
  2. package/src/App.tsx +6 -4
  3. package/src/api/client.ts +3 -0
  4. package/src/api/diagnose.ts +31 -1
  5. package/src/components/CloudConnectFlow.tsx +173 -13
  6. package/src/components/CloudFunnelButton.tsx +4 -2
  7. package/src/components/ConnectionErrorView.tsx +9 -10
  8. package/src/components/DebugOverlay.tsx +10 -6
  9. package/src/components/cloudConnectHandoff.test.ts +109 -4
  10. package/src/components/cloudConnectHandoff.ts +154 -2
  11. package/src/components/curl/ServiceCurlButton.tsx +19 -26
  12. package/src/components/diagnose/AgentCase.tsx +18 -5
  13. package/src/components/diagnose/AnalysisStory.test.tsx +308 -0
  14. package/src/components/diagnose/AnalysisStory.tsx +564 -0
  15. package/src/components/diagnose/DiagnoseContext.test.ts +10 -0
  16. package/src/components/diagnose/DiagnoseContext.tsx +25 -8
  17. package/src/components/diagnose/DiagnoseSurface.tsx +20 -14
  18. package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +771 -0
  19. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +8 -33
  20. package/src/components/diagnose/InvestigationEvidencePane.tsx +809 -168
  21. package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +30 -0
  22. package/src/components/diagnose/InvestigationResourceEvidence.tsx +20 -0
  23. package/src/components/diagnose/InvestigationView.tsx +493 -529
  24. package/src/components/diagnose/investigationCase.test.tsx +267 -129
  25. package/src/components/diagnose/investigationCase.ts +122 -77
  26. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +27 -0
  27. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +28 -0
  28. package/src/components/diagnose/investigationEvidence/builder.ts +11 -2
  29. package/src/components/diagnose/investigationEvidence/identity.test.ts +25 -7
  30. package/src/components/diagnose/investigationEvidence/identity.ts +4 -4
  31. package/src/components/diagnose/investigationEvidence/observations.ts +24 -0
  32. package/src/components/diagnose/investigationEvidence/projection.test.ts +36 -3
  33. package/src/components/diagnose/investigationEvidence/types.ts +2 -0
  34. package/src/components/diagnose/investigationEvidencePresentation.test.ts +2 -0
  35. package/src/components/diagnose/investigationEvidencePresentation.ts +3 -0
  36. package/src/components/diagnose/investigationState.test.ts +316 -58
  37. package/src/components/diagnose/investigationState.ts +257 -44
  38. package/src/components/diagnose/investigationStory.test.ts +161 -0
  39. package/src/components/diagnose/investigationStory.ts +207 -0
  40. package/src/components/diagnose/parts.test.tsx +9 -6
  41. package/src/components/diagnose/parts.tsx +977 -193
  42. package/src/components/diagnose/storyParts.test.tsx +426 -0
  43. package/src/components/diagnose/toolCallLabel.test.ts +51 -0
  44. package/src/components/diagnose/toolCallLabel.ts +135 -0
  45. package/src/components/diagnose/useDisclosureReveal.ts +10 -11
  46. package/src/components/helm/HelmCompareRoute.tsx +18 -4
  47. package/src/components/helm/HelmReleaseDrawer.tsx +11 -26
  48. package/src/components/helm/InstallWizard.tsx +8 -3
  49. package/src/components/home/MCPSetupDialog.tsx +15 -9
  50. package/src/components/portforward/PortForwardManager.tsx +6 -13
  51. package/src/components/resource/PrometheusChartsGrid.tsx +3 -3
  52. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +3 -3
  53. package/src/components/resource/WorkloadMetricsSection.tsx +15 -21
  54. package/src/components/resources/PodFilePreview.tsx +3 -3
  55. package/src/components/resources/renderers/ServiceRenderer.tsx +2 -1
  56. package/src/components/settings/SettingsDialog.tsx +4 -2
  57. package/src/components/ui/CommandPalette.tsx +10 -6
  58. package/src/components/ui/DiagnosticsOverlay.tsx +10 -6
  59. package/src/components/ui/Disclosure.tsx +47 -0
  60. package/src/components/ui/Markdown.tsx +23 -11
  61. package/src/components/ui/ShortcutHelpOverlay.tsx +3 -1
  62. package/src/components/ui/UpdateNotification.tsx +18 -2
  63. package/src/index.css +19 -6
@@ -0,0 +1,207 @@
1
+ /**
2
+ * The story is the agent's prose with Radar results placed inside it. A
3
+ * placement is `[[radar:evidence=N]]` alone on a line, N being the 0-based
4
+ * index into the verdict's `evidence` array; the same marker inside a sentence
5
+ * is a reference back to the placed card. Markers inside fenced code, inline
6
+ * code or a blockquote are the agent quoting something and never place: a
7
+ * tool result pasted into the story must not read as a citation.
8
+ *
9
+ * This is a line tokenizer that tracks fences and inline code, not a full
10
+ * Markdown parser; the grammar it accepts is deliberately small and the same
11
+ * fixtures pin it on both sides of the wire (see internal/ai/story_test.go).
12
+ */
13
+
14
+ // An agent that writes the ledger's ref form, [[radar:evidence-ref=ev_…]],
15
+ // meant a placement; the parser rewrites a cited ref to its index, and one
16
+ // nothing cites reaches here as a ref the resolver cannot place. Either way
17
+ // it is a marker, never literal text.
18
+ const STORY_PLACEMENT_RE =
19
+ /\[\[radar:evidence(?:=(\d+)|-ref=([A-Za-z0-9_]+))(?:\|(compact))?\]\]/g;
20
+ // Four or more leading spaces is Markdown indented code, so a marker there
21
+ // stays literal like one inside a fence.
22
+ const BLOCK_PLACEMENT_RE =
23
+ /^\s{0,3}\[\[radar:evidence(?:=(\d+)|-ref=([A-Za-z0-9_]+))(?:\|(compact))?\]\]\s*$/;
24
+ /** A reference the story cannot resolve renders as a placement Radar cannot identify. */
25
+ export const UNRESOLVED_STORY_INDEX = -1;
26
+ export type StoryRefResolver = (ref: string) => number | undefined;
27
+ function markerIndex(
28
+ index: string | undefined,
29
+ ref: string | undefined,
30
+ resolveRef: StoryRefResolver | undefined,
31
+ ): number {
32
+ if (index !== undefined) return Number(index);
33
+ const resolved = ref && resolveRef ? resolveRef(ref) : undefined;
34
+ return resolved === undefined || resolved < 0
35
+ ? UNRESOLVED_STORY_INDEX
36
+ : resolved;
37
+ }
38
+ const FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
39
+ // A closing fence is bare: same character, at least the opening length, and
40
+ // nothing but whitespace after it. "````not-a-close" is still code.
41
+ const FENCE_CLOSE_RE = /^ {0,3}(`{3,}|~{3,})\s*$/;
42
+
43
+ /** Maximum results placed as cards; later placements become references. */
44
+ export const MAX_STORY_PLACEMENTS = 6;
45
+
46
+ export type StorySegment =
47
+ | { kind: "prose"; markdown: string }
48
+ | {
49
+ kind: "placement";
50
+ index: number;
51
+ /** The agent asked for the header only: the reader needs the fact, not the detail. */
52
+ compact?: boolean;
53
+ /** Placed by Radar under the paragraph that first mentioned it inline. */
54
+ auto?: boolean;
55
+ };
56
+
57
+ interface StorySplit {
58
+ segments: StorySegment[];
59
+ }
60
+
61
+ /** The href an inline reference is rewritten to; the Markdown link renderer reads it back. */
62
+ export function storyReferenceHref(index: number): string {
63
+ return `#radar-evidence-${index}`;
64
+ }
65
+
66
+ export function storyReferenceIndex(
67
+ href: string | undefined,
68
+ ): number | undefined {
69
+ const match = href ? /^#radar-evidence-(-?\d+)$/.exec(href) : null;
70
+ return match ? Number(match[1]) : undefined;
71
+ }
72
+
73
+ /**
74
+ * Rewrites inline markers in one line to reference links, leaving markers
75
+ * inside inline code spans alone. A backtick count is enough because a marker
76
+ * contains none: the span state at each marker is the parity of backticks
77
+ * before it.
78
+ */
79
+ // A code span opens with a run of backticks and closes with a run of the same
80
+ // length; a run of another length inside it is literal. Counting single
81
+ // backticks would open a span at `` and never close it.
82
+ function codeSpanAfter(text: string, openRun: number): number {
83
+ for (const run of text.match(/`+/g) ?? []) {
84
+ if (openRun === 0) openRun = run.length;
85
+ else if (run.length === openRun) openRun = 0;
86
+ }
87
+ return openRun;
88
+ }
89
+
90
+ // A code span may run across lines within one paragraph, so the open run
91
+ // travels from line to line and a blank line ends it with the paragraph.
92
+ function rewriteInlineMarkers(
93
+ line: string,
94
+ resolveRef: StoryRefResolver | undefined,
95
+ openRun: number,
96
+ ): { text: string; openRun: number } {
97
+ let out = "";
98
+ let cursor = 0;
99
+ STORY_PLACEMENT_RE.lastIndex = 0;
100
+ for (const match of line.matchAll(STORY_PLACEMENT_RE)) {
101
+ const start = match.index ?? 0;
102
+ const before = line.slice(cursor, start);
103
+ openRun = codeSpanAfter(before, openRun);
104
+ out += before;
105
+ if (openRun > 0) {
106
+ out += match[0];
107
+ } else {
108
+ const index = markerIndex(match[1], match[2], resolveRef);
109
+ out += `[${match[0]}](${storyReferenceHref(index)})`;
110
+ }
111
+ cursor = start + match[0].length;
112
+ }
113
+ const tail = line.slice(cursor);
114
+ return { text: out + tail, openRun: codeSpanAfter(tail, openRun) };
115
+ }
116
+
117
+ export function splitStory(
118
+ report: string,
119
+ resolveRef?: StoryRefResolver,
120
+ ): StorySplit {
121
+ const segments: StorySegment[] = [];
122
+ const prose: string[] = [];
123
+ let fence: string | undefined;
124
+ let openRun = 0;
125
+ const flush = () => {
126
+ openRun = 0;
127
+ // Trim blank lines, not indentation: a segment that opens with indented
128
+ // code must keep its four spaces to stay code.
129
+ const markdown = prose.join("\n").replace(/^\n+/, "").trimEnd();
130
+ if (markdown) segments.push({ kind: "prose", markdown });
131
+ prose.length = 0;
132
+ };
133
+ for (const line of report.split(/\r?\n/)) {
134
+ const fenceMatch = FENCE_RE.exec(line);
135
+ if (fence) {
136
+ prose.push(line);
137
+ const close = FENCE_CLOSE_RE.exec(line);
138
+ if (
139
+ close &&
140
+ close[1].startsWith(fence[0]) &&
141
+ close[1].length >= fence.length
142
+ ) {
143
+ fence = undefined;
144
+ }
145
+ continue;
146
+ }
147
+ if (fenceMatch) {
148
+ // A fence interrupts a paragraph, and with it any open code span.
149
+ fence = fenceMatch[1];
150
+ openRun = 0;
151
+ prose.push(line);
152
+ continue;
153
+ }
154
+ // A blank line outside a fence ends a paragraph. Prose is segmented per
155
+ // paragraph so the collapsed preview can show exactly the paragraph
156
+ // before the first placed card, and a line clamp applies to one
157
+ // paragraph rather than to everything before the card.
158
+ if (line.trim() === "") {
159
+ flush();
160
+ continue;
161
+ }
162
+ // Blockquotes and indented code (four spaces or a tab) are quoted text:
163
+ // markers there stay literal, never citations or placements.
164
+ if (/^ {0,3}>/.test(line) || /^( {4,}|\t)/.test(line)) {
165
+ prose.push(line);
166
+ continue;
167
+ }
168
+ const block = openRun === 0 ? BLOCK_PLACEMENT_RE.exec(line) : null;
169
+ if (block) {
170
+ flush();
171
+ segments.push({
172
+ kind: "placement",
173
+ index: markerIndex(block[1], block[2], resolveRef),
174
+ ...(block[3] ? { compact: true } : {}),
175
+ });
176
+ continue;
177
+ }
178
+ const rewritten = rewriteInlineMarkers(line, resolveRef, openRun);
179
+ openRun = rewritten.openRun;
180
+ prose.push(rewritten.text);
181
+ }
182
+ flush();
183
+ return { segments };
184
+ }
185
+
186
+ /** The story without any markers, for copying and for models reading it back. */
187
+ export function storyPlainText(report: string): string {
188
+ return report
189
+ .replace(STORY_PLACEMENT_RE, "")
190
+ .replace(/\n{3,}/g, "\n\n")
191
+ .trim();
192
+ }
193
+
194
+ /** The one definition of "renders as a story": a summary headline or any marker. */
195
+ export function diagnosisHasStoryShape(
196
+ diagnosis: { summary?: string; report?: string } | null | undefined,
197
+ ): boolean {
198
+ if (!diagnosis) return false;
199
+ return !!diagnosis.summary?.trim() || storyHasPlacements(diagnosis.report);
200
+ }
201
+
202
+ /** True when a report carries the story contract (any placement or reference). */
203
+ export function storyHasPlacements(report: string | undefined): boolean {
204
+ if (!report) return false;
205
+ STORY_PLACEMENT_RE.lastIndex = 0;
206
+ return STORY_PLACEMENT_RE.test(report);
207
+ }
@@ -708,9 +708,10 @@ describe("tool row duration and failure reason", () => {
708
708
  expect(args?.[1]).toContain("flex-1");
709
709
  expect(args?.[1]).toContain("truncate");
710
710
  const reason = html.match(
711
- /<span class="investigation-tool-reason ([^"]*)">([^<]*)</,
711
+ /<span class="investigation-tool-reason ([^"]*)"[^>]*>([^<]*)</,
712
712
  );
713
- expect(reason?.[1]).toContain("shrink-0");
713
+ expect(reason?.[1]).toContain("min-w-0");
714
+ expect(reason?.[1]).toContain("truncate");
714
715
  expect(reason?.[2]).toBe(
715
716
  "resource not found: secret &quot;dev/does-not-exist&quot; not found",
716
717
  );
@@ -1152,7 +1153,7 @@ describe("ResultCard conclusion states", () => {
1152
1153
  expect(html).not.toContain("animation-delay");
1153
1154
  });
1154
1155
 
1155
- it("keeps only the recommended action in the first Findings scan", () => {
1156
+ it("opens only the recommended action in the first Findings scan", () => {
1156
1157
  const html = renderToStaticMarkup(
1157
1158
  <ResultCard
1158
1159
  diagnosis={diagnosis({
@@ -1170,9 +1171,11 @@ describe("ResultCard conclusion states", () => {
1170
1171
  );
1171
1172
 
1172
1173
  expect(html).toContain("Push the missing image.");
1173
- expect(html).not.toContain("Inspect the registry.");
1174
- expect(html).not.toContain("Restart the rollout.");
1175
- expect(html).toContain("Show 2 more steps");
1174
+ // The other steps fold to one readable row each rather than vanishing.
1175
+ expect(html.match(/data-step-folded=/g)).toHaveLength(2);
1176
+ expect(html).toContain("Inspect the registry.");
1177
+ expect(html).toContain("Restart the rollout.");
1178
+ expect(html).toContain("Expand all steps");
1176
1179
  expect(html).toContain('aria-expanded="false"');
1177
1180
  expect(html).toContain("grid-template-rows:0fr");
1178
1181
  expect(html).toContain("motion-reduce:transition-none");