@elench/testkit 0.1.97 → 0.1.98

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 (74) hide show
  1. package/lib/app/browser-bridge.mjs +1 -1
  2. package/lib/cli/assistant/app.mjs +25 -1
  3. package/lib/cli/assistant/composer.mjs +1 -1
  4. package/lib/cli/assistant/context-pack.mjs +4 -4
  5. package/lib/cli/assistant/interactive.mjs +1 -1
  6. package/lib/cli/assistant/prompt-builder.mjs +2 -2
  7. package/lib/cli/{agents → assistant/providers}/index.mjs +3 -3
  8. package/lib/cli/assistant/session.mjs +5 -5
  9. package/lib/cli/assistant/slash-commands.mjs +22 -1
  10. package/lib/cli/assistant/state.mjs +148 -75
  11. package/lib/cli/assistant/tool-registry.mjs +305 -39
  12. package/lib/cli/assistant/view-model.mjs +1 -1
  13. package/lib/cli/commands/assistant.mjs +4 -3
  14. package/lib/cli/commands/browser/serve.mjs +5 -23
  15. package/lib/cli/commands/cleanup.mjs +8 -2
  16. package/lib/cli/commands/db/snapshot/capture.mjs +8 -4
  17. package/lib/cli/commands/destroy.mjs +8 -2
  18. package/lib/cli/commands/discover.mjs +5 -27
  19. package/lib/cli/commands/doctor.mjs +5 -5
  20. package/lib/cli/commands/flags.mjs +61 -0
  21. package/lib/cli/commands/run.mjs +10 -2
  22. package/lib/cli/commands/status.mjs +10 -2
  23. package/lib/cli/commands/typecheck.mjs +5 -5
  24. package/lib/cli/{tui/inspect-app.mjs → components/blocks/run-tree.mjs} +29 -54
  25. package/lib/cli/{tui → components/primitives}/filter-bar.mjs +1 -1
  26. package/lib/cli/{presentation → components/primitives}/summary-box.mjs +1 -1
  27. package/lib/cli/config.mjs +63 -0
  28. package/lib/cli/operations/browser/serve/operation.mjs +23 -0
  29. package/lib/cli/operations/cleanup/operation.mjs +8 -0
  30. package/lib/cli/{db.mjs → operations/db/snapshot/capture/operation.mjs} +15 -9
  31. package/lib/cli/operations/destroy/operation.mjs +12 -0
  32. package/lib/cli/operations/discover/operation.mjs +32 -0
  33. package/lib/cli/operations/doctor/operation.mjs +5 -0
  34. package/lib/cli/operations/run/operation.mjs +129 -0
  35. package/lib/cli/operations/status/operation.mjs +7 -0
  36. package/lib/cli/operations/typecheck/operation.mjs +5 -0
  37. package/lib/cli/renderers/browser-serve/text.mjs +6 -0
  38. package/lib/cli/renderers/cleanup/text.mjs +3 -0
  39. package/lib/cli/renderers/db-snapshot-capture/text.mjs +3 -0
  40. package/lib/cli/renderers/destroy/text.mjs +3 -0
  41. package/lib/cli/{presentation/discovery-reporter.mjs → renderers/discover/report.mjs} +3 -3
  42. package/lib/cli/renderers/discover/text.mjs +7 -0
  43. package/lib/cli/renderers/doctor/text.mjs +7 -0
  44. package/lib/cli/{presentation/failure-presentation.mjs → renderers/run/failure.mjs} +6 -6
  45. package/lib/cli/renderers/run/interactive.mjs +119 -0
  46. package/lib/cli/{presentation/run-reporter.mjs → renderers/run/text-reporter.mjs} +5 -5
  47. package/lib/cli/renderers/status/text.mjs +7 -0
  48. package/lib/cli/renderers/typecheck/text.mjs +7 -0
  49. package/lib/cli/{tui/inspect-model.mjs → state/run/model.mjs} +11 -26
  50. package/lib/cli/{tui/inspect-state.mjs → state/run/state.mjs} +11 -18
  51. package/lib/cli/{tui → state/tree}/fuzzy-match.mjs +1 -1
  52. package/lib/cli/terminal/capabilities.mjs +33 -0
  53. package/lib/database/index.mjs +9 -21
  54. package/lib/{cli/viewer.mjs → results/artifacts.mjs} +1 -1
  55. package/lib/{cli/context-resources.mjs → results/context.mjs} +1 -1
  56. package/lib/runner/maintenance.mjs +25 -14
  57. package/lib/runner/readiness.mjs +5 -4
  58. package/lib/runner/state-io.mjs +10 -4
  59. package/node_modules/@elench/next-analysis/package.json +1 -1
  60. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  61. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  62. package/node_modules/@elench/ts-analysis/package.json +1 -1
  63. package/package.json +6 -7
  64. package/lib/cli/command-helpers.mjs +0 -191
  65. package/lib/cli/presentation/tree-reporter.mjs +0 -96
  66. package/lib/cli/tui/inspect-artifact-adapter.mjs +0 -3
  67. package/lib/cli/tui/inspect-live-adapter.mjs +0 -15
  68. /package/lib/cli/{agents → assistant}/providers/claude.mjs +0 -0
  69. /package/lib/cli/{agents → assistant}/providers/codex.mjs +0 -0
  70. /package/lib/cli/{agents → assistant}/providers/shared.mjs +0 -0
  71. /package/lib/cli/{presentation/events-reporter.mjs → renderers/run/events.mjs} +0 -0
  72. /package/lib/cli/{presentation → terminal}/colors.mjs +0 -0
  73. /package/lib/cli/{presentation/terminal-layout.mjs → terminal/layout.mjs} +0 -0
  74. /package/lib/{cli/presentation → results}/code-frames.mjs +0 -0
@@ -1,6 +1,6 @@
1
1
  import { loadConfigContext, resolveProductDir } from "../config/index.mjs";
2
2
  import { discoverTests } from "../discovery/index.mjs";
3
- import { loadCurrentRunArtifact } from "../cli/viewer.mjs";
3
+ import { loadCurrentRunArtifact } from "../results/artifacts.mjs";
4
4
 
5
5
  export async function loadBrowserBridgeContext(options = {}) {
6
6
  const productDir = resolveProductDir(process.cwd(), options.dir);
@@ -1,6 +1,7 @@
1
1
  import React, { createElement, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Box, Text, useApp, useBoxMetrics, useCursor, useInput, useStdout } from "ink";
3
- import { bold, cyan, dim, green, red, yellow } from "../presentation/colors.mjs";
3
+ import { bold, cyan, dim, green, red, yellow } from "../terminal/colors.mjs";
4
+ import { RunTreeView } from "../components/blocks/run-tree.mjs";
4
5
  import { getComposerDisplayModel } from "./composer.mjs";
5
6
  import { buildAssistantViewModel } from "./view-model.mjs";
6
7
 
@@ -49,6 +50,7 @@ export function AssistantApp({
49
50
  }),
50
51
  [snapshot, stdout?.columns]
51
52
  );
53
+ const runSession = assistantState.getLiveRunSession?.() || assistantState.getLastRunSession?.() || null;
52
54
 
53
55
  return createElement(
54
56
  Box,
@@ -63,6 +65,28 @@ export function AssistantApp({
63
65
  view.blocks.length === 0
64
66
  ? createElement(WelcomePanel, { view })
65
67
  : createElement(Transcript, { view }),
68
+ runSession
69
+ ? createElement(
70
+ Box,
71
+ { flexDirection: "column", marginTop: 1 },
72
+ createElement(Text, null, bold("Run Session")),
73
+ createElement(
74
+ Box,
75
+ {
76
+ borderStyle: "round",
77
+ flexDirection: "column",
78
+ paddingLeft: 1,
79
+ paddingRight: 1,
80
+ },
81
+ createElement(RunTreeView, {
82
+ runState: runSession.runState,
83
+ stdout,
84
+ productDir: runSession.productDir,
85
+ interactive: false,
86
+ })
87
+ )
88
+ )
89
+ : null,
66
90
  createElement(Text, null, ""),
67
91
  createElement(ComposerBar, { view, busy: snapshot.busy }),
68
92
  createElement(Text, null, dim(view.statusLine)),
@@ -1,4 +1,4 @@
1
- import { measureWidth } from "../presentation/terminal-layout.mjs";
1
+ import { measureWidth } from "../terminal/layout.mjs";
2
2
 
3
3
  const segmenter =
4
4
  typeof Intl !== "undefined" && typeof Intl.Segmenter === "function"
@@ -1,11 +1,11 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { fileURLToPath } from "url";
4
- import { readContextContent, buildContextSelection } from "../context-resources.mjs";
4
+ import { readContextContent, buildContextSelection } from "../../results/context.mjs";
5
5
 
6
6
  export function prepareAssistantContextPack({
7
7
  productDir,
8
- inspectState,
8
+ runState,
9
9
  } = {}) {
10
10
  const contextDir = path.join(productDir, ".testkit", "assistant");
11
11
  const binDir = path.join(contextDir, "bin");
@@ -23,7 +23,7 @@ export function prepareAssistantContextPack({
23
23
  const wrapperPath = path.join(binDir, "testkit");
24
24
 
25
25
  function refresh() {
26
- const snapshot = inspectState?.getSnapshot?.() || {};
26
+ const snapshot = runState?.getSnapshot?.() || {};
27
27
  const detailContent = readContextContent({ productDir, snapshot, mode: "detail", logTail: 12 });
28
28
  const logsContent = readContextContent({ productDir, snapshot, mode: "logs", logTail: 12 });
29
29
  const artifactsContent = readContextContent({ productDir, snapshot, mode: "artifacts", logTail: 12 });
@@ -160,7 +160,7 @@ function buildContextMarkdown(productDir, snapshot, paths) {
160
160
  lines.push(
161
161
  "",
162
162
  "## Guidance",
163
- "- Use shell commands like `npm run testkit`, `npx testkit`, or `testkit run <type> --dir .` when you need to execute tests.",
163
+ "- Use dedicated testkit tools for run/discover/status/doctor/typecheck actions before falling back to generic shell commands.",
164
164
  "- Do not reinterpret CLI syntax after an execution failure unless `testkit run --help` confirms a syntax problem.",
165
165
  "- Use the command log and focused context files before rereading artifacts manually.",
166
166
  "- Prefer repo-local commands over guessing project-specific wrappers.",
@@ -2,7 +2,7 @@ import React, { createElement } from "react";
2
2
  import { render } from "ink";
3
3
  import { createAssistantState } from "./state.mjs";
4
4
  import { AssistantApp } from "./app.mjs";
5
- import { loadLatestRunArtifact, resolveFileSubject } from "../viewer.mjs";
5
+ import { loadLatestRunArtifact, resolveFileSubject } from "../../results/artifacts.mjs";
6
6
 
7
7
  export async function runInteractiveAssistant({
8
8
  productDir,
@@ -1,4 +1,4 @@
1
- import { readContextContent } from "../context-resources.mjs";
1
+ import { readContextContent } from "../../results/context.mjs";
2
2
  import { buildAssistantResponseContract } from "./protocol.mjs";
3
3
 
4
4
  export function buildAssistantPrompt({
@@ -16,7 +16,7 @@ export function buildAssistantPrompt({
16
16
  "You are Testkit Assistant.",
17
17
  "You help users run tests, inspect failures, read logs and artifacts, and navigate the current local test state.",
18
18
  "All user natural-language requests must be handled through your own reasoning plus the available tools.",
19
- "Prefer real repository commands through shell_exec when the user asks to run tests or inspect the working repo.",
19
+ "Use the dedicated testkit tools for run/discover/status/doctor/typecheck actions; use shell_exec only for arbitrary repository commands outside those actions.",
20
20
  "Use read_context before repeating artifact/log inspection work, and use read_file/search_repo when you need codebase context.",
21
21
  buildAssistantResponseContract({ tools }),
22
22
  "",
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { startClaudeHostedSession } from "./providers/claude.mjs";
4
- import { startCodexHostedSession } from "./providers/codex.mjs";
3
+ import { startClaudeHostedSession } from "./claude.mjs";
4
+ import { startCodexHostedSession } from "./codex.mjs";
5
5
 
6
6
  const PROVIDERS = ["codex", "claude"];
7
7
 
@@ -55,7 +55,7 @@ export function isProviderInstalled(provider, env = process.env) {
55
55
  return false;
56
56
  }
57
57
 
58
- export function startAgentSession({
58
+ export function startProviderSession({
59
59
  provider = "auto",
60
60
  model = null,
61
61
  effort = null,
@@ -1,11 +1,11 @@
1
- import { startAgentSession, resolvePreferredProvider } from "../agents/index.mjs";
1
+ import { startProviderSession, resolvePreferredProvider } from "./providers/index.mjs";
2
2
  import { buildAssistantPrompt } from "./prompt-builder.mjs";
3
3
  import { listAssistantTools, executeAssistantTool } from "./tool-registry.mjs";
4
4
  import { parseAssistantEnvelope } from "./protocol.mjs";
5
5
 
6
6
  export async function runAssistantConversationTurn({
7
7
  productDir,
8
- inspectState,
8
+ runState,
9
9
  transcript,
10
10
  userMessage,
11
11
  provider = "auto",
@@ -21,7 +21,7 @@ export async function runAssistantConversationTurn({
21
21
  const tools = listAssistantTools();
22
22
  const toolContext = {
23
23
  productDir,
24
- inspectState,
24
+ runState,
25
25
  configs,
26
26
  env,
27
27
  commandLog,
@@ -32,7 +32,7 @@ export async function runAssistantConversationTurn({
32
32
  const emitted = [];
33
33
 
34
34
  for (let attempt = 0; attempt < 6; attempt += 1) {
35
- const snapshot = inspectState.getSnapshot();
35
+ const snapshot = runState.getSnapshot();
36
36
  const prompt = buildAssistantPrompt({
37
37
  productDir,
38
38
  snapshot,
@@ -52,7 +52,7 @@ export async function runAssistantConversationTurn({
52
52
  });
53
53
  onStatus?.(`Thinking with ${resolvedProvider}...`);
54
54
  const events = [];
55
- const session = startAgentSession({
55
+ const session = startProviderSession({
56
56
  provider: runtimeSettings.provider || provider,
57
57
  model: runtimeSettings.model || null,
58
58
  effort: runtimeSettings.effort || null,
@@ -1,6 +1,6 @@
1
- const RUN_TYPES = new Set(["int", "e2e", "scenario", "dal", "load", "pw", "all"]);
2
1
  import { ASSISTANT_EFFORTS, ASSISTANT_PROVIDERS } from "./settings.mjs";
3
2
 
3
+ const RUN_TYPES = new Set(["int", "e2e", "scenario", "dal", "load", "pw", "all"]);
4
4
  const PROVIDERS = new Set(ASSISTANT_PROVIDERS);
5
5
  const EFFORTS = new Set(ASSISTANT_EFFORTS);
6
6
 
@@ -62,6 +62,17 @@ export function parseSlashCommand(input) {
62
62
  throw new Error('/settings expects "show" or "reset"');
63
63
  }
64
64
 
65
+ if (command === "config") {
66
+ const action = tokens[0] || "show";
67
+ if (action === "show") return { type: "config-show" };
68
+ if (action === "reset") return { type: "config-reset" };
69
+ if (action === "auto-collapse-passed" || action === "autoCollapsePassedTreeBranches") {
70
+ const value = parseBooleanToken(tokens[1], "/config auto-collapse-passed");
71
+ return { type: "config-set-auto-collapse", value };
72
+ }
73
+ throw new Error('/config expects "show", "reset", or "auto-collapse-passed <on|off>"');
74
+ }
75
+
65
76
  if (command === "file" || command === "focus") {
66
77
  if (!tokens[0]) throw new Error(`/${command} expects a file path`);
67
78
  return { type: "file", file: tokens.join(" ") };
@@ -134,11 +145,21 @@ export function formatSlashHelpLines() {
134
145
  "/provider-arg clear",
135
146
  "/settings",
136
147
  "/settings reset",
148
+ "/config",
149
+ "/config auto-collapse-passed <on|off>",
150
+ "/config reset",
137
151
  "/clear",
138
152
  "/quit",
139
153
  ];
140
154
  }
141
155
 
156
+ function parseBooleanToken(value, commandName) {
157
+ const normalized = String(value || "").trim().toLowerCase();
158
+ if (["on", "true", "yes", "1"].includes(normalized)) return true;
159
+ if (["off", "false", "no", "0"].includes(normalized)) return false;
160
+ throw new Error(`${commandName} expects on or off`);
161
+ }
162
+
142
163
  function parseRunCommandTokens(tokens) {
143
164
  const options = {
144
165
  type: [],
@@ -1,7 +1,14 @@
1
- import { loadCurrentRunArtifact, loadLatestRunArtifact } from "../viewer.mjs";
2
- import { createInspectState } from "../tui/inspect-state.mjs";
3
- import { buildContextSelection } from "../context-resources.mjs";
4
- import { isProviderInstalled } from "../agents/index.mjs";
1
+ import { loadCurrentRunArtifact, loadLatestRunArtifact } from "../../results/artifacts.mjs";
2
+ import {
3
+ formatCliConfig,
4
+ loadCliConfig,
5
+ mergeCliConfig,
6
+ resetCliConfig,
7
+ saveCliConfig,
8
+ } from "../config.mjs";
9
+ import { createRunState } from "../state/run/state.mjs";
10
+ import { buildContextSelection } from "../../results/context.mjs";
11
+ import { isProviderInstalled } from "./providers/index.mjs";
5
12
  import { parseSlashCommand, formatSlashHelpLines } from "./slash-commands.mjs";
6
13
  import { executeAssistantTool } from "./tool-registry.mjs";
7
14
  import { runAssistantConversationTurn } from "./session.mjs";
@@ -41,10 +48,12 @@ export function createAssistantState({
41
48
  configs = [],
42
49
  env = process.env,
43
50
  } = {}) {
44
- const inspectState = createInspectState({ dataSource });
51
+ const runState = createRunState({ dataSource });
52
+ let cliConfig = loadCliConfig(productDir);
53
+ runState.setAutoCollapsePassedTreeBranches(cliConfig.autoCollapsePassedTreeBranches);
45
54
  const commandLog = prepareAssistantContextPack({
46
55
  productDir,
47
- inspectState,
56
+ runState,
48
57
  });
49
58
 
50
59
  const listeners = new Set();
@@ -76,8 +85,11 @@ export function createAssistantState({
76
85
  model: settings.model,
77
86
  prompt: "",
78
87
  });
88
+ let liveRunSession = null;
89
+ let lastRunSession = null;
90
+ let liveRunSessionUnsubscribe = null;
79
91
 
80
- inspectState.subscribe(() => {
92
+ runState.subscribe(() => {
81
93
  commandLog.refresh();
82
94
  notify();
83
95
  });
@@ -104,13 +116,46 @@ export function createAssistantState({
104
116
  commandLog.refresh();
105
117
  }
106
118
 
119
+ function attachRunSession(session, { active = true } = {}) {
120
+ if (liveRunSessionUnsubscribe) {
121
+ liveRunSessionUnsubscribe();
122
+ liveRunSessionUnsubscribe = null;
123
+ }
124
+ if (!session) {
125
+ if (!active) notify();
126
+ return;
127
+ }
128
+ lastRunSession = session;
129
+ if (active) {
130
+ liveRunSession = session;
131
+ liveRunSessionUnsubscribe = session.runState.subscribe(() => {
132
+ notify();
133
+ });
134
+ }
135
+ notify();
136
+ }
137
+
138
+ function completeRunSession(session) {
139
+ if (liveRunSession === session) {
140
+ if (liveRunSessionUnsubscribe) {
141
+ liveRunSessionUnsubscribe();
142
+ liveRunSessionUnsubscribe = null;
143
+ }
144
+ liveRunSession = null;
145
+ }
146
+ if (session) lastRunSession = session;
147
+ notify();
148
+ }
149
+
107
150
  const state = {
108
- inspectState,
151
+ runState,
109
152
  commandLog,
153
+ attachRunSession,
154
+ completeRunSession,
110
155
 
111
156
  async loadLatestArtifact() {
112
157
  try {
113
- inspectState.hydrateFromArtifact(loadLatestRunArtifact(productDir));
158
+ runState.hydrateFromArtifact(loadLatestRunArtifact(productDir));
114
159
  } catch {
115
160
  // No artifact yet.
116
161
  }
@@ -119,7 +164,7 @@ export function createAssistantState({
119
164
 
120
165
  async loadCurrentArtifact() {
121
166
  try {
122
- inspectState.hydrateFromArtifact(loadCurrentRunArtifact(productDir));
167
+ runState.hydrateFromArtifact(loadCurrentRunArtifact(productDir));
123
168
  } catch {
124
169
  // No artifact yet.
125
170
  }
@@ -127,13 +172,13 @@ export function createAssistantState({
127
172
  },
128
173
 
129
174
  revealFile(serviceName, filePath) {
130
- const revealed = inspectState.revealFile(serviceName, filePath);
175
+ const revealed = runState.revealFile(serviceName, filePath);
131
176
  refreshContextPack();
132
177
  return revealed;
133
178
  },
134
179
 
135
180
  revealService(serviceName) {
136
- const revealed = inspectState.revealService(serviceName);
181
+ const revealed = runState.revealService(serviceName);
137
182
  refreshContextPack();
138
183
  return revealed;
139
184
  },
@@ -228,6 +273,19 @@ export function createAssistantState({
228
273
  notify();
229
274
  },
230
275
 
276
+ setCliConfig(nextConfig) {
277
+ cliConfig = mergeCliConfig(cliConfig, nextConfig);
278
+ saveCliConfig(productDir, cliConfig);
279
+ runState.setAutoCollapsePassedTreeBranches(cliConfig.autoCollapsePassedTreeBranches);
280
+ notify();
281
+ },
282
+
283
+ resetCliConfig() {
284
+ cliConfig = resetCliConfig(productDir);
285
+ runState.setAutoCollapsePassedTreeBranches(cliConfig.autoCollapsePassedTreeBranches);
286
+ notify();
287
+ },
288
+
231
289
  resetSettings() {
232
290
  settings = resetAssistantSettings(productDir);
233
291
  resolvedProviderName = null;
@@ -239,6 +297,14 @@ export function createAssistantState({
239
297
  notify();
240
298
  },
241
299
 
300
+ getLiveRunSession() {
301
+ return liveRunSession;
302
+ },
303
+
304
+ getLastRunSession() {
305
+ return lastRunSession;
306
+ },
307
+
242
308
  async submitCurrentComposer() {
243
309
  const value = composerState.text.trim();
244
310
  composerState = createComposerState();
@@ -263,6 +329,7 @@ export function createAssistantState({
263
329
  }
264
330
  if (slash) {
265
331
  try {
332
+ setBusy(true, `Running ${slash.type}...`);
266
333
  await executeSlashCommand({
267
334
  slash,
268
335
  state,
@@ -277,6 +344,8 @@ export function createAssistantState({
277
344
  role: "system",
278
345
  text: error instanceof Error ? error.message : String(error),
279
346
  });
347
+ } finally {
348
+ setBusy(false, null);
280
349
  }
281
350
  refreshContextPack();
282
351
  notify();
@@ -286,6 +355,7 @@ export function createAssistantState({
286
355
  const routedSlash = routeLocalIntent(trimmed);
287
356
  if (routedSlash) {
288
357
  try {
358
+ setBusy(true, `Running ${routedSlash.type}...`);
289
359
  await executeSlashCommand({
290
360
  slash: routedSlash,
291
361
  state,
@@ -300,6 +370,8 @@ export function createAssistantState({
300
370
  role: "system",
301
371
  text: error instanceof Error ? error.message : String(error),
302
372
  });
373
+ } finally {
374
+ setBusy(false, null);
303
375
  }
304
376
  refreshContextPack();
305
377
  notify();
@@ -310,7 +382,7 @@ export function createAssistantState({
310
382
  setBusy(true, `Thinking with ${settings.provider === "auto" ? "provider" : settings.provider}...`);
311
383
  const emitted = await runAssistantConversationTurn({
312
384
  productDir,
313
- inspectState,
385
+ runState,
314
386
  transcript: messages.map((entry) => ({ role: entry.role, text: entry.text })),
315
387
  userMessage: trimmed,
316
388
  settings,
@@ -334,7 +406,7 @@ export function createAssistantState({
334
406
  notify();
335
407
  },
336
408
  onToolEvent(event) {
337
- handleAssistantToolEvent(event, appendMessage);
409
+ handleAssistantToolEvent(state, event, appendMessage);
338
410
  },
339
411
  });
340
412
  for (const message of emitted) appendMessage(message);
@@ -356,8 +428,8 @@ export function createAssistantState({
356
428
 
357
429
  getSnapshot() {
358
430
  return {
359
- context: buildContextSelection(inspectState.getSnapshot()),
360
- inspect: inspectState.getSnapshot(),
431
+ context: buildContextSelection(runState.getSnapshot()),
432
+ run: runState.getSnapshot(),
361
433
  productDir,
362
434
  messages: [...messages],
363
435
  composer: composerState.text,
@@ -369,8 +441,11 @@ export function createAssistantState({
369
441
  model: settings.model,
370
442
  effort: settings.effort,
371
443
  providerArgs: [...settings.providerArgs],
444
+ cliConfig,
372
445
  activeStatus,
373
446
  contextUsage,
447
+ liveRunSession: serializeRunSession(liveRunSession),
448
+ lastRunSession: serializeRunSession(lastRunSession),
374
449
  contextPaths: {
375
450
  contextPath: commandLog.contextPath,
376
451
  summaryPath: commandLog.summaryPath,
@@ -460,28 +535,32 @@ async function executeSlashCommand({
460
535
  appendMessage({ role: "assistant", text: "Assistant settings reset." });
461
536
  return;
462
537
  }
538
+ if (slash.type === "config-show") {
539
+ appendMessage({ role: "assistant", text: formatCliConfig(state.getSnapshot().cliConfig) });
540
+ return;
541
+ }
542
+ if (slash.type === "config-reset") {
543
+ state.resetCliConfig();
544
+ appendMessage({ role: "assistant", text: "CLI config reset." });
545
+ return;
546
+ }
547
+ if (slash.type === "config-set-auto-collapse") {
548
+ state.setCliConfig({ autoCollapsePassedTreeBranches: slash.value });
549
+ appendMessage({
550
+ role: "assistant",
551
+ text: `autoCollapsePassedTreeBranches set to ${slash.value}.`,
552
+ });
553
+ return;
554
+ }
463
555
 
464
556
  const result = await executeSlashTool(slash, {
465
557
  productDir,
466
- inspectState: state.inspectState,
558
+ runState: state.runState,
467
559
  configs,
468
560
  env,
469
561
  commandLog: state.commandLog,
470
562
  onEvent(event) {
471
- if (event.type === "tool-start") {
472
- appendMessage({
473
- role: "tool",
474
- status: "running",
475
- title: event.title || event.tool || "Tool",
476
- text: event.message,
477
- data: {
478
- command: event.command || null,
479
- testkitRelated: Boolean(event.testkitRelated),
480
- },
481
- });
482
- } else if (event.type === "tool-status") {
483
- state.setNotice(event.message);
484
- }
563
+ handleAssistantToolEvent(state, event, appendMessage);
485
564
  },
486
565
  provider: settings.provider,
487
566
  });
@@ -494,18 +573,32 @@ async function executeSlashCommand({
494
573
  });
495
574
  }
496
575
 
497
- function handleAssistantToolEvent(event, appendMessage) {
498
- if (!event || event.type !== "tool-start") return;
499
- appendMessage({
500
- role: "tool",
501
- status: "running",
502
- title: event.title || event.tool || "Tool",
503
- text: event.message || "Running tool",
504
- data: {
505
- command: event.command || null,
506
- testkitRelated: Boolean(event.testkitRelated),
507
- },
508
- });
576
+ function handleAssistantToolEvent(state, event, appendMessage) {
577
+ if (!event) return;
578
+ if (event.type === "tool-start") {
579
+ appendMessage({
580
+ role: "tool",
581
+ status: "running",
582
+ title: event.title || event.tool || "Tool",
583
+ text: event.message || "Running tool",
584
+ data: {
585
+ command: event.command || null,
586
+ testkitRelated: Boolean(event.testkitRelated),
587
+ },
588
+ });
589
+ return;
590
+ }
591
+ if (event.type === "tool-status") {
592
+ state.setNotice(event.message);
593
+ return;
594
+ }
595
+ if (event.type === "run-session-start") {
596
+ state.attachRunSession(event.session, { active: true });
597
+ return;
598
+ }
599
+ if (event.type === "run-session-end") {
600
+ state.completeRunSession(event.session);
601
+ }
509
602
  }
510
603
 
511
604
  function formatSettings(snapshot) {
@@ -538,46 +631,18 @@ async function executeSlashTool(slash, context) {
538
631
  case "service":
539
632
  return executeAssistantTool("read_context", { service: slash.service, mode: "detail" }, context);
540
633
  case "status":
541
- return executeAssistantTool("shell_exec", { command: "testkit status --dir ." }, context);
634
+ return executeAssistantTool("show_status", {}, context);
542
635
  case "discover":
543
- return executeAssistantTool("shell_exec", { command: "testkit discover --dir ." }, context);
636
+ return executeAssistantTool("discover_tests", {}, context);
544
637
  case "doctor":
545
- return executeAssistantTool("shell_exec", { command: "testkit doctor --dir ." }, context);
638
+ return executeAssistantTool("run_doctor", {}, context);
546
639
  case "run":
547
- return executeAssistantTool("shell_exec", { command: buildRunSlashCommand(slash.options) }, context);
640
+ return executeAssistantTool("run_tests", slash.options, context);
548
641
  default:
549
642
  throw new Error(`Unsupported slash command "${slash.type}"`);
550
643
  }
551
644
  }
552
645
 
553
- function buildRunSlashCommand(options = {}) {
554
- const types = options.type || [];
555
- const parts = ["testkit", "run"];
556
- if (types.length === 1) {
557
- parts.push(types[0]);
558
- }
559
- parts.push("--dir", ".");
560
- if (types.length !== 1) {
561
- for (const type of types) {
562
- parts.push("--type", type);
563
- }
564
- }
565
- for (const suite of options.suite || []) {
566
- parts.push("--suite", suite);
567
- }
568
- for (const file of options.file || []) {
569
- parts.push("--file", file);
570
- }
571
- if (options.service) parts.push("--service", options.service);
572
- return parts.map(shellEscapeArg).join(" ");
573
- }
574
-
575
- function shellEscapeArg(value) {
576
- const stringValue = String(value);
577
- if (/^[a-zA-Z0-9._:/-]+$/.test(stringValue)) return stringValue;
578
- return `'${stringValue.replace(/'/g, `'\\''`)}'`;
579
- }
580
-
581
646
  function parseSlashCommandSafe(input) {
582
647
  try {
583
648
  return parseSlashCommand(input);
@@ -619,3 +684,11 @@ function routeLocalIntent(input) {
619
684
  if (/^list\s+test\s+files$/.test(normalized)) return { type: "discover" };
620
685
  return null;
621
686
  }
687
+
688
+ function serializeRunSession(session) {
689
+ if (!session) return null;
690
+ return {
691
+ productDir: session.productDir,
692
+ snapshot: session.getSnapshot(),
693
+ };
694
+ }