@elench/testkit 0.1.111 → 0.1.113

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 (43) hide show
  1. package/README.md +1 -1
  2. package/lib/bundler/index.mjs +95 -1
  3. package/lib/cli/args.mjs +1 -1
  4. package/lib/cli/assistant/app.mjs +70 -20
  5. package/lib/cli/assistant/command-normalize.mjs +22 -0
  6. package/lib/cli/assistant/command-observer.mjs +49 -4
  7. package/lib/cli/assistant/command-results.mjs +10 -1
  8. package/lib/cli/assistant/context-pack.mjs +45 -15
  9. package/lib/cli/assistant/domain.d.mts +59 -0
  10. package/lib/cli/assistant/domain.d.mts.map +1 -0
  11. package/lib/cli/assistant/domain.mjs +2 -0
  12. package/lib/cli/assistant/domain.mjs.map +1 -0
  13. package/lib/cli/assistant/session.mjs +3 -1
  14. package/lib/cli/assistant/state.mjs +109 -2
  15. package/lib/cli/assistant/view-model.mjs +69 -9
  16. package/lib/cli/commands/run.mjs +1 -1
  17. package/lib/cli/components/blocks/run-tree.mjs +30 -64
  18. package/lib/cli/entrypoint.mjs +1 -1
  19. package/lib/cli/renderers/run/inline-detail.mjs +64 -0
  20. package/lib/cli/state/run/model.mjs +24 -95
  21. package/lib/cli/state/run/state.mjs +0 -22
  22. package/lib/config/discovery.mjs +0 -10
  23. package/lib/discovery/index.mjs +1 -1
  24. package/lib/domain/test-types.mjs +5 -14
  25. package/lib/runner/default-runtime-runner.mjs +3 -1
  26. package/lib/runner/failure-details.mjs +22 -0
  27. package/lib/runner/maintenance.mjs +1 -1
  28. package/lib/runner/provenance.mjs +4 -1
  29. package/lib/runner/results.mjs +31 -0
  30. package/lib/runner/status-model.mjs +15 -7
  31. package/lib/runner/suite-selection.mjs +2 -3
  32. package/node_modules/@elench/next-analysis/package.json +1 -1
  33. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  34. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  35. package/node_modules/@elench/ts-analysis/package.json +1 -1
  36. package/package.json +5 -5
  37. package/lib/cli/components/primitives/filter-bar.mjs +0 -12
  38. package/lib/cli/state/tree/fuzzy-match.mjs +0 -106
  39. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts +0 -188
  40. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +0 -1
  41. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js +0 -293
  42. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js.map +0 -1
  43. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +0 -25
@@ -0,0 +1,59 @@
1
+ export type AssistantTurnState = "idle" | "slash_running" | "provider_running" | "cancelling" | "failed";
2
+ export interface AssistantTurn {
3
+ id: string | null;
4
+ state: AssistantTurnState;
5
+ input?: string;
6
+ startedAt?: string;
7
+ finishedAt?: string;
8
+ failedAt?: string;
9
+ error?: AssistantDiagnostic;
10
+ }
11
+ export interface AssistantDiagnostic {
12
+ level?: "info" | "warning" | "error";
13
+ code?: string;
14
+ message: string;
15
+ timestamp?: string;
16
+ }
17
+ export type AssistantActivityKind = "user_message" | "assistant_message" | "system_message" | "provider_command" | "provider_status" | "testkit_command" | "testkit_run";
18
+ export interface AssistantActivity {
19
+ id: string;
20
+ kind: AssistantActivityKind;
21
+ turnId: string | null;
22
+ title?: string | null;
23
+ text?: string;
24
+ status?: "pending" | "running" | "done" | "error" | null;
25
+ command?: string | null;
26
+ commandId?: string | null;
27
+ supersededBy?: string | null;
28
+ data?: unknown;
29
+ }
30
+ export interface AssistantCommandIdentity {
31
+ sessionId: string | null;
32
+ turnId: string | null;
33
+ commandId: string;
34
+ }
35
+ export interface AssistantCommandObservation {
36
+ type: "command_start" | "command_exit" | "command_result" | "run_artifact";
37
+ identity: AssistantCommandIdentity;
38
+ kind?: string;
39
+ argv?: string[];
40
+ cwd?: string;
41
+ exitCode?: number | null;
42
+ signal?: string | null;
43
+ artifactRunId?: string | null;
44
+ }
45
+ export type AssistantProviderEventType = "session-start" | "status" | "assistant-delta" | "assistant-final" | "tool-start" | "tool-update" | "tool-end" | "error" | "session-end";
46
+ export interface AssistantProviderEvent {
47
+ type: AssistantProviderEventType;
48
+ provider?: "codex" | "claude" | string;
49
+ id?: string | null;
50
+ name?: string;
51
+ text?: string;
52
+ status?: string;
53
+ input?: unknown;
54
+ output?: unknown;
55
+ transient?: boolean;
56
+ display?: boolean;
57
+ data?: unknown;
58
+ }
59
+ //# sourceMappingURL=domain.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"domain.d.mts","sourceRoot":"","sources":["../../../src/cli/assistant/domain.mts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,eAAe,GAAG,kBAAkB,GAAG,YAAY,GAAG,QAAQ,CAAC;AAEzG,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,KAAK,EAAE,kBAAkB,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,qBAAqB,GAC7B,cAAc,GACd,mBAAmB,GACnB,gBAAgB,GAChB,kBAAkB,GAClB,iBAAiB,GACjB,iBAAiB,GACjB,aAAa,CAAC;AAElB,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,qBAAqB,CAAC;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IACzD,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,eAAe,GAAG,cAAc,GAAG,gBAAgB,GAAG,cAAc,CAAC;IAC3E,QAAQ,EAAE,wBAAwB,CAAC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,MAAM,0BAA0B,GAClC,eAAe,GACf,QAAQ,GACR,iBAAiB,GACjB,iBAAiB,GACjB,YAAY,GACZ,aAAa,GACb,UAAU,GACV,OAAO,GACP,aAAa,CAAC;AAElB,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,0BAA0B,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;IACvC,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=domain.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"domain.mjs","sourceRoot":"","sources":["../../../src/cli/assistant/domain.mts"],"names":[],"mappings":""}
@@ -11,6 +11,7 @@ export async function runAssistantConversationTurn({
11
11
  userMessage,
12
12
  provider = "auto",
13
13
  settings = null,
14
+ turnId = null,
14
15
  env = process.env,
15
16
  configs,
16
17
  commandLog,
@@ -24,6 +25,7 @@ export async function runAssistantConversationTurn({
24
25
  productDir,
25
26
  runState,
26
27
  commandLog,
28
+ turnId,
27
29
  onEvent: onToolEvent,
28
30
  });
29
31
 
@@ -38,7 +40,7 @@ export async function runAssistantConversationTurn({
38
40
 
39
41
  const runtimeSettings = settings || { provider };
40
42
  const resolvedProvider = resolvePreferredProvider(runtimeSettings.provider || provider, env);
41
- const providerEnv = commandLog?.providerEnv?.(env) || env;
43
+ const providerEnv = commandLog?.providerEnv?.(env, { turnId }) || env;
42
44
  const timeoutMs = resolveProviderTimeoutMs(providerEnv);
43
45
  const tracePath = shouldTraceProviderEvents(env, providerEnv)
44
46
  ? commandLog?.providerEventsPath || path.join(commandLog?.contextDir || path.join(productDir, ".testkit", "assistant"), "provider-events.jsonl")
@@ -17,6 +17,7 @@ import { parseSlashCommand, formatSlashHelpLines } from "./slash-commands.mjs";
17
17
  import { executeAssistantAction } from "./actions.mjs";
18
18
  import { runAssistantConversationTurn } from "./session.mjs";
19
19
  import { prepareAssistantContextPack } from "./context-pack.mjs";
20
+ import { normalizeCommandLine, unwrapShellCommand } from "./command-normalize.mjs";
20
21
  import {
21
22
  discoverAssistantModels,
22
23
  formatModelChoices,
@@ -88,6 +89,7 @@ export function createAssistantState({
88
89
  let activeStatus = null;
89
90
  let startupNoticeEmitted = false;
90
91
  let lastTurnError = null;
92
+ let activeTurn = idleTurn();
91
93
  let contextUsage = buildContextUsage({
92
94
  provider: resolvedProviderName || settings.provider,
93
95
  model: settings.model,
@@ -109,6 +111,7 @@ export function createAssistantState({
109
111
  function appendMessage(message) {
110
112
  const entry = {
111
113
  id: `msg-${messages.length + 1}`,
114
+ ...(activeTurn?.id && !message.turnId ? { turnId: activeTurn.id } : {}),
112
115
  ...message,
113
116
  };
114
117
  messages.push(entry);
@@ -182,6 +185,7 @@ export function createAssistantState({
182
185
  commandLog,
183
186
  attachRunSession,
184
187
  completeRunSession,
188
+ updateMessage,
185
189
 
186
190
  async loadLatestArtifact() {
187
191
  try {
@@ -364,7 +368,8 @@ export function createAssistantState({
364
368
  startupNoticeEmitted = true;
365
369
  appendMessage({ role: "system", text: notice });
366
370
  }
367
- appendMessage({ role: "user", text: trimmed });
371
+ const turnId = createAssistantTurnId();
372
+ appendMessage({ role: "user", text: trimmed, turnId });
368
373
 
369
374
  const slash = parseSlashCommandSafe(trimmed);
370
375
  if (slash?.type === "__error__") {
@@ -373,6 +378,8 @@ export function createAssistantState({
373
378
  }
374
379
  if (slash) {
375
380
  try {
381
+ activeTurn = { id: turnId, state: "slash_running", input: trimmed, startedAt: new Date().toISOString() };
382
+ commandLog.setActiveTurnId?.(turnId);
376
383
  setBusy(true, `Running ${slash.type}...`);
377
384
  await executeSlashCommand({
378
385
  slash,
@@ -389,6 +396,14 @@ export function createAssistantState({
389
396
  text: error instanceof Error ? error.message : String(error),
390
397
  });
391
398
  } finally {
399
+ commandLog.setActiveTurnId?.(null);
400
+ activeTurn = {
401
+ id: turnId,
402
+ state: "idle",
403
+ input: trimmed,
404
+ startedAt: activeTurn?.startedAt || new Date().toISOString(),
405
+ finishedAt: new Date().toISOString(),
406
+ };
392
407
  setBusy(false, null);
393
408
  }
394
409
  refreshContextPack();
@@ -397,6 +412,8 @@ export function createAssistantState({
397
412
  }
398
413
 
399
414
  try {
415
+ activeTurn = { id: turnId, state: "provider_running", input: trimmed, startedAt: new Date().toISOString() };
416
+ commandLog.setActiveTurnId?.(turnId);
400
417
  setBusy(true, `Thinking with ${settings.provider === "auto" ? "provider" : settings.provider}...`);
401
418
  const providerTurn = createProviderTurnState();
402
419
  await runAssistantConversationTurn({
@@ -405,6 +422,7 @@ export function createAssistantState({
405
422
  transcript: buildConversationTranscript(messages),
406
423
  userMessage: trimmed,
407
424
  settings,
425
+ turnId,
408
426
  env,
409
427
  configs,
410
428
  commandLog,
@@ -447,8 +465,26 @@ export function createAssistantState({
447
465
  role: "system",
448
466
  text: lastTurnError.message,
449
467
  });
468
+ activeTurn = {
469
+ id: turnId,
470
+ state: "failed",
471
+ input: trimmed,
472
+ startedAt: activeTurn?.startedAt || new Date().toISOString(),
473
+ failedAt: new Date().toISOString(),
474
+ error: lastTurnError,
475
+ };
450
476
  } finally {
477
+ commandLog.setActiveTurnId?.(null);
451
478
  refreshContextPack();
479
+ if (activeTurn?.id === turnId && activeTurn.state !== "failed") {
480
+ activeTurn = {
481
+ id: turnId,
482
+ state: "idle",
483
+ input: trimmed,
484
+ startedAt: activeTurn?.startedAt || new Date().toISOString(),
485
+ finishedAt: new Date().toISOString(),
486
+ };
487
+ }
452
488
  setBusy(false, null);
453
489
  }
454
490
  },
@@ -459,11 +495,13 @@ export function createAssistantState({
459
495
  },
460
496
 
461
497
  getSnapshot() {
498
+ const snapshotMessages = messages.map(serializeMessageForSnapshot);
462
499
  return {
463
500
  context: buildContextSelection(runState.getSnapshot()),
464
501
  run: runState.getSnapshot(),
465
502
  productDir,
466
- messages: messages.map(serializeMessageForSnapshot),
503
+ messages: snapshotMessages,
504
+ activities: buildSnapshotActivities(snapshotMessages),
467
505
  composer: composerState.text,
468
506
  composerCursor: composerState.cursor,
469
507
  notice,
@@ -475,6 +513,7 @@ export function createAssistantState({
475
513
  providerArgs: [...settings.providerArgs],
476
514
  cliConfig,
477
515
  activeStatus,
516
+ turn: activeTurn,
478
517
  lastTurnError,
479
518
  diagnostics: [...diagnostics],
480
519
  contextUsage,
@@ -508,6 +547,17 @@ function resolveInitialProvider(provider, env) {
508
547
  return null;
509
548
  }
510
549
 
550
+ function idleTurn() {
551
+ return {
552
+ id: null,
553
+ state: "idle",
554
+ };
555
+ }
556
+
557
+ function createAssistantTurnId(now = Date.now(), random = Math.random) {
558
+ return `turn-${now}-${random().toString(36).slice(2, 10)}`;
559
+ }
560
+
511
561
  async function executeSlashCommand({
512
562
  slash,
513
563
  state,
@@ -664,6 +714,7 @@ function handleAssistantToolEvent(state, event, appendMessage) {
664
714
  return;
665
715
  }
666
716
  if (event.type === "observed-testkit-command") {
717
+ suppressMatchingProviderCommand(state, event.command);
667
718
  appendMessage({
668
719
  role: "tool",
669
720
  toolName: event.command?.kind || "testkit",
@@ -679,6 +730,28 @@ function handleAssistantToolEvent(state, event, appendMessage) {
679
730
  }
680
731
  }
681
732
 
733
+ function suppressMatchingProviderCommand(state, command) {
734
+ const observed = normalizeCommandLine(formatObservedCommandLine(command));
735
+ if (!observed) return;
736
+ const snapshot = state.getSnapshot();
737
+ for (const message of snapshot.messages || []) {
738
+ if (message.role !== "provider-tool") continue;
739
+ const providerCommand = normalizeCommandLine(providerToolCommandLine(message.data));
740
+ if (!providerCommand || providerCommand !== observed) continue;
741
+ state.updateMessage?.(message.id, (current) => ({
742
+ data: {
743
+ ...(current.data || {}),
744
+ supersededByTestkitCommand: command?.commandId || true,
745
+ },
746
+ }));
747
+ }
748
+ }
749
+
750
+ function providerToolCommandLine(event) {
751
+ if (!event) return null;
752
+ return event.input || event.data?.command || event.data?.input || null;
753
+ }
754
+
682
755
  function createProviderTurnState() {
683
756
  return {
684
757
  assistantMessageId: null,
@@ -940,6 +1013,40 @@ function serializeMessageForSnapshot(message) {
940
1013
  };
941
1014
  }
942
1015
 
1016
+ function buildSnapshotActivities(messages) {
1017
+ return (messages || []).map((message) => {
1018
+ const base = {
1019
+ id: message.id,
1020
+ turnId: message.turnId || null,
1021
+ title: message.title || null,
1022
+ text: message.text || "",
1023
+ status: message.status || null,
1024
+ data: message.data || null,
1025
+ };
1026
+ if (message.role === "user") return { ...base, kind: "user_message" };
1027
+ if (message.role === "assistant") return { ...base, kind: "assistant_message" };
1028
+ if (message.role === "provider-tool") {
1029
+ return {
1030
+ ...base,
1031
+ kind: "provider_command",
1032
+ command: providerToolCommandLine(message.data),
1033
+ supersededBy: message.data?.supersededByTestkitCommand || null,
1034
+ };
1035
+ }
1036
+ if (message.role === "provider-activity") return { ...base, kind: "provider_status" };
1037
+ if (message.role === "tool" && message.data?.testkitRelated) {
1038
+ const kind = message.data?.kind === "run" ? "testkit_run" : "testkit_command";
1039
+ return {
1040
+ ...base,
1041
+ kind,
1042
+ command: message.data?.command || null,
1043
+ commandId: message.data?.commandId || null,
1044
+ };
1045
+ }
1046
+ return { ...base, kind: "system_message" };
1047
+ });
1048
+ }
1049
+
943
1050
  function buildConversationTranscript(messages) {
944
1051
  return (messages || [])
945
1052
  .filter((entry) => !["provider-activity", "provider-tool", "provider-error"].includes(entry.role))
@@ -1,5 +1,6 @@
1
1
  import path from "path";
2
2
  import { formatContextRemaining } from "./context-window.mjs";
3
+ import { normalizeCommandLine, unwrapShellCommand } from "./command-normalize.mjs";
3
4
 
4
5
  const PROVIDER_COMMAND_OUTPUT_PREVIEW_LINES = 12;
5
6
  const PROVIDER_FILE_READ_OUTPUT_PREVIEW_LINES = 8;
@@ -13,7 +14,7 @@ export function buildAssistantViewModel(snapshot, { cwd = process.cwd(), termina
13
14
  title: `testkit · ${repoName}`,
14
15
  welcome: buildWelcomeModel(snapshot, { cwd, providerLabel }),
15
16
  qualitySignal: buildQualitySignal(snapshot),
16
- blocks: buildTranscriptBlocks(snapshot.messages || []),
17
+ blocks: snapshot.activities ? buildActivityBlocks(snapshot.activities) : buildTranscriptBlocks(snapshot.messages || []),
17
18
  composer: {
18
19
  text: snapshot.composer || "",
19
20
  cursor: snapshot.composerCursor ?? 0,
@@ -134,7 +135,8 @@ export function buildWelcomeModel(snapshot, { cwd = process.cwd(), providerLabel
134
135
  }
135
136
 
136
137
  export function buildTranscriptBlocks(messages) {
137
- return (messages || []).map((message) => {
138
+ const visibleMessages = filterSupersededProviderCommands(messages || []);
139
+ return visibleMessages.map((message) => {
138
140
  const role = message.role || "system";
139
141
  if (role === "tool") {
140
142
  const outputPreview = summarizeOutput(message.text || "", TESTKIT_COMMAND_OUTPUT_PREVIEW_LINES);
@@ -204,6 +206,71 @@ export function buildTranscriptBlocks(messages) {
204
206
  });
205
207
  }
206
208
 
209
+ export function buildActivityBlocks(activities) {
210
+ const messages = (activities || [])
211
+ .filter((activity) => !activity.supersededBy)
212
+ .map((activity) => {
213
+ if (activity.kind === "user_message") return { id: activity.id, role: "user", text: activity.text || "" };
214
+ if (activity.kind === "assistant_message") return { id: activity.id, role: "assistant", text: activity.text || "" };
215
+ if (activity.kind === "provider_command") {
216
+ return {
217
+ id: activity.id,
218
+ role: "provider-tool",
219
+ title: activity.title || "provider command",
220
+ text: activity.text || "",
221
+ status: activity.status || null,
222
+ data: {
223
+ ...(activity.data || {}),
224
+ input: activity.command || activity.data?.input || null,
225
+ },
226
+ };
227
+ }
228
+ if (activity.kind === "provider_status") {
229
+ return {
230
+ id: activity.id,
231
+ role: "provider-activity",
232
+ title: activity.title || null,
233
+ text: activity.text || "",
234
+ status: activity.status || null,
235
+ };
236
+ }
237
+ if (activity.kind === "testkit_command" || activity.kind === "testkit_run") {
238
+ return {
239
+ id: activity.id,
240
+ role: "tool",
241
+ title: activity.title || (activity.kind === "testkit_run" ? "testkit run" : "testkit command"),
242
+ text: activity.text || "",
243
+ status: activity.status || null,
244
+ data: {
245
+ ...(activity.data || {}),
246
+ testkitRelated: true,
247
+ kind: activity.kind === "testkit_run" ? "run" : activity.data?.kind,
248
+ command: activity.command || activity.data?.command || null,
249
+ commandId: activity.commandId || activity.data?.commandId || null,
250
+ },
251
+ };
252
+ }
253
+ return { id: activity.id, role: "system", title: activity.title || null, text: activity.text || "" };
254
+ });
255
+ return buildTranscriptBlocks(messages);
256
+ }
257
+
258
+ function filterSupersededProviderCommands(messages) {
259
+ const observedCommands = new Set(
260
+ (messages || [])
261
+ .filter((message) => message.role === "tool" && message.data?.testkitRelated)
262
+ .map((message) => normalizeCommandLine(message.data?.command))
263
+ .filter(Boolean)
264
+ );
265
+ return (messages || []).filter((message) => {
266
+ if (message.role !== "provider-tool") return true;
267
+ if (message.data?.supersededByTestkitCommand) return false;
268
+ const providerCommand = normalizeCommandLine(message.data?.input || message.data?.data?.command || message.data?.data?.input);
269
+ if (!providerCommand) return true;
270
+ return !observedCommands.has(providerCommand);
271
+ });
272
+ }
273
+
207
274
  export function buildStatusLine(snapshot, { cwd = process.cwd(), providerLabel = null } = {}) {
208
275
  const context = formatContextRemaining(snapshot.contextUsage);
209
276
  const provider = providerLabel || buildProviderLabel(snapshot);
@@ -339,13 +406,6 @@ function isDiffProducingShellCommand(command) {
339
406
  return /^((git\s+diff|git\s+show|diff|apply_patch)\b|.*\bapply_patch\b)/.test(normalized);
340
407
  }
341
408
 
342
- function unwrapShellCommand(command) {
343
- const text = String(command || "").trim();
344
- const match = text.match(/(?:^|\s)(?:[^\s'"]*\/)?(?:bash|sh|zsh)\s+-lc\s+(['"])([\s\S]*)\1\s*$/);
345
- if (!match) return text;
346
- return match[2].replace(/\\(["'\\$`])/g, "$1").trim();
347
- }
348
-
349
409
  function stringifyMaybe(value) {
350
410
  if (value == null) return "";
351
411
  if (typeof value === "string") return value;
@@ -14,7 +14,7 @@ export default class RunCommand extends Command {
14
14
  type: Args.string({
15
15
  description: `Optional suite type shortcut: ${publicTestTypeListText({ includeAll: true })}`,
16
16
  required: false,
17
- options: publicTestTypeList({ includeAll: true, includeLegacy: true }),
17
+ options: publicTestTypeList({ includeAll: true }),
18
18
  }),
19
19
  };
20
20
 
@@ -12,8 +12,8 @@ import {
12
12
  yellow,
13
13
  } from "../../terminal/colors.mjs";
14
14
  import { renderSummaryBox } from "../primitives/summary-box.mjs";
15
- import { applyHighlight } from "../../state/tree/fuzzy-match.mjs";
16
- import { FilterBar } from "../primitives/filter-bar.mjs";
15
+ import { getTerminalWidth } from "../../terminal/layout.mjs";
16
+ import { renderFailureDetail, renderPassedDetail } from "../../renderers/run/inline-detail.mjs";
17
17
 
18
18
  const SPINNER_FRAMES = ["|", "/", "-", "\\"];
19
19
 
@@ -25,8 +25,9 @@ export function RunTreeView({
25
25
  interactive = true,
26
26
  } = {}) {
27
27
  const { exit } = useApp();
28
+ const controlledSnapshot = Boolean(snapshotOverride);
28
29
  const [snapshot, setSnapshot] = useState(() => snapshotOverride || runState.getSnapshot());
29
- const { frame } = useAnimation({ interval: 80, isActive: !snapshot.finished });
30
+ const { frame } = useAnimation({ interval: 80, isActive: !controlledSnapshot && !snapshot.finished });
30
31
  const spinnerFrame = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
31
32
 
32
33
  useEffect(() => {
@@ -49,38 +50,10 @@ export function RunTreeView({
49
50
  return;
50
51
  }
51
52
 
52
- if (snapshot.filter.active) {
53
- if (key.escape) {
54
- runState.deactivateFilter();
55
- return;
56
- }
57
- if (key.return) return;
58
- if (key.downArrow || input === "j") {
59
- runState.moveCursorDown();
60
- return;
61
- }
62
- if (key.upArrow || input === "k") {
63
- runState.moveCursorUp();
64
- return;
65
- }
66
- if (key.backspace || key.delete) {
67
- runState.updateFilterQuery(snapshot.filter.query.slice(0, -1));
68
- return;
69
- }
70
- if (isPrintableInput(input, key)) {
71
- runState.updateFilterQuery(`${snapshot.filter.query}${input}`);
72
- }
73
- return;
74
- }
75
-
76
53
  if (input === "q") {
77
54
  (onRequestClose || exit)();
78
55
  return;
79
56
  }
80
- if (input === "/") {
81
- runState.activateFilter();
82
- return;
83
- }
84
57
  if (key.downArrow || input === "j") {
85
58
  runState.moveCursorDown();
86
59
  return;
@@ -96,6 +69,7 @@ export function RunTreeView({
96
69
  }, { isActive: interactive });
97
70
 
98
71
  const visibleTreeEntries = useMemo(() => snapshot.visibleEntries || [], [snapshot.visibleEntries]);
72
+ const terminalWidth = getTerminalWidth(stdout, 100);
99
73
  const summaryLines = snapshot.finished && snapshot.summaryData
100
74
  ? renderSummaryBox(snapshot.summaryData.rows, { stdout })
101
75
  : [];
@@ -108,10 +82,8 @@ export function RunTreeView({
108
82
  createElement(
109
83
  Box,
110
84
  { key: "main", marginTop: 1, flexDirection: "column" },
111
- ...visibleTreeEntries.map(renderTreeLine.bind(null, snapshot, spinnerFrame))
85
+ ...visibleTreeEntries.flatMap(renderTreeLine.bind(null, snapshot, spinnerFrame, terminalWidth))
112
86
  ),
113
- snapshot.filter.active ? createElement(Text, { key: "filter-gap" }, "") : null,
114
- snapshot.filter.active ? createElement(FilterBar, { key: "filter-bar", filter: snapshot.filter }) : null,
115
87
  summaryLines.length > 0 ? createElement(Text, { key: "summary-gap" }, "") : null,
116
88
  ...summaryLines.map((line, index) => createElement(Text, { key: `summary-${index}` }, line)),
117
89
  createElement(Text, { key: "footer-gap" }, ""),
@@ -123,31 +95,36 @@ export function buildHeaderText(snapshot) {
123
95
  const progressText = snapshot.totalCount > 0 ? `[${snapshot.completedCount}/${snapshot.totalCount}]` : "[0/0]";
124
96
  const phaseText = snapshot.phase || (snapshot.finished ? "run complete" : "preparing");
125
97
  const sourceText = snapshot.dataSource === "artifact" ? "artifact run" : snapshot.finished ? "live summary" : "live run";
126
- const filterText = snapshot.filter.active ? `filter ${snapshot.filter.count}` : null;
127
- return [progressText, phaseText, sourceText, filterText].filter(Boolean).join(" · ");
98
+ return [progressText, phaseText, sourceText].filter(Boolean).join(" · ");
128
99
  }
129
100
 
130
101
  export function buildFooterText(snapshot, { interactive = true } = {}) {
131
102
  if (!snapshot.finished) return "Run in progress";
132
- if (snapshot.filter.active) {
133
- return "type to filter · ↑/↓ move · Esc clear filter · q quit";
134
- }
135
- return interactive ? "↑/↓ move · Enter collapse/expand · / filter · q quit" : "Run complete";
103
+ return interactive ? "↑/↓ move · Enter toggle detail · q quit" : "Run complete";
136
104
  }
137
105
 
138
- function renderTreeLine(snapshot, spinnerFrame, entry) {
106
+ function renderTreeLine(snapshot, spinnerFrame, terminalWidth, entry) {
139
107
  const selected = entry.id === snapshot.selectedEntryId;
140
108
  const pointer = selected ? `${bold(">")} ` : " ";
141
109
  const indent = " ".repeat(entry.depth);
142
- const rawLabel = entry.label;
143
- const match = entry.match;
144
- const highlightedLabel = match?.field === "label"
145
- ? applyHighlight(rawLabel, match.positions, bold)
146
- : rawLabel;
147
- const renderedLabel = decorateEntryLabel(entry, highlightedLabel, match);
110
+ const renderedLabel = decorateEntryLabel(entry, entry.label);
148
111
  const icon = entryIcon(entry, spinnerFrame);
149
112
  const line = `${pointer}${indent}${icon ? `${icon} ` : ""}${renderedLabel}${entrySuffix(entry)}`;
150
- return createElement(Text, { key: entry.id }, line);
113
+
114
+ const elements = [createElement(Text, { key: entry.id }, line)];
115
+
116
+ if (entry.kind === "file" && !entry.collapsed && snapshot.finished) {
117
+ const detailLines = entry.status === "failed"
118
+ ? renderFailureDetail(entry, { width: terminalWidth, regressionCatalog: snapshot.regressionCatalog })
119
+ : entry.status === "passed"
120
+ ? renderPassedDetail(entry, { width: terminalWidth })
121
+ : [];
122
+ for (let i = 0; i < detailLines.length; i++) {
123
+ elements.push(createElement(Text, { key: `${entry.id}-detail-${i}` }, detailLines[i]));
124
+ }
125
+ }
126
+
127
+ return elements;
151
128
  }
152
129
 
153
130
  function entryIcon(entry, spinnerFrame) {
@@ -161,16 +138,11 @@ function entryIcon(entry, spinnerFrame) {
161
138
  return dim("·");
162
139
  }
163
140
 
164
- function decorateEntryLabel(entry, label, match) {
165
- let rendered = label;
166
- if (entry.kind === "service") rendered = colorService(label);
167
- else if (entry.kind === "type") rendered = colorTypeBadge(label.toUpperCase());
168
- else if (entry.kind === "suite") rendered = bold(label);
169
-
170
- if (match?.field === "path" && entry.filePath) {
171
- rendered += ` ${dim(`(${applyHighlight(entry.filePath, match.positions, bold)})`)}`;
172
- }
173
- return rendered;
141
+ function decorateEntryLabel(entry, label) {
142
+ if (entry.kind === "service") return colorService(label);
143
+ if (entry.kind === "type") return colorTypeBadge(label.toUpperCase());
144
+ if (entry.kind === "suite") return bold(label);
145
+ return label;
174
146
  }
175
147
 
176
148
  function entrySuffix(entry) {
@@ -186,9 +158,3 @@ function entrySuffix(entry) {
186
158
  }
187
159
  return "";
188
160
  }
189
-
190
- function isPrintableInput(input, key) {
191
- if (!input) return false;
192
- if (key.ctrl || key.meta || key.escape || key.return || key.tab) return false;
193
- return input >= " ";
194
- }
@@ -16,7 +16,7 @@ export function normalizeCliArgs(argv) {
16
16
  "browser",
17
17
  "db",
18
18
  ]);
19
- const runTypeShortcuts = new Set(publicTestTypeList({ includeAll: true, includeLegacy: true }));
19
+ const runTypeShortcuts = new Set(publicTestTypeList({ includeAll: true }));
20
20
  const valueFlags = new Set([
21
21
  "--dir",
22
22
  "--service",
@@ -0,0 +1,64 @@
1
+ import { buildFailurePresentation } from "../../../runner/formatting.mjs";
2
+ import { renderIndentedBlock } from "../../terminal/layout.mjs";
3
+ import { dim, green, red } from "../../terminal/colors.mjs";
4
+ import figures from "figures";
5
+
6
+ export function renderFailureDetail(entry, { width, regressionCatalog } = {}) {
7
+ const fileSummary = {
8
+ service: entry.serviceName,
9
+ type: normalizeType(entry),
10
+ path: entry.filePath,
11
+ error: entry.error || null,
12
+ failureDetails: Array.isArray(entry.failureDetails) ? entry.failureDetails : [],
13
+ suiteError: null,
14
+ };
15
+
16
+ const failureView = buildFailurePresentation(fileSummary, regressionCatalog);
17
+ const lines = [];
18
+ const indent = " ".repeat(2 + (entry.depth || 0) * 2 + 2);
19
+
20
+ if (failureView.primary) {
21
+ lines.push(...renderIndentedBlock(failureView.primary, { width, indent }));
22
+ }
23
+ for (const detail of failureView.details) {
24
+ lines.push(...renderIndentedBlock(detail, { width, indent }));
25
+ }
26
+ return lines;
27
+ }
28
+
29
+ export function renderPassedDetail(entry, { width } = {}) {
30
+ const lines = [];
31
+ const indent = " ".repeat(2 + (entry.depth || 0) * 2 + 2);
32
+
33
+ const checks = Array.isArray(entry.checkDetails) ? entry.checkDetails : [];
34
+ if (checks.length > 0) {
35
+ const passed = checks.filter((c) => c.passed).length;
36
+ lines.push(...renderIndentedBlock(dim(`${passed}/${checks.length} checks passed`), { width, indent }));
37
+ const maxDisplay = 8;
38
+ const displayed = checks.slice(0, maxDisplay);
39
+ for (const check of displayed) {
40
+ const icon = check.passed ? green(figures.tick) : red(figures.cross);
41
+ lines.push(...renderIndentedBlock(`${icon} ${dim(check.name)}`, { width, indent: `${indent} ` }));
42
+ }
43
+ if (checks.length > maxDisplay) {
44
+ lines.push(...renderIndentedBlock(dim(`+${checks.length - maxDisplay} more`), { width, indent: `${indent} ` }));
45
+ }
46
+ }
47
+
48
+ const artifacts = Array.isArray(entry.artifacts) ? entry.artifacts : [];
49
+ for (const artifact of artifacts) {
50
+ if (artifact.kind === "testkit.checks") continue;
51
+ if (artifact.kind === "runtime.output") continue;
52
+ if (artifact.summary) {
53
+ lines.push(...renderIndentedBlock(dim(artifact.summary), { width, indent }));
54
+ }
55
+ }
56
+
57
+ return lines;
58
+ }
59
+
60
+ function normalizeType(entry) {
61
+ if (entry.framework === "playwright" || entry.type === "ui") return "ui";
62
+ if (entry.type === "integration") return "int";
63
+ return entry.type;
64
+ }