@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,308 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+
4
+ import {
5
+ AnalysisStory,
6
+ resolveStoryPlacements,
7
+ type StoryPlacementLoss,
8
+ type StoryPlacementTarget,
9
+ } from "./AnalysisStory";
10
+ import type { InvestigationCaseItem } from "./investigationCase";
11
+
12
+ function target(index: number, observationId: string): StoryPlacementTarget {
13
+ return {
14
+ item: {
15
+ index,
16
+ role: "cause",
17
+ claim: "",
18
+ placement: "card",
19
+ groupId: `group-${observationId}`,
20
+ source: { id: `src-${observationId}` },
21
+ observation: {
22
+ source: { id: `src-${observationId}` },
23
+ revision: 1,
24
+ title: `Card ${observationId}`,
25
+ },
26
+ } as unknown as InvestigationCaseItem,
27
+ domId: `story-group-${observationId}`,
28
+ };
29
+ }
30
+
31
+ const resolver =
32
+ (table: Record<number, StoryPlacementTarget | StoryPlacementLoss>) =>
33
+ (index: number) =>
34
+ table[index] ?? ("invalid" as const);
35
+
36
+ describe("resolveStoryPlacements", () => {
37
+ it("places the first block marker per observation and references the rest", () => {
38
+ const story = resolveStoryPlacements(
39
+ "A.\n\n[[radar:evidence=0]]\n\nAgain [[radar:evidence=0]] and [[radar:evidence=1]].\n\n[[radar:evidence=1]]\n\n[[radar:evidence=0]]",
40
+ resolver({ 0: target(0, "a"), 1: target(1, "a") }),
41
+ );
42
+ // Item 1 resolves to the same observation as item 0: one card, not two.
43
+ expect(story.byIndex.get(0)?.kind).toBe("placed");
44
+ expect(story.byIndex.get(1)?.kind).toBe("reference");
45
+ expect(story.placedCount).toBe(1);
46
+ expect(story.lostItems).toBe(0);
47
+ });
48
+
49
+ it("renders one card for a repeated block marker and keeps the paragraph when nothing places", () => {
50
+ const repeated = resolveStoryPlacements(
51
+ "[[radar:evidence=0]]\n\nAgain:\n\n[[radar:evidence=0]]",
52
+ resolver({ 0: target(0, "a") }),
53
+ );
54
+ expect(repeated.placedAt.get(0)).toBe(0);
55
+ const html = renderToStaticMarkup(
56
+ <AnalysisStory
57
+ report={"Opening paragraph.\n\n[[radar:evidence=2]]\n\nMore."}
58
+ resolveItem={resolver({ 2: "unlinked" })}
59
+ renderPlacement={() => null}
60
+ onReveal={vi.fn()}
61
+ />,
62
+ );
63
+ expect(html).toContain("Opening paragraph.");
64
+ expect(html).toContain('data-story-lost-support="unlinked"');
65
+ });
66
+
67
+ it("places a card under the paragraph that first mentions it inline, and drops that marker from the sentence", () => {
68
+ const story = resolveStoryPlacements(
69
+ "Envoy boots [[radar:evidence=0]] then hangs on xDS.\n\nLater, see [[radar:evidence=0]] again.",
70
+ resolver({ 0: target(0, "a") }),
71
+ );
72
+ expect(story.segments.map((segment) => segment.kind)).toEqual([
73
+ "prose",
74
+ "placement",
75
+ "prose",
76
+ ]);
77
+ expect(story.segments[1]).toEqual({
78
+ kind: "placement",
79
+ index: 0,
80
+ auto: true,
81
+ });
82
+ expect(story.placedAt.get(0)).toBe(1);
83
+ expect((story.segments[0] as { markdown: string }).markdown).toBe(
84
+ "Envoy boots then hangs on xDS.",
85
+ );
86
+ expect((story.segments[2] as { markdown: string }).markdown).toContain(
87
+ "#radar-evidence-0",
88
+ );
89
+ const html = renderToStaticMarkup(
90
+ <AnalysisStory
91
+ report={
92
+ "Envoy boots [[radar:evidence=0]] then hangs on xDS.\n\nLater, see [[radar:evidence=0]] again."
93
+ }
94
+ resolveItem={resolver({ 0: target(0, "a") })}
95
+ renderPlacement={() => <div data-card>card</div>}
96
+ onReveal={vi.fn()}
97
+ openRequest={1}
98
+ />,
99
+ );
100
+ expect(html).toContain("data-card");
101
+ expect(html).toContain("↑");
102
+ });
103
+
104
+ it("keeps an inline mention a reference when the agent places that card on its own line later, pointing down", () => {
105
+ const html = renderToStaticMarkup(
106
+ <AnalysisStory
107
+ report={
108
+ "First [[radar:evidence=0]] here.\n\nThen:\n\n[[radar:evidence=0]]"
109
+ }
110
+ resolveItem={resolver({ 0: target(0, "a") })}
111
+ renderPlacement={() => <div data-card>card</div>}
112
+ onReveal={vi.fn()}
113
+ openRequest={1}
114
+ />,
115
+ );
116
+ expect(html).toContain("↓");
117
+ expect(html.match(/data-card/g)).toHaveLength(1);
118
+ });
119
+
120
+ it("places two cards that share one source and revision when they are different groups", () => {
121
+ const shared = (index: number, group: string): StoryPlacementTarget => ({
122
+ item: {
123
+ index,
124
+ role: "context",
125
+ claim: "",
126
+ placement: "card",
127
+ groupId: group,
128
+ source: { id: "src-bundle" },
129
+ observation: {
130
+ source: { id: "src-bundle" },
131
+ revision: 1,
132
+ title: group,
133
+ },
134
+ } as unknown as InvestigationCaseItem,
135
+ domId: `story-${group}`,
136
+ });
137
+ const story = resolveStoryPlacements(
138
+ "[[radar:evidence=0]]\n\n[[radar:evidence=1]]",
139
+ resolver({ 0: shared(0, "logs"), 1: shared(1, "events") }),
140
+ );
141
+ expect(story.placedCount).toBe(2);
142
+ expect(story.byIndex.get(1)?.kind).toBe("placed");
143
+ });
144
+
145
+ it("keeps the compact flag of an inline marker it auto-places, and tidies only the marker's hole", () => {
146
+ const story = resolveStoryPlacements(
147
+ "Line one \nline two says [[radar:evidence=0|compact]], then more.",
148
+ resolver({ 0: target(0, "a") }),
149
+ );
150
+ expect(story.segments).toEqual([
151
+ {
152
+ kind: "prose",
153
+ markdown: "Line one \nline two says, then more.",
154
+ },
155
+ { kind: "placement", index: 0, auto: true, compact: true },
156
+ ]);
157
+ const breaks = (report: string) =>
158
+ (
159
+ resolveStoryPlacements(report, resolver({ 0: target(0, "a") }))
160
+ .segments[0] as { markdown: string }
161
+ ).markdown;
162
+ expect(breaks("Says [[radar:evidence=0]] \nnext")).toBe("Says \nnext");
163
+ expect(breaks("Says \n[[radar:evidence=0]] next")).toBe("Says \nnext");
164
+ });
165
+
166
+ it("keeps the agent's own-line placement when a twin item on the same card was mentioned inline first", () => {
167
+ const story = resolveStoryPlacements(
168
+ "Inline [[radar:evidence=1]] first.\n\nLater:\n\n[[radar:evidence=0]]",
169
+ resolver({ 0: target(0, "a"), 1: target(1, "a") }),
170
+ );
171
+ expect(story.segments.map((segment) => segment.kind)).toEqual([
172
+ "prose",
173
+ "prose",
174
+ "placement",
175
+ ]);
176
+ expect(story.byIndex.get(1)?.kind).toBe("reference");
177
+ expect(story.placedAt.get(1)).toBe(2);
178
+ expect(story.placedCount).toBe(1);
179
+ });
180
+
181
+ it("points a chip past the placement cap down, to Captured results", () => {
182
+ const table: Record<number, StoryPlacementTarget | StoryPlacementLoss> = {};
183
+ for (let i = 0; i < 8; i += 1) table[i] = target(i, `o${i}`);
184
+ const markers = Array.from(
185
+ { length: 6 },
186
+ (_, i) => `[[radar:evidence=${i}]]`,
187
+ ).join("\n\n");
188
+ const html = renderToStaticMarkup(
189
+ <AnalysisStory
190
+ report={`${markers}\n\nSee [[radar:evidence=7]] here.`}
191
+ resolveItem={resolver(table)}
192
+ renderPlacement={() => <div data-card>card</div>}
193
+ onReveal={vi.fn()}
194
+ openRequest={1}
195
+ />,
196
+ );
197
+ expect(html.match(/data-card/g)).toHaveLength(6);
198
+ expect(html).toContain('data-story-ref-direction="down"');
199
+
200
+ const ownLine = renderToStaticMarkup(
201
+ <AnalysisStory
202
+ report={Array.from(
203
+ { length: 7 },
204
+ (_, i) => `[[radar:evidence=${i}]]`,
205
+ ).join("\n\n")}
206
+ resolveItem={resolver(table)}
207
+ renderPlacement={() => <div data-card>card</div>}
208
+ onReveal={vi.fn()}
209
+ openRequest={1}
210
+ />,
211
+ );
212
+ expect(ownLine.match(/data-card/g)).toHaveLength(6);
213
+ expect(ownLine).toContain('data-story-ref-direction="down"');
214
+ expect(ownLine).not.toContain('data-story-ref-direction="up"');
215
+ });
216
+
217
+ it("caps placed cards and reports every distinct lost item once", () => {
218
+ const markers = Array.from(
219
+ { length: 8 },
220
+ (_, i) => `[[radar:evidence=${i}]]`,
221
+ ).join("\n\n");
222
+ const table: Record<number, StoryPlacementTarget | StoryPlacementLoss> = {};
223
+ for (let i = 0; i < 8; i += 1) table[i] = target(i, `o${i}`);
224
+ const story = resolveStoryPlacements(markers, resolver(table));
225
+ expect(story.placedCount).toBe(6);
226
+ expect(story.byIndex.get(7)?.kind).toBe("reference");
227
+
228
+ const lost = resolveStoryPlacements(
229
+ "[[radar:evidence=3]]\n\nx [[radar:evidence=3]] [[radar:evidence=4]]",
230
+ resolver({ 3: "unlinked", 4: "unplaced" }),
231
+ );
232
+ expect(lost.lostItems).toBe(2);
233
+ expect(lost.byIndex.get(3)).toEqual({ kind: "lost", reason: "unlinked" });
234
+ });
235
+ });
236
+
237
+ describe("AnalysisStory", () => {
238
+ const render = (report: string, open = false) =>
239
+ renderToStaticMarkup(
240
+ <AnalysisStory
241
+ report={report}
242
+ resolveItem={resolver({
243
+ 0: target(0, "crash"),
244
+ 1: target(1, "chart"),
245
+ 2: "unlinked",
246
+ })}
247
+ renderPlacement={(t, { compact }) => (
248
+ <div data-placed={t.domId} data-compact={compact ? "1" : "0"}>
249
+ {t.item.observation?.title}
250
+ </div>
251
+ )}
252
+ onReveal={vi.fn()}
253
+ openRequest={open ? 1 : 0}
254
+ />,
255
+ );
256
+
257
+ it("previews the paragraph before the first placed card plus that card, compact, and nothing after", () => {
258
+ const html = render(
259
+ "Intro paragraph.\n\nThe container dies on start.\n\n[[radar:evidence=0]]\n\nThen a second thought.\n\n[[radar:evidence=1]]",
260
+ );
261
+ expect(html).toContain("The container dies on start.");
262
+ expect(html).toContain('data-placed="story-group-crash"');
263
+ expect(html).toContain('data-compact="1"');
264
+ expect(html).not.toContain("Intro paragraph.");
265
+ // The paragraph behind the fold shows only as the faded, aria-hidden teaser.
266
+ expect(html).toContain("data-story-teaser");
267
+ expect(html.split("Then a second thought.").length).toBe(2);
268
+ expect(html).not.toContain("story-group-chart");
269
+ expect(html).toContain("Expand");
270
+ expect(html).toContain('data-story-open="false"');
271
+ });
272
+
273
+ it("opens to the whole story with full cards, reference chips and visible lost support", () => {
274
+ const html = render(
275
+ "Intro.\n\n[[radar:evidence=0]]\n\nSee [[radar:evidence=0]] and [[radar:evidence=2]].",
276
+ true,
277
+ );
278
+ expect(html).toContain("Intro.");
279
+ expect(html).toContain('data-compact="0"');
280
+ expect(html).toContain("Show Card crash");
281
+ expect(html).toContain('data-story-lost-support="unlinked"');
282
+ expect(html).toContain("1 cited result could not be shown here.");
283
+ expect(html).toContain("Collapse");
284
+ });
285
+
286
+ it("keeps trailing content behind the fold and folds for it alone", () => {
287
+ const collapsed = render("One paragraph only.");
288
+ expect(collapsed).not.toContain("Expand");
289
+ const withTrailing = renderToStaticMarkup(
290
+ <AnalysisStory
291
+ report="One paragraph only."
292
+ resolveItem={resolver({})}
293
+ renderPlacement={() => null}
294
+ onReveal={vi.fn()}
295
+ trailing={<p data-trailing>Also checked: nothing else.</p>}
296
+ />,
297
+ );
298
+ expect(withTrailing).toContain("Expand");
299
+ expect(withTrailing).not.toContain("data-trailing");
300
+ });
301
+
302
+ it("does not offer a fold when everything already fits the preview", () => {
303
+ const html = render("One paragraph only.");
304
+ expect(html).toContain("One paragraph only.");
305
+ expect(html).not.toContain("Expand");
306
+ expect(html).toContain('data-story-open="true"');
307
+ });
308
+ });