@elench/testkit 0.1.83 → 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.
Files changed (87) hide show
  1. package/lib/cli/agents/investigation-interpreter.mjs +320 -0
  2. package/lib/cli/agents/investigation-log.mjs +37 -0
  3. package/lib/cli/agents/providers/codex.mjs +1 -1
  4. package/lib/cli/presentation/tree-reporter.mjs +33 -1
  5. package/lib/cli/tui/run-session-app.mjs +73 -11
  6. package/lib/cli/tui/run-session-state.mjs +29 -5
  7. package/node_modules/@elench/next-analysis/package.json +1 -1
  8. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  9. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  10. package/node_modules/@elench/ts-analysis/package.json +1 -1
  11. package/package.json +7 -6
  12. package/lib/app/configs.test.mjs +0 -34
  13. package/lib/app/typecheck.test.mjs +0 -24
  14. package/lib/bundler/index.test.mjs +0 -164
  15. package/lib/cli/agents/investigation-context.test.mjs +0 -144
  16. package/lib/cli/agents/providers/claude.test.mjs +0 -95
  17. package/lib/cli/agents/providers/codex.test.mjs +0 -93
  18. package/lib/cli/args.test.mjs +0 -110
  19. package/lib/cli/command-helpers.test.mjs +0 -122
  20. package/lib/cli/commands/investigate.test.mjs +0 -83
  21. package/lib/cli/presentation/code-frames.test.mjs +0 -71
  22. package/lib/cli/presentation/events-reporter.test.mjs +0 -73
  23. package/lib/cli/presentation/run-reporter.test.mjs +0 -192
  24. package/lib/cli/presentation/summary-box.test.mjs +0 -60
  25. package/lib/cli/presentation/terminal-layout.test.mjs +0 -23
  26. package/lib/cli/presentation/tree-reporter.test.mjs +0 -166
  27. package/lib/cli/tui/run-session-app.test.mjs +0 -50
  28. package/lib/cli/tui/run-tree-state.test.mjs +0 -324
  29. package/lib/config/database.test.mjs +0 -29
  30. package/lib/config/discovery.test.mjs +0 -276
  31. package/lib/config/env.test.mjs +0 -40
  32. package/lib/config/index.test.mjs +0 -44
  33. package/lib/config/paths.test.mjs +0 -27
  34. package/lib/config/runtime.test.mjs +0 -82
  35. package/lib/config/skip-config.test.mjs +0 -63
  36. package/lib/config-api/index.test.mjs +0 -398
  37. package/lib/config-api/next-runtime-tsconfig.test.mjs +0 -58
  38. package/lib/coverage/backend-discovery.test.mjs +0 -61
  39. package/lib/coverage/evidence.test.mjs +0 -87
  40. package/lib/coverage/index.test.mjs +0 -715
  41. package/lib/coverage/routing.test.mjs +0 -36
  42. package/lib/coverage/shared.test.mjs +0 -72
  43. package/lib/database/fingerprint.test.mjs +0 -99
  44. package/lib/database/index.test.mjs +0 -95
  45. package/lib/database/naming.test.mjs +0 -39
  46. package/lib/database/state.test.mjs +0 -66
  47. package/lib/database/template-steps.test.mjs +0 -43
  48. package/lib/discovery/file-metadata.test.mjs +0 -51
  49. package/lib/discovery/index.test.mjs +0 -182
  50. package/lib/discovery/path-policy.test.mjs +0 -65
  51. package/lib/drizzle/index.test.mjs +0 -33
  52. package/lib/env/index.test.mjs +0 -82
  53. package/lib/history/index.test.mjs +0 -115
  54. package/lib/package.test.mjs +0 -59
  55. package/lib/playwright/index.test.mjs +0 -43
  56. package/lib/regressions/github.test.mjs +0 -324
  57. package/lib/regressions/index.test.mjs +0 -187
  58. package/lib/reporters/playwright.test.mjs +0 -167
  59. package/lib/runner/default-runtime-errors.test.mjs +0 -49
  60. package/lib/runner/execution-config.test.mjs +0 -67
  61. package/lib/runner/failure-details.test.mjs +0 -114
  62. package/lib/runner/formatting.test.mjs +0 -205
  63. package/lib/runner/metadata.test.mjs +0 -52
  64. package/lib/runner/planning.test.mjs +0 -371
  65. package/lib/runner/playwright-config.test.mjs +0 -78
  66. package/lib/runner/processes.test.mjs +0 -21
  67. package/lib/runner/regressions.test.mjs +0 -168
  68. package/lib/runner/reporting.test.mjs +0 -310
  69. package/lib/runner/results.test.mjs +0 -376
  70. package/lib/runner/runtime-manager.test.mjs +0 -252
  71. package/lib/runner/runtime-preparation.test.mjs +0 -141
  72. package/lib/runner/selection.test.mjs +0 -24
  73. package/lib/runner/setup-operations.test.mjs +0 -94
  74. package/lib/runner/state.test.mjs +0 -62
  75. package/lib/runner/suite-selection.test.mjs +0 -49
  76. package/lib/runner/template.test.mjs +0 -272
  77. package/lib/runtime-src/k6/http-checks.test.mjs +0 -120
  78. package/lib/runtime-src/k6/http.test.mjs +0 -205
  79. package/lib/runtime-src/shared/http-parsing.test.mjs +0 -69
  80. package/lib/shared/build-config.test.mjs +0 -132
  81. package/lib/shared/configured-steps.test.mjs +0 -102
  82. package/lib/shared/execution-schema.test.mjs +0 -26
  83. package/lib/shared/file-timeout.test.mjs +0 -64
  84. package/lib/shared/test-context.test.mjs +0 -43
  85. package/lib/timing/index.test.mjs +0 -64
  86. package/lib/toolchains/index.test.mjs +0 -168
  87. package/lib/vitest/index.test.mjs +0 -20
@@ -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
+ }
@@ -17,7 +17,7 @@ export function startCodexHostedSession({ cwd, prompt, onEvent, purpose = "inves
17
17
  const args = ["exec", "--json", "-o", outputFile];
18
18
 
19
19
  if (purpose === "investigate") {
20
- args.push("-s", "read-only", "-a", "never");
20
+ args.push("-s", "read-only");
21
21
  }
22
22
 
23
23
  args.push(prompt);
@@ -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
- sessionState.appendAgentEvent(event);
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
  }
@@ -80,7 +80,7 @@ export function RunSessionApp({
80
80
  sessionState.selectPreviousFailure();
81
81
  return;
82
82
  }
83
- if (key.return || input === "y" || input === "i") {
83
+ if (input === "y" || input === "i") {
84
84
  if (snapshot.selectedFailure) {
85
85
  onInvestigate?.({
86
86
  provider: "auto",
@@ -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 entries = snapshot.agentSession?.entries || [];
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
- const renderedEntries = entries.length === 0
342
- ? [createElement(Text, { key: "agent-empty" }, dim("Waiting for agent output..."))]
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
- ...renderedEntries,
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.entries || [];
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.entries = entries;
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
- entries: [],
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
- appendAgentEvent(event) {
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.entries || []).some((entry) => entry.kind === "assistant")) {
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/next-analysis",
3
- "version": "0.1.83",
3
+ "version": "0.1.85",
4
4
  "description": "SWC-backed Next.js source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-bridge",
3
- "version": "0.1.83",
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.83"
25
+ "@elench/testkit-protocol": "0.1.85"
26
26
  },
27
27
  "private": false
28
28
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-protocol",
3
- "version": "0.1.83",
3
+ "version": "0.1.85",
4
4
  "description": "Shared browser protocol for testkit bridge and extension consumers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",