@elench/testkit 0.1.85 → 0.1.87
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/README.md +43 -10
- package/lib/cli/commands/inspect.mjs +124 -0
- package/lib/cli/entrypoint.mjs +2 -4
- package/lib/cli/presentation/tree-reporter.mjs +34 -28
- package/lib/cli/tui/detail-pane.mjs +161 -0
- package/lib/cli/tui/filter-bar.mjs +12 -0
- package/lib/cli/tui/fuzzy-match.mjs +106 -0
- package/lib/cli/tui/inspect-app.mjs +306 -0
- package/lib/cli/tui/inspect-artifact-adapter.mjs +3 -0
- package/lib/cli/tui/inspect-live-adapter.mjs +15 -0
- package/lib/cli/tui/inspect-model.mjs +817 -0
- package/lib/cli/tui/inspect-state.mjs +321 -0
- package/lib/index.d.ts +30 -8
- package/lib/index.mjs +3 -0
- package/lib/runtime/index.d.ts +77 -0
- package/lib/runtime-src/k6/dal-fixtures.js +66 -0
- package/lib/runtime-src/k6/dal-suite.js +21 -1
- package/lib/runtime-src/shared/fixture-engine.mjs +320 -0
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +5 -5
- package/lib/cli/commands/artifacts.mjs +0 -45
- package/lib/cli/commands/logs.mjs +0 -47
- package/lib/cli/commands/show.mjs +0 -47
- package/lib/cli/commands/watch.mjs +0 -23
- package/lib/cli/tui/run-app.mjs +0 -1
- package/lib/cli/tui/run-session-app.mjs +0 -432
- package/lib/cli/tui/run-session-state.mjs +0 -505
- package/lib/cli/tui/run-tree-state.mjs +0 -1
- package/lib/cli/tui/watch-app.mjs +0 -220
|
@@ -1,432 +0,0 @@
|
|
|
1
|
-
import React, { createElement, useEffect, useState } from "react";
|
|
2
|
-
import { Box, Text, useAnimation, useInput } from "ink";
|
|
3
|
-
import figures from "figures";
|
|
4
|
-
import { formatDuration } from "../../runner/formatting.mjs";
|
|
5
|
-
import {
|
|
6
|
-
bold,
|
|
7
|
-
colorService,
|
|
8
|
-
colorTypeBadge,
|
|
9
|
-
dim,
|
|
10
|
-
green,
|
|
11
|
-
red,
|
|
12
|
-
yellow,
|
|
13
|
-
} from "../presentation/colors.mjs";
|
|
14
|
-
import { renderSummaryBox } from "../presentation/summary-box.mjs";
|
|
15
|
-
import { renderFailureBlock } from "../presentation/failure-presentation.mjs";
|
|
16
|
-
import { getTerminalWidth } from "../presentation/terminal-layout.mjs";
|
|
17
|
-
import { defaultInvestigationMessage } from "../agents/prompt-builder.mjs";
|
|
18
|
-
import { loadInvestigationContext } from "../agents/investigation-context.mjs";
|
|
19
|
-
|
|
20
|
-
const TREE_BRANCH = "\u251C\u2500\u2500 ";
|
|
21
|
-
const TREE_LAST = "\u2514\u2500\u2500 ";
|
|
22
|
-
const TREE_PIPE = "\u2502 ";
|
|
23
|
-
const TREE_SPACE = " ";
|
|
24
|
-
const SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
25
|
-
|
|
26
|
-
export function RunSessionApp({
|
|
27
|
-
sessionState,
|
|
28
|
-
stdout,
|
|
29
|
-
productDir,
|
|
30
|
-
onInvestigate,
|
|
31
|
-
onRequestClose,
|
|
32
|
-
onCancelInvestigation,
|
|
33
|
-
} = {}) {
|
|
34
|
-
const [snapshot, setSnapshot] = useState(() => sessionState.getSnapshot());
|
|
35
|
-
const [detailLines, setDetailLines] = useState([]);
|
|
36
|
-
const { frame } = useAnimation({ interval: 80, isActive: !snapshot.finished });
|
|
37
|
-
const spinnerFrame = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
38
|
-
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
const unsubscribe = sessionState.subscribe(() => {
|
|
41
|
-
setSnapshot(sessionState.getSnapshot());
|
|
42
|
-
});
|
|
43
|
-
return unsubscribe;
|
|
44
|
-
}, [sessionState]);
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
if (!snapshot.selectedFailure) {
|
|
48
|
-
setDetailLines([]);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
const context = loadInvestigationContext({
|
|
53
|
-
productDir,
|
|
54
|
-
serviceName: snapshot.selectedFailure.serviceName,
|
|
55
|
-
filePath: snapshot.selectedFailure.filePath,
|
|
56
|
-
});
|
|
57
|
-
setDetailLines(context.detailLines);
|
|
58
|
-
} catch (error) {
|
|
59
|
-
setDetailLines([error instanceof Error ? error.message : String(error)]);
|
|
60
|
-
}
|
|
61
|
-
}, [productDir, snapshot.selectedFailure?.key]);
|
|
62
|
-
|
|
63
|
-
useInput((input, key) => {
|
|
64
|
-
if (!snapshot.finished) {
|
|
65
|
-
if (input === "q") sessionState.setNotice("Run is still in progress. Wait for completion before closing.");
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (input === "q") {
|
|
70
|
-
onRequestClose?.();
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (snapshot.mode === "complete") {
|
|
75
|
-
if (key.downArrow || input === "j") {
|
|
76
|
-
sessionState.selectNextFailure();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (key.upArrow || input === "k") {
|
|
80
|
-
sessionState.selectPreviousFailure();
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
if (input === "y" || input === "i") {
|
|
84
|
-
if (snapshot.selectedFailure) {
|
|
85
|
-
onInvestigate?.({
|
|
86
|
-
provider: "auto",
|
|
87
|
-
userMessage: defaultInvestigationMessage(),
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (input === "c" && snapshot.selectedFailure) {
|
|
93
|
-
onInvestigate?.({ provider: "claude", userMessage: defaultInvestigationMessage() });
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (input === "o" && snapshot.selectedFailure) {
|
|
97
|
-
onInvestigate?.({ provider: "codex", userMessage: defaultInvestigationMessage() });
|
|
98
|
-
}
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (snapshot.mode === "investigating") {
|
|
103
|
-
if (input === "t") {
|
|
104
|
-
sessionState.toggleInvestigationViewMode();
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
if (input === "x") {
|
|
108
|
-
onCancelInvestigation?.();
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
if (input === "b" && snapshot.agentSession?.status !== "running") {
|
|
112
|
-
sessionState.returnToSummary();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const treeElements = buildTreeElements({ snapshot, spinnerFrame, stdout });
|
|
118
|
-
const detailElements =
|
|
119
|
-
snapshot.mode === "investigating"
|
|
120
|
-
? buildAgentPane(snapshot)
|
|
121
|
-
: buildFailureDetailPane(snapshot, detailLines);
|
|
122
|
-
|
|
123
|
-
const footerText = buildFooterText(snapshot);
|
|
124
|
-
const headerText = buildHeaderText(snapshot);
|
|
125
|
-
const summaryLines =
|
|
126
|
-
snapshot.finished && snapshot.summaryData
|
|
127
|
-
? renderSummaryBox(snapshot.summaryData.rows, { stdout })
|
|
128
|
-
: [];
|
|
129
|
-
const terminalWidth = getTerminalWidth(stdout, 100);
|
|
130
|
-
const leftWidth = Math.max(42, Math.floor(terminalWidth * 0.58));
|
|
131
|
-
const rightWidth = Math.max(28, terminalWidth - leftWidth - 1);
|
|
132
|
-
|
|
133
|
-
return createElement(
|
|
134
|
-
Box,
|
|
135
|
-
{ flexDirection: "column" },
|
|
136
|
-
createElement(Text, { key: "header" }, dim(headerText)),
|
|
137
|
-
snapshot.notice ? createElement(Text, { key: "notice" }, yellow(snapshot.notice)) : null,
|
|
138
|
-
createElement(
|
|
139
|
-
Box,
|
|
140
|
-
{ key: "main", marginTop: 1, flexDirection: "row" },
|
|
141
|
-
createElement(Box, { width: leftWidth, flexDirection: "column", paddingRight: 1 }, ...treeElements),
|
|
142
|
-
createElement(Box, { width: rightWidth, flexDirection: "column", paddingLeft: 1 }, ...detailElements)
|
|
143
|
-
),
|
|
144
|
-
summaryLines.length > 0 ? createElement(Text, { key: "summary-gap" }, "") : null,
|
|
145
|
-
...summaryLines.map((line, index) => createElement(Text, { key: `summary-${index}` }, line)),
|
|
146
|
-
createElement(Text, { key: "footer-gap" }, ""),
|
|
147
|
-
createElement(Text, { key: "footer" }, dim(footerText))
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function buildHeaderText(snapshot) {
|
|
152
|
-
const progressText = snapshot.totalCount > 0 ? `[${snapshot.completedCount}/${snapshot.totalCount}]` : "[0/0]";
|
|
153
|
-
const phaseText = snapshot.phase || (snapshot.finished ? "run complete" : "preparing");
|
|
154
|
-
const modeText = snapshot.mode === "investigating"
|
|
155
|
-
? `investigating with ${snapshot.agentSession?.provider || "agent"}`
|
|
156
|
-
: snapshot.finished
|
|
157
|
-
? "interactive summary"
|
|
158
|
-
: "live run tree";
|
|
159
|
-
return [progressText, phaseText, modeText].filter(Boolean).join(" · ");
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function buildFooterText(snapshot) {
|
|
163
|
-
if (!snapshot.finished) return "Run in progress";
|
|
164
|
-
if (snapshot.mode === "investigating") {
|
|
165
|
-
if (snapshot.agentSession?.status === "running" || snapshot.agentSession?.status === "starting") {
|
|
166
|
-
return "t transcript · x cancel investigation · q quit";
|
|
167
|
-
}
|
|
168
|
-
return "t transcript · b back to summary · q quit";
|
|
169
|
-
}
|
|
170
|
-
if (!snapshot.selectedFailure) return "q quit";
|
|
171
|
-
return "↑/↓ select failure · y investigate · c Claude · o Codex · q quit";
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function buildTreeElements({ snapshot, spinnerFrame, stdout }) {
|
|
175
|
-
const elements = [];
|
|
176
|
-
for (let serviceIndex = 0; serviceIndex < snapshot.services.length; serviceIndex += 1) {
|
|
177
|
-
const service = snapshot.services[serviceIndex];
|
|
178
|
-
const isLastService = serviceIndex === snapshot.services.length - 1;
|
|
179
|
-
const serviceConnector = isLastService ? TREE_LAST : TREE_BRANCH;
|
|
180
|
-
|
|
181
|
-
if (service.skipped) {
|
|
182
|
-
elements.push(
|
|
183
|
-
createElement(
|
|
184
|
-
Text,
|
|
185
|
-
{ key: `svc-${service.name}` },
|
|
186
|
-
`${serviceConnector}${colorService(service.name)} ${dim(service.skipReason || "skipped")}`
|
|
187
|
-
)
|
|
188
|
-
);
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
elements.push(
|
|
193
|
-
createElement(Text, { key: `svc-${service.name}` }, `${serviceConnector}${colorService(service.name)}`)
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
const serviceIndent = isLastService ? TREE_SPACE : TREE_PIPE;
|
|
197
|
-
for (let typeIndex = 0; typeIndex < service.types.length; typeIndex += 1) {
|
|
198
|
-
const typeNode = service.types[typeIndex];
|
|
199
|
-
const isLastType = typeIndex === service.types.length - 1;
|
|
200
|
-
const typeConnector = isLastType ? TREE_LAST : TREE_BRANCH;
|
|
201
|
-
|
|
202
|
-
if (typeNode.collapsed) {
|
|
203
|
-
const totalFiles = typeNode.suites.reduce((sum, suite) => sum + suite.fileCount, 0);
|
|
204
|
-
const totalDuration = typeNode.suites.reduce((sum, suite) => sum + suite.totalDurationMs, 0);
|
|
205
|
-
const allSkipped = typeNode.suites.every((suite) => suite.collapseStatus === "all_skipped");
|
|
206
|
-
const icon = allSkipped ? yellow(figures.arrowDown) : green(figures.tick);
|
|
207
|
-
const suffix = allSkipped
|
|
208
|
-
? dim(`(${totalFiles} files skipped)`)
|
|
209
|
-
: dim(`(${totalFiles} files) ${formatDuration(totalDuration)}`);
|
|
210
|
-
elements.push(
|
|
211
|
-
createElement(
|
|
212
|
-
Text,
|
|
213
|
-
{ key: `type-${service.name}-${typeNode.type}` },
|
|
214
|
-
`${serviceIndent}${typeConnector}${colorTypeBadge(typeNode.type.toUpperCase())} ${icon} ${suffix}`
|
|
215
|
-
)
|
|
216
|
-
);
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
elements.push(
|
|
221
|
-
createElement(
|
|
222
|
-
Text,
|
|
223
|
-
{ key: `type-${service.name}-${typeNode.type}` },
|
|
224
|
-
`${serviceIndent}${typeConnector}${colorTypeBadge(typeNode.type.toUpperCase())}`
|
|
225
|
-
)
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
const typeIndent = serviceIndent + (isLastType ? TREE_SPACE : TREE_PIPE);
|
|
229
|
-
for (let suiteIndex = 0; suiteIndex < typeNode.suites.length; suiteIndex += 1) {
|
|
230
|
-
const suite = typeNode.suites[suiteIndex];
|
|
231
|
-
const isLastSuite = suiteIndex === typeNode.suites.length - 1;
|
|
232
|
-
const suiteConnector = isLastSuite ? TREE_LAST : TREE_BRANCH;
|
|
233
|
-
|
|
234
|
-
if (suite.collapsed) {
|
|
235
|
-
const icon = suite.collapseStatus === "all_skipped" ? yellow(figures.arrowDown) : green(figures.tick);
|
|
236
|
-
const suffix = suite.collapseStatus === "all_skipped"
|
|
237
|
-
? dim(`(${suite.fileCount} files skipped)`)
|
|
238
|
-
: dim(`(${suite.fileCount} files) ${formatDuration(suite.totalDurationMs)}`);
|
|
239
|
-
elements.push(
|
|
240
|
-
createElement(
|
|
241
|
-
Text,
|
|
242
|
-
{ key: `suite-${service.name}-${suite.key}` },
|
|
243
|
-
`${typeIndent}${suiteConnector}${icon} ${bold(suite.groupLabel)} ${suffix}`
|
|
244
|
-
)
|
|
245
|
-
);
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const mixedLabel =
|
|
250
|
-
suite.passedCount > 0 && suite.passedCount < suite.fileCount
|
|
251
|
-
? dim(` (${suite.fileCount} files · ${suite.passedCount} passed)`)
|
|
252
|
-
: "";
|
|
253
|
-
elements.push(
|
|
254
|
-
createElement(
|
|
255
|
-
Text,
|
|
256
|
-
{ key: `suite-${service.name}-${suite.key}` },
|
|
257
|
-
`${typeIndent}${suiteConnector}${bold(suite.groupLabel)}${mixedLabel}`
|
|
258
|
-
)
|
|
259
|
-
);
|
|
260
|
-
|
|
261
|
-
const suiteIndent = typeIndent + (isLastSuite ? TREE_SPACE : TREE_PIPE);
|
|
262
|
-
for (let fileIndex = 0; fileIndex < suite.visibleFiles.length; fileIndex += 1) {
|
|
263
|
-
const file = suite.visibleFiles[fileIndex];
|
|
264
|
-
const isLastFile = fileIndex === suite.visibleFiles.length - 1;
|
|
265
|
-
const fileConnector = isLastFile ? TREE_LAST : TREE_BRANCH;
|
|
266
|
-
const failureKey = `${service.name}::${file.path}`;
|
|
267
|
-
const selected = failureKey === snapshot.selectedFailureKey;
|
|
268
|
-
const pointer = selected ? `${bold(">")} ` : "";
|
|
269
|
-
const icon = statusIcon(file.status, spinnerFrame);
|
|
270
|
-
const duration = file.durationMs != null ? ` ${dim(formatDuration(file.durationMs))}` : "";
|
|
271
|
-
elements.push(
|
|
272
|
-
createElement(
|
|
273
|
-
Text,
|
|
274
|
-
{ key: `file-${service.name}-${file.path}` },
|
|
275
|
-
`${suiteIndent}${fileConnector}${pointer}${icon} ${file.displayName}${duration}`
|
|
276
|
-
)
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
if (file.status !== "failed" || (!file.error && !file.failureDetails)) continue;
|
|
280
|
-
const failureLines = renderFailureBlock(
|
|
281
|
-
{
|
|
282
|
-
serviceName: service.name,
|
|
283
|
-
type: suite.key.split(":")[0],
|
|
284
|
-
file: file.path,
|
|
285
|
-
framework: suite.framework,
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
error: file.error,
|
|
289
|
-
failureDetails: file.failureDetails || [],
|
|
290
|
-
failed: true,
|
|
291
|
-
},
|
|
292
|
-
{
|
|
293
|
-
width: Math.max(40, Math.floor(getTerminalWidth(stdout, 100) * 0.55)),
|
|
294
|
-
regressionCatalog: snapshot.regressionCatalog,
|
|
295
|
-
}
|
|
296
|
-
);
|
|
297
|
-
const failureIndent = suiteIndent + (isLastFile ? TREE_SPACE : TREE_PIPE);
|
|
298
|
-
for (let failureIndex = 0; failureIndex < failureLines.length; failureIndex += 1) {
|
|
299
|
-
elements.push(
|
|
300
|
-
createElement(
|
|
301
|
-
Text,
|
|
302
|
-
{ key: `failure-${service.name}-${file.path}-${failureIndex}` },
|
|
303
|
-
`${failureIndent}${failureLines[failureIndex]}`
|
|
304
|
-
)
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
return elements;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function buildFailureDetailPane(snapshot, detailLines) {
|
|
315
|
-
if (!snapshot.selectedFailure) {
|
|
316
|
-
return [
|
|
317
|
-
createElement(Text, { key: "detail-title" }, bold("Selected Failure")),
|
|
318
|
-
createElement(Text, { key: "detail-empty" }, dim("No failed file selected.")),
|
|
319
|
-
];
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const lines = detailLines.length > 0 ? detailLines : ["Loading failure details..."];
|
|
323
|
-
return [
|
|
324
|
-
createElement(Text, { key: "detail-title" }, bold("Selected Failure")),
|
|
325
|
-
createElement(
|
|
326
|
-
Text,
|
|
327
|
-
{ key: "detail-path" },
|
|
328
|
-
`${snapshot.selectedFailure.serviceName} · ${snapshot.selectedFailure.filePath}`
|
|
329
|
-
),
|
|
330
|
-
createElement(Text, { key: "detail-gap" }, ""),
|
|
331
|
-
...lines.slice(0, 30).map((line, index) => createElement(Text, { key: `detail-${index}` }, line)),
|
|
332
|
-
];
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function buildAgentPane(snapshot) {
|
|
336
|
-
const transcriptEntries = snapshot.agentSession?.transcriptEntries || [];
|
|
337
|
-
const timeline = snapshot.agentSession?.timeline || [];
|
|
338
|
-
const viewMode = snapshot.agentSession?.viewMode || "summary";
|
|
339
|
-
const status = snapshot.agentSession?.status || "idle";
|
|
340
|
-
const statusText =
|
|
341
|
-
status === "running" || status === "starting"
|
|
342
|
-
? yellow(`Status: ${status}`)
|
|
343
|
-
: status === "error"
|
|
344
|
-
? red(`Status: ${status}`)
|
|
345
|
-
: green(`Status: ${status}`);
|
|
346
|
-
const phaseText = snapshot.agentSession?.activePhase
|
|
347
|
-
? dim(`Phase: ${snapshot.agentSession.activePhase}`)
|
|
348
|
-
: dim("Phase: planning");
|
|
349
|
-
|
|
350
|
-
return [
|
|
351
|
-
createElement(Text, { key: "agent-title" }, bold("Investigation")),
|
|
352
|
-
createElement(Text, { key: "agent-status" }, statusText),
|
|
353
|
-
createElement(Text, { key: "agent-phase" }, phaseText),
|
|
354
|
-
createElement(Text, { key: "agent-gap" }, ""),
|
|
355
|
-
...(viewMode === "transcript"
|
|
356
|
-
? buildTranscriptEntries(transcriptEntries)
|
|
357
|
-
: buildSummaryEntries(snapshot.agentSession, timeline)),
|
|
358
|
-
];
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function buildSummaryEntries(agentSession, timeline) {
|
|
362
|
-
const summaryLines = [];
|
|
363
|
-
const activeStep = agentSession?.activeStep || null;
|
|
364
|
-
const summary = agentSession?.summary || { inspectedCount: 0, rerunCount: 0, editedFiles: [] };
|
|
365
|
-
|
|
366
|
-
if (activeStep?.label) {
|
|
367
|
-
summaryLines.push(
|
|
368
|
-
createElement(Text, { key: "summary-step" }, `${yellow(figures.pointerSmall)} ${activeStep.label}${activeStep.detail ? ` ${dim(`· ${activeStep.detail}`)}` : ""}`)
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (summary.inspectedCount > 0 || summary.editedFiles.length > 0 || summary.rerunCount > 0) {
|
|
373
|
-
const parts = [];
|
|
374
|
-
if (summary.inspectedCount > 0) parts.push(`${summary.inspectedCount} check${summary.inspectedCount === 1 ? "" : "s"}`);
|
|
375
|
-
if (summary.editedFiles.length > 0) parts.push(`${summary.editedFiles.length} file${summary.editedFiles.length === 1 ? "" : "s"} edited`);
|
|
376
|
-
if (summary.rerunCount > 0) parts.push(`${summary.rerunCount} rerun${summary.rerunCount === 1 ? "" : "s"}`);
|
|
377
|
-
summaryLines.push(createElement(Text, { key: "summary-counts" }, dim(parts.join(" · "))));
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const renderedTimeline = timeline.filter((entry) => entry.kind !== "step").slice(-8);
|
|
381
|
-
if (renderedTimeline.length === 0) {
|
|
382
|
-
summaryLines.push(createElement(Text, { key: "summary-empty" }, dim("Waiting for investigation progress...")));
|
|
383
|
-
return summaryLines;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
summaryLines.push(createElement(Text, { key: "summary-gap-2" }, ""));
|
|
387
|
-
for (let index = 0; index < renderedTimeline.length; index += 1) {
|
|
388
|
-
const entry = renderedTimeline[index];
|
|
389
|
-
summaryLines.push(
|
|
390
|
-
createElement(Text, { key: `timeline-${index}` }, formatTimelineEntry(entry))
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
return summaryLines;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function buildTranscriptEntries(entries) {
|
|
397
|
-
if (entries.length === 0) {
|
|
398
|
-
return [createElement(Text, { key: "agent-empty" }, dim("Waiting for agent output..."))];
|
|
399
|
-
}
|
|
400
|
-
return entries.slice(-24).map((entry, index) =>
|
|
401
|
-
createElement(Text, { key: `agent-${index}` }, formatAgentEntry(entry))
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
export function formatTimelineEntry(entry) {
|
|
406
|
-
if (entry.kind === "phase") return dim(entry.label);
|
|
407
|
-
if (entry.kind === "finding") return `${yellow(figures.bullet)} ${entry.summary}`;
|
|
408
|
-
if (entry.kind === "edit") return `${yellow(figures.bullet)} ${entry.summary}`;
|
|
409
|
-
if (entry.kind === "rerun") return `${yellow(figures.bullet)} ${entry.summary}`;
|
|
410
|
-
if (entry.kind === "result") return `${green(figures.tick)} ${entry.summary}`;
|
|
411
|
-
if (entry.kind === "notice") {
|
|
412
|
-
return entry.severity === "error" ? red(entry.message) : yellow(entry.message);
|
|
413
|
-
}
|
|
414
|
-
return entry.label || entry.summary || entry.message || "";
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
export function formatAgentEntry(entry) {
|
|
418
|
-
if (entry.kind === "tool") return yellow(`[tool] ${entry.text}`);
|
|
419
|
-
if (entry.kind === "status") return dim(`[status] ${entry.text}`);
|
|
420
|
-
if (entry.kind === "error") return red(`[error] ${entry.text}`);
|
|
421
|
-
return entry.text;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function statusIcon(status, spinnerFrame) {
|
|
425
|
-
if (status === "pending") return dim(figures.bullet);
|
|
426
|
-
if (status === "running") return spinnerFrame;
|
|
427
|
-
if (status === "passed") return green(figures.tick);
|
|
428
|
-
if (status === "failed") return red(figures.cross);
|
|
429
|
-
if (status === "skipped") return yellow(figures.arrowDown);
|
|
430
|
-
if (status === "not_run") return dim(figures.bullet);
|
|
431
|
-
return dim(figures.bullet);
|
|
432
|
-
}
|