@elench/testkit 0.1.84 → 0.1.85
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/lib/cli/agents/investigation-interpreter.mjs +320 -0
- package/lib/cli/agents/investigation-log.mjs +37 -0
- package/lib/cli/presentation/tree-reporter.mjs +33 -1
- package/lib/cli/tui/run-session-app.mjs +72 -10
- package/lib/cli/tui/run-session-state.mjs +29 -5
- 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
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
const INSPECT_COMMANDS = new Set([
|
|
2
|
+
"cat",
|
|
3
|
+
"find",
|
|
4
|
+
"grep",
|
|
5
|
+
"head",
|
|
6
|
+
"ls",
|
|
7
|
+
"open",
|
|
8
|
+
"read",
|
|
9
|
+
"rg",
|
|
10
|
+
"sed",
|
|
11
|
+
"tail",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const EDIT_COMMANDS = new Set([
|
|
15
|
+
"apply_patch",
|
|
16
|
+
"cp",
|
|
17
|
+
"ed",
|
|
18
|
+
"mv",
|
|
19
|
+
"perl",
|
|
20
|
+
"python",
|
|
21
|
+
"python3",
|
|
22
|
+
"ruby",
|
|
23
|
+
"tee",
|
|
24
|
+
"write",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const RERUN_COMMANDS = new Set([
|
|
28
|
+
"npm",
|
|
29
|
+
"npx",
|
|
30
|
+
"pnpm",
|
|
31
|
+
"pytest",
|
|
32
|
+
"testkit",
|
|
33
|
+
"vitest",
|
|
34
|
+
"yarn",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const PHASE_LABELS = {
|
|
38
|
+
planning: "Planning investigation",
|
|
39
|
+
inspecting: "Inspecting relevant files",
|
|
40
|
+
editing: "Editing candidate fix",
|
|
41
|
+
rerunning: "Rerunning validation",
|
|
42
|
+
summarizing: "Summarizing outcome",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function createInvestigationInterpreter() {
|
|
46
|
+
let phase = "planning";
|
|
47
|
+
let timeline = [];
|
|
48
|
+
let activeStepId = null;
|
|
49
|
+
let nextId = 1;
|
|
50
|
+
let inspectedCount = 0;
|
|
51
|
+
let rerunCount = 0;
|
|
52
|
+
const editedFiles = new Set();
|
|
53
|
+
|
|
54
|
+
pushTimeline({
|
|
55
|
+
kind: "phase",
|
|
56
|
+
phase,
|
|
57
|
+
label: PHASE_LABELS[phase],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
consumeProviderEvent(event) {
|
|
62
|
+
if (!event || !event.type) return getSnapshot();
|
|
63
|
+
|
|
64
|
+
if (event.type === "start") {
|
|
65
|
+
setPhase("planning");
|
|
66
|
+
activateStep("planning", "Preparing investigation", "active");
|
|
67
|
+
return getSnapshot();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (event.type === "tool") {
|
|
71
|
+
handleToolEvent(event);
|
|
72
|
+
return getSnapshot();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (event.type === "status") {
|
|
76
|
+
handleStatusEvent(event);
|
|
77
|
+
return getSnapshot();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (event.type === "error") {
|
|
81
|
+
failActiveStep();
|
|
82
|
+
pushTimeline({
|
|
83
|
+
kind: "notice",
|
|
84
|
+
severity: "error",
|
|
85
|
+
message: event.message || "Agent error",
|
|
86
|
+
});
|
|
87
|
+
return getSnapshot();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (event.type === "final") {
|
|
91
|
+
finalizeWithText(event.text || "");
|
|
92
|
+
return getSnapshot();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (event.type === "exit" && Number(event.code) !== 0) {
|
|
96
|
+
failActiveStep();
|
|
97
|
+
pushTimeline({
|
|
98
|
+
kind: "notice",
|
|
99
|
+
severity: "error",
|
|
100
|
+
message: `Agent exited with code ${event.code}`,
|
|
101
|
+
});
|
|
102
|
+
return getSnapshot();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return getSnapshot();
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
getSnapshot,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function getSnapshot() {
|
|
112
|
+
return {
|
|
113
|
+
phase,
|
|
114
|
+
activeStep: activeStepId ? findTimeline(activeStepId) : null,
|
|
115
|
+
timeline: timeline.map((entry) => ({
|
|
116
|
+
...entry,
|
|
117
|
+
files: Array.isArray(entry.files) ? [...entry.files] : entry.files,
|
|
118
|
+
})),
|
|
119
|
+
summary: {
|
|
120
|
+
inspectedCount,
|
|
121
|
+
rerunCount,
|
|
122
|
+
editedFiles: [...editedFiles].sort(),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function handleToolEvent(event) {
|
|
128
|
+
const commandText = String(event.name || "").trim();
|
|
129
|
+
const detailText = String(event.detail || "").trim();
|
|
130
|
+
const command = firstToken(commandText);
|
|
131
|
+
|
|
132
|
+
if (RERUN_COMMANDS.has(command) || /(?:^|\s)(testkit|vitest|pytest|npm|pnpm|yarn)(?:\s|$)/i.test(commandText)) {
|
|
133
|
+
setPhase("rerunning");
|
|
134
|
+
rerunCount += 1;
|
|
135
|
+
activateStep("rerunning", "Rerunning validation", "active", compactDetail(commandText, detailText));
|
|
136
|
+
pushTimeline({
|
|
137
|
+
kind: "rerun",
|
|
138
|
+
summary: compactDetail(commandText, detailText) || commandText,
|
|
139
|
+
command: commandText || detailText || null,
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (EDIT_COMMANDS.has(command) || /apply_patch|write|edit/i.test(commandText)) {
|
|
145
|
+
setPhase("editing");
|
|
146
|
+
const files = extractFileHints(`${commandText} ${detailText}`);
|
|
147
|
+
for (const file of files) editedFiles.add(file);
|
|
148
|
+
activateStep(
|
|
149
|
+
"editing",
|
|
150
|
+
"Editing candidate fix",
|
|
151
|
+
"active",
|
|
152
|
+
files.length > 0 ? `${files.length} file${files.length === 1 ? "" : "s"} touched` : compactDetail(commandText, detailText)
|
|
153
|
+
);
|
|
154
|
+
if (files.length > 0) {
|
|
155
|
+
upsertTimeline(
|
|
156
|
+
"latest-edit",
|
|
157
|
+
{
|
|
158
|
+
kind: "edit",
|
|
159
|
+
summary: `Edited ${files.length} file${files.length === 1 ? "" : "s"}`,
|
|
160
|
+
files,
|
|
161
|
+
},
|
|
162
|
+
true
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (INSPECT_COMMANDS.has(command) || /git|diff|read|search|inspect|open/i.test(commandText)) {
|
|
169
|
+
setPhase("inspecting");
|
|
170
|
+
inspectedCount += 1;
|
|
171
|
+
activateStep(
|
|
172
|
+
"inspecting",
|
|
173
|
+
"Inspecting relevant files",
|
|
174
|
+
"active",
|
|
175
|
+
`${inspectedCount} check${inspectedCount === 1 ? "" : "s"}`
|
|
176
|
+
);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
setPhase("inspecting");
|
|
181
|
+
activateStep("inspecting", "Inspecting relevant files", "active", compactDetail(commandText, detailText));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function handleStatusEvent(event) {
|
|
185
|
+
const message = String(event.message || "").trim();
|
|
186
|
+
if (!message) return;
|
|
187
|
+
if (/error|failed|exception/i.test(message)) {
|
|
188
|
+
pushTimeline({
|
|
189
|
+
kind: "notice",
|
|
190
|
+
severity: "warning",
|
|
191
|
+
message,
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (/plan|thinking|analy/i.test(message) && phase === "planning") {
|
|
196
|
+
activateStep("planning", "Planning investigation", "active", "Gathering context");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function finalizeWithText(text) {
|
|
201
|
+
setPhase("summarizing");
|
|
202
|
+
completeActiveStep();
|
|
203
|
+
const summary = summarizeFinalText(text);
|
|
204
|
+
if (summary) {
|
|
205
|
+
pushTimeline({
|
|
206
|
+
kind: "result",
|
|
207
|
+
summary,
|
|
208
|
+
finalText: text,
|
|
209
|
+
});
|
|
210
|
+
if (/root cause|likely|caused by|problem/i.test(summary)) {
|
|
211
|
+
pushTimeline({
|
|
212
|
+
kind: "finding",
|
|
213
|
+
severity: "info",
|
|
214
|
+
summary,
|
|
215
|
+
confidence: inferConfidence(summary),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function setPhase(nextPhase) {
|
|
222
|
+
if (!nextPhase || nextPhase === phase) return;
|
|
223
|
+
completeActiveStep();
|
|
224
|
+
phase = nextPhase;
|
|
225
|
+
pushTimeline({
|
|
226
|
+
kind: "phase",
|
|
227
|
+
phase,
|
|
228
|
+
label: PHASE_LABELS[phase] || nextPhase,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function activateStep(key, label, status, detail = null) {
|
|
233
|
+
const stepId = `step:${key}`;
|
|
234
|
+
upsertTimeline(
|
|
235
|
+
stepId,
|
|
236
|
+
{
|
|
237
|
+
kind: "step",
|
|
238
|
+
label,
|
|
239
|
+
status,
|
|
240
|
+
detail,
|
|
241
|
+
},
|
|
242
|
+
false
|
|
243
|
+
);
|
|
244
|
+
activeStepId = stepId;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function completeActiveStep() {
|
|
248
|
+
if (!activeStepId) return;
|
|
249
|
+
const entry = findTimeline(activeStepId);
|
|
250
|
+
if (entry && entry.status === "active") entry.status = "done";
|
|
251
|
+
activeStepId = null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function failActiveStep() {
|
|
255
|
+
if (!activeStepId) return;
|
|
256
|
+
const entry = findTimeline(activeStepId);
|
|
257
|
+
if (entry) entry.status = "failed";
|
|
258
|
+
activeStepId = null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function findTimeline(id) {
|
|
262
|
+
return timeline.find((entry) => entry.id === id) || null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function pushTimeline(entry) {
|
|
266
|
+
timeline.push({
|
|
267
|
+
id: `timeline:${nextId += 1}`,
|
|
268
|
+
at: Date.now(),
|
|
269
|
+
...entry,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function upsertTimeline(id, entry, replaceFiles = false) {
|
|
274
|
+
const existing = findTimeline(id);
|
|
275
|
+
if (existing) {
|
|
276
|
+
Object.assign(existing, entry);
|
|
277
|
+
if (replaceFiles && Array.isArray(entry.files)) existing.files = [...entry.files];
|
|
278
|
+
return existing;
|
|
279
|
+
}
|
|
280
|
+
const nextEntry = {
|
|
281
|
+
id,
|
|
282
|
+
at: Date.now(),
|
|
283
|
+
...entry,
|
|
284
|
+
};
|
|
285
|
+
timeline.push(nextEntry);
|
|
286
|
+
return nextEntry;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function firstToken(commandText) {
|
|
291
|
+
const normalized = String(commandText || "").trim();
|
|
292
|
+
if (!normalized) return "";
|
|
293
|
+
return normalized.split(/\s+/)[0].toLowerCase();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function compactDetail(commandText, detailText) {
|
|
297
|
+
if (commandText && detailText && detailText !== commandText) {
|
|
298
|
+
return `${commandText} (${detailText})`;
|
|
299
|
+
}
|
|
300
|
+
return commandText || detailText || null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function summarizeFinalText(text) {
|
|
304
|
+
const normalized = String(text || "").trim();
|
|
305
|
+
if (!normalized) return "";
|
|
306
|
+
const [firstParagraph] = normalized.split(/\n\s*\n/);
|
|
307
|
+
const [firstSentence] = firstParagraph.split(/(?<=[.!?])\s+/);
|
|
308
|
+
return firstSentence.trim();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function inferConfidence(summary) {
|
|
312
|
+
if (/definitely|confirmed|root cause/i.test(summary)) return "high";
|
|
313
|
+
if (/likely|probably|appears/i.test(summary)) return "medium";
|
|
314
|
+
return "low";
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function extractFileHints(text) {
|
|
318
|
+
const matches = String(text || "").match(/[A-Za-z0-9_./-]+\.(?:mjs|js|cjs|ts|tsx|jsx|json|md)/g) || [];
|
|
319
|
+
return [...new Set(matches)];
|
|
320
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export function writeInvestigationLog({ productDir, selectedFailure, agentSession } = {}) {
|
|
5
|
+
if (!productDir || !selectedFailure || !agentSession) return null;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const rootDir = path.join(productDir, ".testkit", "agent-sessions");
|
|
9
|
+
fs.mkdirSync(rootDir, { recursive: true });
|
|
10
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
11
|
+
const fileName = `${stamp}-${sanitize(selectedFailure.serviceName)}-${sanitize(path.basename(selectedFailure.filePath))}.json`;
|
|
12
|
+
const filePath = path.join(rootDir, fileName);
|
|
13
|
+
const payload = {
|
|
14
|
+
serviceName: selectedFailure.serviceName,
|
|
15
|
+
filePath: selectedFailure.filePath,
|
|
16
|
+
provider: agentSession.provider,
|
|
17
|
+
status: agentSession.status,
|
|
18
|
+
startedAt: agentSession.startedAt || null,
|
|
19
|
+
endedAt: agentSession.endedAt || null,
|
|
20
|
+
finalText: agentSession.finalText || "",
|
|
21
|
+
error: agentSession.error || null,
|
|
22
|
+
exitCode: agentSession.exitCode ?? null,
|
|
23
|
+
transcriptEntries: agentSession.transcriptEntries || [],
|
|
24
|
+
timeline: agentSession.timeline || [],
|
|
25
|
+
summary: agentSession.summary || null,
|
|
26
|
+
activePhase: agentSession.activePhase || null,
|
|
27
|
+
};
|
|
28
|
+
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
29
|
+
return filePath;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sanitize(value) {
|
|
36
|
+
return String(value || "unknown").replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
37
|
+
}
|
|
@@ -4,10 +4,13 @@ import { createRunSessionState } from "../tui/run-session-state.mjs";
|
|
|
4
4
|
import { RunSessionApp } from "../tui/run-session-app.mjs";
|
|
5
5
|
import { suiteSelectionType } from "../../runner/suite-selection.mjs";
|
|
6
6
|
import { startHostedInvestigation } from "../agents/investigate.mjs";
|
|
7
|
+
import { createInvestigationInterpreter } from "../agents/investigation-interpreter.mjs";
|
|
8
|
+
import { writeInvestigationLog } from "../agents/investigation-log.mjs";
|
|
7
9
|
|
|
8
10
|
export function createTreeReporter({ stdout = process.stdout, stderr = process.stderr, productDir } = {}) {
|
|
9
11
|
const sessionState = createRunSessionState();
|
|
10
12
|
let activeAgentSession = null;
|
|
13
|
+
let activeInterpreter = null;
|
|
11
14
|
let investigationToken = 0;
|
|
12
15
|
|
|
13
16
|
const app = render(
|
|
@@ -101,9 +104,11 @@ export function createTreeReporter({ stdout = process.stdout, stderr = process.s
|
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
const token = ++investigationToken;
|
|
107
|
+
let finalDelivered = false;
|
|
104
108
|
sessionState.beginInvestigation({ provider, userMessage });
|
|
105
109
|
|
|
106
110
|
try {
|
|
111
|
+
activeInterpreter = createInvestigationInterpreter();
|
|
107
112
|
activeAgentSession = startHostedInvestigation({
|
|
108
113
|
productDir,
|
|
109
114
|
serviceName: snapshot.selectedFailure.serviceName,
|
|
@@ -112,28 +117,43 @@ export function createTreeReporter({ stdout = process.stdout, stderr = process.s
|
|
|
112
117
|
userMessage,
|
|
113
118
|
onEvent(event) {
|
|
114
119
|
if (token !== investigationToken) return;
|
|
115
|
-
|
|
120
|
+
if (event.type === "final") finalDelivered = true;
|
|
121
|
+
const presentation = activeInterpreter?.consumeProviderEvent(event) || null;
|
|
122
|
+
sessionState.recordInvestigationProgress(event, presentation);
|
|
116
123
|
},
|
|
117
124
|
});
|
|
118
125
|
const result = await activeAgentSession.completion;
|
|
119
126
|
if (token !== investigationToken) return;
|
|
120
127
|
activeAgentSession = null;
|
|
128
|
+
if (result.finalText && !finalDelivered) {
|
|
129
|
+
const finalEvent = { type: "final", text: result.finalText };
|
|
130
|
+
const presentation = activeInterpreter?.consumeProviderEvent(finalEvent) || null;
|
|
131
|
+
sessionState.recordInvestigationProgress(finalEvent, presentation);
|
|
132
|
+
}
|
|
121
133
|
if (result.cancelled) {
|
|
122
134
|
sessionState.cancelAgentSession("Cancelled investigation.");
|
|
135
|
+
persistInvestigationLog();
|
|
136
|
+
activeInterpreter = null;
|
|
123
137
|
return;
|
|
124
138
|
}
|
|
125
139
|
if (result.exitCode !== 0 && !result.finalText) {
|
|
126
140
|
sessionState.failAgentSession(result.stderr || `Agent exited with code ${result.exitCode}`);
|
|
141
|
+
persistInvestigationLog();
|
|
142
|
+
activeInterpreter = null;
|
|
127
143
|
return;
|
|
128
144
|
}
|
|
129
145
|
sessionState.completeAgentSession({
|
|
130
146
|
finalText: result.finalText,
|
|
131
147
|
exitCode: result.exitCode,
|
|
132
148
|
});
|
|
149
|
+
persistInvestigationLog();
|
|
150
|
+
activeInterpreter = null;
|
|
133
151
|
} catch (error) {
|
|
134
152
|
if (token !== investigationToken) return;
|
|
135
153
|
activeAgentSession = null;
|
|
136
154
|
sessionState.failAgentSession(error);
|
|
155
|
+
persistInvestigationLog();
|
|
156
|
+
activeInterpreter = null;
|
|
137
157
|
}
|
|
138
158
|
}
|
|
139
159
|
|
|
@@ -146,6 +166,8 @@ export function createTreeReporter({ stdout = process.stdout, stderr = process.s
|
|
|
146
166
|
activeAgentSession.cancel();
|
|
147
167
|
activeAgentSession = null;
|
|
148
168
|
sessionState.cancelAgentSession("Cancelled investigation.");
|
|
169
|
+
persistInvestigationLog();
|
|
170
|
+
activeInterpreter = null;
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
function close() {
|
|
@@ -156,4 +178,14 @@ export function createTreeReporter({ stdout = process.stdout, stderr = process.s
|
|
|
156
178
|
}
|
|
157
179
|
app.unmount();
|
|
158
180
|
}
|
|
181
|
+
|
|
182
|
+
function persistInvestigationLog() {
|
|
183
|
+
const snapshot = sessionState.getSnapshot();
|
|
184
|
+
if (!snapshot.selectedFailure || !snapshot.agentSession) return;
|
|
185
|
+
writeInvestigationLog({
|
|
186
|
+
productDir,
|
|
187
|
+
selectedFailure: snapshot.selectedFailure,
|
|
188
|
+
agentSession: snapshot.agentSession,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
159
191
|
}
|
|
@@ -100,6 +100,10 @@ export function RunSessionApp({
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
if (snapshot.mode === "investigating") {
|
|
103
|
+
if (input === "t") {
|
|
104
|
+
sessionState.toggleInvestigationViewMode();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
103
107
|
if (input === "x") {
|
|
104
108
|
onCancelInvestigation?.();
|
|
105
109
|
return;
|
|
@@ -159,9 +163,9 @@ export function buildFooterText(snapshot) {
|
|
|
159
163
|
if (!snapshot.finished) return "Run in progress";
|
|
160
164
|
if (snapshot.mode === "investigating") {
|
|
161
165
|
if (snapshot.agentSession?.status === "running" || snapshot.agentSession?.status === "starting") {
|
|
162
|
-
return "x cancel investigation · q quit";
|
|
166
|
+
return "t transcript · x cancel investigation · q quit";
|
|
163
167
|
}
|
|
164
|
-
return "b back to summary · q quit";
|
|
168
|
+
return "t transcript · b back to summary · q quit";
|
|
165
169
|
}
|
|
166
170
|
if (!snapshot.selectedFailure) return "q quit";
|
|
167
171
|
return "↑/↓ select failure · y investigate · c Claude · o Codex · q quit";
|
|
@@ -329,7 +333,9 @@ function buildFailureDetailPane(snapshot, detailLines) {
|
|
|
329
333
|
}
|
|
330
334
|
|
|
331
335
|
function buildAgentPane(snapshot) {
|
|
332
|
-
const
|
|
336
|
+
const transcriptEntries = snapshot.agentSession?.transcriptEntries || [];
|
|
337
|
+
const timeline = snapshot.agentSession?.timeline || [];
|
|
338
|
+
const viewMode = snapshot.agentSession?.viewMode || "summary";
|
|
333
339
|
const status = snapshot.agentSession?.status || "idle";
|
|
334
340
|
const statusText =
|
|
335
341
|
status === "running" || status === "starting"
|
|
@@ -337,21 +343,77 @@ function buildAgentPane(snapshot) {
|
|
|
337
343
|
: status === "error"
|
|
338
344
|
? red(`Status: ${status}`)
|
|
339
345
|
: green(`Status: ${status}`);
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
: entries.slice(-32).map((entry, index) =>
|
|
344
|
-
createElement(Text, { key: `agent-${index}` }, formatAgentEntry(entry))
|
|
345
|
-
);
|
|
346
|
+
const phaseText = snapshot.agentSession?.activePhase
|
|
347
|
+
? dim(`Phase: ${snapshot.agentSession.activePhase}`)
|
|
348
|
+
: dim("Phase: planning");
|
|
346
349
|
|
|
347
350
|
return [
|
|
348
351
|
createElement(Text, { key: "agent-title" }, bold("Investigation")),
|
|
349
352
|
createElement(Text, { key: "agent-status" }, statusText),
|
|
353
|
+
createElement(Text, { key: "agent-phase" }, phaseText),
|
|
350
354
|
createElement(Text, { key: "agent-gap" }, ""),
|
|
351
|
-
...
|
|
355
|
+
...(viewMode === "transcript"
|
|
356
|
+
? buildTranscriptEntries(transcriptEntries)
|
|
357
|
+
: buildSummaryEntries(snapshot.agentSession, timeline)),
|
|
352
358
|
];
|
|
353
359
|
}
|
|
354
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
|
+
|
|
355
417
|
export function formatAgentEntry(entry) {
|
|
356
418
|
if (entry.kind === "tool") return yellow(`[tool] ${entry.text}`);
|
|
357
419
|
if (entry.kind === "status") return dim(`[status] ${entry.text}`);
|
|
@@ -125,7 +125,7 @@ export function createRunSessionState() {
|
|
|
125
125
|
function appendTranscriptEntry(kind, text) {
|
|
126
126
|
if (!agentSession || !text) return;
|
|
127
127
|
const normalizedText = String(text);
|
|
128
|
-
const entries = agentSession.
|
|
128
|
+
const entries = agentSession.transcriptEntries || [];
|
|
129
129
|
const lastEntry = entries.at(-1);
|
|
130
130
|
if (kind === "assistant" && lastEntry?.kind === "assistant") {
|
|
131
131
|
lastEntry.text += normalizedText;
|
|
@@ -133,7 +133,7 @@ export function createRunSessionState() {
|
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
135
135
|
entries.push({ kind, text: normalizedText });
|
|
136
|
-
agentSession.
|
|
136
|
+
agentSession.transcriptEntries = entries;
|
|
137
137
|
agentSession.updatedAt = Date.now();
|
|
138
138
|
}
|
|
139
139
|
|
|
@@ -324,19 +324,26 @@ export function createRunSessionState() {
|
|
|
324
324
|
status: "starting",
|
|
325
325
|
startedAt: Date.now(),
|
|
326
326
|
updatedAt: Date.now(),
|
|
327
|
-
|
|
327
|
+
rawEvents: [],
|
|
328
|
+
transcriptEntries: [],
|
|
329
|
+
timeline: [],
|
|
330
|
+
summary: null,
|
|
331
|
+
activePhase: "planning",
|
|
332
|
+
activeStep: null,
|
|
333
|
+
viewMode: "summary",
|
|
328
334
|
};
|
|
329
335
|
notify();
|
|
330
336
|
},
|
|
331
337
|
|
|
332
|
-
|
|
338
|
+
recordInvestigationProgress(event, presentation = null) {
|
|
333
339
|
if (!agentSession || !event) return;
|
|
340
|
+
agentSession.rawEvents.push({ ...event });
|
|
334
341
|
if (event.type === "start") {
|
|
335
342
|
agentSession.status = "running";
|
|
336
343
|
} else if (event.type === "delta") {
|
|
337
344
|
appendTranscriptEntry("assistant", event.text || "");
|
|
338
345
|
} else if (event.type === "final") {
|
|
339
|
-
if (event.text && !(agentSession.
|
|
346
|
+
if (event.text && !(agentSession.transcriptEntries || []).some((entry) => entry.kind === "assistant")) {
|
|
340
347
|
appendTranscriptEntry("assistant", event.text);
|
|
341
348
|
}
|
|
342
349
|
agentSession.finalText = event.text || agentSession.finalText || "";
|
|
@@ -349,6 +356,23 @@ export function createRunSessionState() {
|
|
|
349
356
|
} else if (event.type === "exit") {
|
|
350
357
|
agentSession.exitCode = event.code;
|
|
351
358
|
}
|
|
359
|
+
|
|
360
|
+
if (presentation) {
|
|
361
|
+
agentSession.activePhase = presentation.phase || agentSession.activePhase || null;
|
|
362
|
+
agentSession.activeStep = presentation.activeStep || null;
|
|
363
|
+
agentSession.timeline = presentation.timeline || [];
|
|
364
|
+
agentSession.summary = presentation.summary || null;
|
|
365
|
+
}
|
|
366
|
+
notify();
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
appendAgentEvent(event) {
|
|
370
|
+
this.recordInvestigationProgress(event);
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
toggleInvestigationViewMode() {
|
|
374
|
+
if (!agentSession) return;
|
|
375
|
+
agentSession.viewMode = agentSession.viewMode === "summary" ? "transcript" : "summary";
|
|
352
376
|
notify();
|
|
353
377
|
},
|
|
354
378
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.85",
|
|
4
4
|
"description": "Browser bridge helpers for testkit",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@elench/testkit-protocol": "0.1.
|
|
25
|
+
"@elench/testkit-protocol": "0.1.85"
|
|
26
26
|
},
|
|
27
27
|
"private": false
|
|
28
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.85",
|
|
4
4
|
"description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -82,10 +82,10 @@
|
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
84
|
"@babel/code-frame": "^7.29.0",
|
|
85
|
-
"@elench/next-analysis": "0.1.
|
|
86
|
-
"@elench/testkit-bridge": "0.1.
|
|
87
|
-
"@elench/testkit-protocol": "0.1.
|
|
88
|
-
"@elench/ts-analysis": "0.1.
|
|
85
|
+
"@elench/next-analysis": "0.1.85",
|
|
86
|
+
"@elench/testkit-bridge": "0.1.85",
|
|
87
|
+
"@elench/testkit-protocol": "0.1.85",
|
|
88
|
+
"@elench/ts-analysis": "0.1.85",
|
|
89
89
|
"@oclif/core": "^4.10.6",
|
|
90
90
|
"esbuild": "^0.25.11",
|
|
91
91
|
"execa": "^9.5.0",
|