@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
|
@@ -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("
|
|
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 "dev/does-not-exist" 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("
|
|
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
|
-
|
|
1174
|
-
expect(html).
|
|
1175
|
-
expect(html).toContain("
|
|
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");
|