@elench/testkit 0.1.96 → 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 (79) hide show
  1. package/lib/app/browser-bridge.mjs +1 -1
  2. package/lib/cli/assistant/app.mjs +49 -12
  3. package/lib/cli/assistant/composer.mjs +19 -1
  4. package/lib/cli/assistant/context-pack.mjs +9 -8
  5. package/lib/cli/assistant/interactive.mjs +1 -1
  6. package/lib/cli/assistant/model-discovery.mjs +243 -0
  7. package/lib/cli/assistant/prompt-builder.mjs +2 -5
  8. package/lib/cli/{agents → assistant}/providers/claude.mjs +41 -3
  9. package/lib/cli/{agents → assistant}/providers/codex.mjs +33 -14
  10. package/lib/cli/{agents → assistant/providers}/index.mjs +3 -3
  11. package/lib/cli/{agents → assistant}/providers/shared.mjs +6 -2
  12. package/lib/cli/assistant/session.mjs +31 -6
  13. package/lib/cli/assistant/slash-commands.mjs +30 -3
  14. package/lib/cli/assistant/state.mjs +237 -71
  15. package/lib/cli/assistant/tool-registry.mjs +325 -39
  16. package/lib/cli/assistant/view-model.mjs +1 -1
  17. package/lib/cli/commands/assistant.mjs +4 -3
  18. package/lib/cli/commands/browser/serve.mjs +5 -23
  19. package/lib/cli/commands/cleanup.mjs +8 -2
  20. package/lib/cli/commands/db/snapshot/capture.mjs +8 -4
  21. package/lib/cli/commands/destroy.mjs +8 -2
  22. package/lib/cli/commands/discover.mjs +5 -27
  23. package/lib/cli/commands/doctor.mjs +5 -5
  24. package/lib/cli/commands/flags.mjs +61 -0
  25. package/lib/cli/commands/run.mjs +10 -2
  26. package/lib/cli/commands/status.mjs +10 -2
  27. package/lib/cli/commands/typecheck.mjs +5 -5
  28. package/lib/cli/{tui/inspect-app.mjs → components/blocks/run-tree.mjs} +29 -54
  29. package/lib/cli/{tui → components/primitives}/filter-bar.mjs +1 -1
  30. package/lib/cli/{presentation → components/primitives}/summary-box.mjs +1 -1
  31. package/lib/cli/config.mjs +63 -0
  32. package/lib/cli/operations/browser/serve/operation.mjs +23 -0
  33. package/lib/cli/operations/cleanup/operation.mjs +8 -0
  34. package/lib/cli/{db.mjs → operations/db/snapshot/capture/operation.mjs} +15 -9
  35. package/lib/cli/operations/destroy/operation.mjs +12 -0
  36. package/lib/cli/operations/discover/operation.mjs +32 -0
  37. package/lib/cli/operations/doctor/operation.mjs +5 -0
  38. package/lib/cli/operations/run/operation.mjs +129 -0
  39. package/lib/cli/operations/status/operation.mjs +7 -0
  40. package/lib/cli/operations/typecheck/operation.mjs +5 -0
  41. package/lib/cli/renderers/browser-serve/text.mjs +6 -0
  42. package/lib/cli/renderers/cleanup/text.mjs +3 -0
  43. package/lib/cli/renderers/db-snapshot-capture/text.mjs +3 -0
  44. package/lib/cli/renderers/destroy/text.mjs +3 -0
  45. package/lib/cli/{presentation/discovery-reporter.mjs → renderers/discover/report.mjs} +3 -3
  46. package/lib/cli/renderers/discover/text.mjs +7 -0
  47. package/lib/cli/renderers/doctor/text.mjs +7 -0
  48. package/lib/cli/{presentation/failure-presentation.mjs → renderers/run/failure.mjs} +6 -6
  49. package/lib/cli/renderers/run/interactive.mjs +119 -0
  50. package/lib/cli/{presentation/run-reporter.mjs → renderers/run/text-reporter.mjs} +5 -5
  51. package/lib/cli/renderers/status/text.mjs +7 -0
  52. package/lib/cli/renderers/typecheck/text.mjs +7 -0
  53. package/lib/cli/{tui/inspect-model.mjs → state/run/model.mjs} +11 -26
  54. package/lib/cli/{tui/inspect-state.mjs → state/run/state.mjs} +11 -18
  55. package/lib/cli/{tui → state/tree}/fuzzy-match.mjs +1 -1
  56. package/lib/cli/terminal/capabilities.mjs +33 -0
  57. package/lib/database/index.mjs +9 -21
  58. package/lib/database/template-steps.mjs +3 -3
  59. package/lib/{cli/viewer.mjs → results/artifacts.mjs} +1 -1
  60. package/lib/{cli/context-resources.mjs → results/context.mjs} +1 -1
  61. package/lib/runner/maintenance.mjs +25 -14
  62. package/lib/runner/readiness.mjs +5 -4
  63. package/lib/runner/runtime-preparation.mjs +36 -0
  64. package/lib/runner/state-io.mjs +10 -4
  65. package/lib/runner/template.mjs +24 -3
  66. package/node_modules/@elench/next-analysis/package.json +1 -1
  67. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  68. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  69. package/node_modules/@elench/ts-analysis/package.json +1 -1
  70. package/package.json +5 -5
  71. package/lib/cli/assistant/command-plan.mjs +0 -227
  72. package/lib/cli/command-helpers.mjs +0 -191
  73. package/lib/cli/presentation/tree-reporter.mjs +0 -96
  74. package/lib/cli/tui/inspect-artifact-adapter.mjs +0 -3
  75. package/lib/cli/tui/inspect-live-adapter.mjs +0 -15
  76. /package/lib/cli/{presentation/events-reporter.mjs → renderers/run/events.mjs} +0 -0
  77. /package/lib/cli/{presentation → terminal}/colors.mjs +0 -0
  78. /package/lib/cli/{presentation/terminal-layout.mjs → terminal/layout.mjs} +0 -0
  79. /package/lib/{cli/presentation → results}/code-frames.mjs +0 -0
@@ -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,
@@ -74,7 +74,24 @@ export async function runAssistantConversationTurn({
74
74
  emitted.push({ role: "assistant", text: envelope.commentary });
75
75
  currentTranscript.push({ role: "assistant", text: envelope.commentary });
76
76
  }
77
- const toolResult = await executeAssistantTool(envelope.tool, envelope.arguments, toolContext);
77
+ let toolResult;
78
+ try {
79
+ toolResult = await executeAssistantTool(envelope.tool, envelope.arguments, toolContext);
80
+ } catch (error) {
81
+ const toolText = formatToolError(envelope.tool, error);
82
+ emitted.push({
83
+ role: "tool",
84
+ text: toolText,
85
+ toolName: envelope.tool,
86
+ title: `${envelope.tool} error`,
87
+ data: { ok: false, error: toolText },
88
+ });
89
+ currentTranscript.push({
90
+ role: "tool",
91
+ text: `${envelope.tool}: ${toolText}`,
92
+ });
93
+ continue;
94
+ }
78
95
  const toolText = toolResult.text || `${envelope.tool} completed`;
79
96
  emitted.push({
80
97
  role: "tool",
@@ -105,6 +122,14 @@ export async function runAssistantConversationTurn({
105
122
  return emitted;
106
123
  }
107
124
 
125
+ export function formatToolError(tool, error) {
126
+ const message = error instanceof Error ? error.message : String(error);
127
+ if (tool === "shell_exec" && /command string/.test(message)) {
128
+ return "The assistant requested shell_exec without a command. Retry with arguments.command set to the exact shell command.";
129
+ }
130
+ return `Tool failed: ${message}`;
131
+ }
132
+
108
133
  function formatProviderEvent(event) {
109
134
  if (event.type === "tool") {
110
135
  return `${event.provider}: ${event.name}${event.detail ? ` (${event.detail})` : ""}`;
@@ -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
 
@@ -23,8 +23,14 @@ export function parseSlashCommand(input) {
23
23
  }
24
24
 
25
25
  if (command === "model") {
26
+ if (tokens.length === 0) return { type: "model-list" };
27
+ if (tokens[0] === "list") return { type: "model-list" };
28
+ if (tokens[0] === "custom") {
29
+ const customModel = tokens.slice(1).join(" ").trim();
30
+ if (!customModel) throw new Error("/model custom expects a model name");
31
+ return { type: "model", model: customModel, custom: true };
32
+ }
26
33
  const model = tokens.join(" ").trim();
27
- if (!model) throw new Error("/model expects a model name or default");
28
34
  return { type: "model", model: model === "default" ? null : model };
29
35
  }
30
36
 
@@ -56,6 +62,17 @@ export function parseSlashCommand(input) {
56
62
  throw new Error('/settings expects "show" or "reset"');
57
63
  }
58
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
+
59
76
  if (command === "file" || command === "focus") {
60
77
  if (!tokens[0]) throw new Error(`/${command} expects a file path`);
61
78
  return { type: "file", file: tokens.join(" ") };
@@ -121,18 +138,28 @@ export function formatSlashHelpLines() {
121
138
  "/status",
122
139
  "/doctor",
123
140
  "/provider <auto|claude|codex>",
124
- "/model <model|default>",
141
+ "/model [list|default|custom <model>|model]",
125
142
  "/effort <low|medium|high|xhigh|max|default>",
126
143
  "/provider-arg add <arg>",
127
144
  "/provider-arg list",
128
145
  "/provider-arg clear",
129
146
  "/settings",
130
147
  "/settings reset",
148
+ "/config",
149
+ "/config auto-collapse-passed <on|off>",
150
+ "/config reset",
131
151
  "/clear",
132
152
  "/quit",
133
153
  ];
134
154
  }
135
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
+
136
163
  function parseRunCommandTokens(tokens) {
137
164
  const options = {
138
165
  type: [],
@@ -1,11 +1,23 @@
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";
8
15
  import { prepareAssistantContextPack } from "./context-pack.mjs";
16
+ import {
17
+ discoverAssistantModels,
18
+ formatModelChoices,
19
+ getModelProviderMismatch,
20
+ } from "./model-discovery.mjs";
9
21
  import {
10
22
  DEFAULT_ASSISTANT_SETTINGS,
11
23
  loadAssistantSettings,
@@ -36,10 +48,12 @@ export function createAssistantState({
36
48
  configs = [],
37
49
  env = process.env,
38
50
  } = {}) {
39
- const inspectState = createInspectState({ dataSource });
51
+ const runState = createRunState({ dataSource });
52
+ let cliConfig = loadCliConfig(productDir);
53
+ runState.setAutoCollapsePassedTreeBranches(cliConfig.autoCollapsePassedTreeBranches);
40
54
  const commandLog = prepareAssistantContextPack({
41
55
  productDir,
42
- inspectState,
56
+ runState,
43
57
  });
44
58
 
45
59
  const listeners = new Set();
@@ -57,14 +71,25 @@ export function createAssistantState({
57
71
  }
58
72
  );
59
73
  let resolvedProviderName = resolveInitialProvider(settings.provider, env);
74
+ const sanitizedStartup = sanitizeSettingsForResolvedProvider({
75
+ productDir,
76
+ settings,
77
+ resolvedProvider: resolvedProviderName,
78
+ });
79
+ settings = sanitizedStartup.settings;
80
+ if (sanitizedStartup.notice) notice = sanitizedStartup.notice;
60
81
  let activeStatus = null;
82
+ let startupNoticeEmitted = false;
61
83
  let contextUsage = buildContextUsage({
62
84
  provider: resolvedProviderName || settings.provider,
63
85
  model: settings.model,
64
86
  prompt: "",
65
87
  });
88
+ let liveRunSession = null;
89
+ let lastRunSession = null;
90
+ let liveRunSessionUnsubscribe = null;
66
91
 
67
- inspectState.subscribe(() => {
92
+ runState.subscribe(() => {
68
93
  commandLog.refresh();
69
94
  notify();
70
95
  });
@@ -91,13 +116,46 @@ export function createAssistantState({
91
116
  commandLog.refresh();
92
117
  }
93
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
+
94
150
  const state = {
95
- inspectState,
151
+ runState,
96
152
  commandLog,
153
+ attachRunSession,
154
+ completeRunSession,
97
155
 
98
156
  async loadLatestArtifact() {
99
157
  try {
100
- inspectState.hydrateFromArtifact(loadLatestRunArtifact(productDir));
158
+ runState.hydrateFromArtifact(loadLatestRunArtifact(productDir));
101
159
  } catch {
102
160
  // No artifact yet.
103
161
  }
@@ -106,7 +164,7 @@ export function createAssistantState({
106
164
 
107
165
  async loadCurrentArtifact() {
108
166
  try {
109
- inspectState.hydrateFromArtifact(loadCurrentRunArtifact(productDir));
167
+ runState.hydrateFromArtifact(loadCurrentRunArtifact(productDir));
110
168
  } catch {
111
169
  // No artifact yet.
112
170
  }
@@ -114,13 +172,13 @@ export function createAssistantState({
114
172
  },
115
173
 
116
174
  revealFile(serviceName, filePath) {
117
- const revealed = inspectState.revealFile(serviceName, filePath);
175
+ const revealed = runState.revealFile(serviceName, filePath);
118
176
  refreshContextPack();
119
177
  return revealed;
120
178
  },
121
179
 
122
180
  revealService(serviceName) {
123
- const revealed = inspectState.revealService(serviceName);
181
+ const revealed = runState.revealService(serviceName);
124
182
  refreshContextPack();
125
183
  return revealed;
126
184
  },
@@ -176,12 +234,20 @@ export function createAssistantState({
176
234
 
177
235
  setProvider(nextProvider) {
178
236
  settings = mergeAssistantSettings(settings, { provider: nextProvider || DEFAULT_ASSISTANT_SETTINGS.provider });
179
- resolvedProviderName = null;
237
+ resolvedProviderName = resolveInitialProvider(settings.provider, env);
238
+ if (settings.model && getModelProviderMismatch(resolvedProviderName, settings.model)) {
239
+ settings = mergeAssistantSettings(settings, { model: null });
240
+ }
180
241
  saveAssistantSettings(productDir, settings);
181
242
  notify();
182
243
  },
183
244
 
184
- setModel(nextModel) {
245
+ setModel(nextModel, { custom = false } = {}) {
246
+ const resolvedProvider = resolveInitialProvider(settings.provider, env);
247
+ const mismatch = getModelProviderMismatch(resolvedProvider, nextModel);
248
+ if (mismatch && !custom) {
249
+ throw new Error(mismatch);
250
+ }
185
251
  settings = mergeAssistantSettings(settings, { model: nextModel || null });
186
252
  saveAssistantSettings(productDir, settings);
187
253
  notify();
@@ -207,6 +273,19 @@ export function createAssistantState({
207
273
  notify();
208
274
  },
209
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
+
210
289
  resetSettings() {
211
290
  settings = resetAssistantSettings(productDir);
212
291
  resolvedProviderName = null;
@@ -218,6 +297,14 @@ export function createAssistantState({
218
297
  notify();
219
298
  },
220
299
 
300
+ getLiveRunSession() {
301
+ return liveRunSession;
302
+ },
303
+
304
+ getLastRunSession() {
305
+ return lastRunSession;
306
+ },
307
+
221
308
  async submitCurrentComposer() {
222
309
  const value = composerState.text.trim();
223
310
  composerState = createComposerState();
@@ -229,6 +316,10 @@ export function createAssistantState({
229
316
  async submitInput(input) {
230
317
  const trimmed = String(input || "").trim();
231
318
  if (!trimmed) return;
319
+ if (notice && !startupNoticeEmitted) {
320
+ startupNoticeEmitted = true;
321
+ appendMessage({ role: "system", text: notice });
322
+ }
232
323
  appendMessage({ role: "user", text: trimmed });
233
324
 
234
325
  const slash = parseSlashCommandSafe(trimmed);
@@ -238,6 +329,7 @@ export function createAssistantState({
238
329
  }
239
330
  if (slash) {
240
331
  try {
332
+ setBusy(true, `Running ${slash.type}...`);
241
333
  await executeSlashCommand({
242
334
  slash,
243
335
  state,
@@ -252,6 +344,34 @@ export function createAssistantState({
252
344
  role: "system",
253
345
  text: error instanceof Error ? error.message : String(error),
254
346
  });
347
+ } finally {
348
+ setBusy(false, null);
349
+ }
350
+ refreshContextPack();
351
+ notify();
352
+ return;
353
+ }
354
+
355
+ const routedSlash = routeLocalIntent(trimmed);
356
+ if (routedSlash) {
357
+ try {
358
+ setBusy(true, `Running ${routedSlash.type}...`);
359
+ await executeSlashCommand({
360
+ slash: routedSlash,
361
+ state,
362
+ productDir,
363
+ settings,
364
+ configs,
365
+ env,
366
+ appendMessage,
367
+ });
368
+ } catch (error) {
369
+ appendMessage({
370
+ role: "system",
371
+ text: error instanceof Error ? error.message : String(error),
372
+ });
373
+ } finally {
374
+ setBusy(false, null);
255
375
  }
256
376
  refreshContextPack();
257
377
  notify();
@@ -262,7 +382,7 @@ export function createAssistantState({
262
382
  setBusy(true, `Thinking with ${settings.provider === "auto" ? "provider" : settings.provider}...`);
263
383
  const emitted = await runAssistantConversationTurn({
264
384
  productDir,
265
- inspectState,
385
+ runState,
266
386
  transcript: messages.map((entry) => ({ role: entry.role, text: entry.text })),
267
387
  userMessage: trimmed,
268
388
  settings,
@@ -286,7 +406,7 @@ export function createAssistantState({
286
406
  notify();
287
407
  },
288
408
  onToolEvent(event) {
289
- handleAssistantToolEvent(event, appendMessage);
409
+ handleAssistantToolEvent(state, event, appendMessage);
290
410
  },
291
411
  });
292
412
  for (const message of emitted) appendMessage(message);
@@ -308,8 +428,8 @@ export function createAssistantState({
308
428
 
309
429
  getSnapshot() {
310
430
  return {
311
- context: buildContextSelection(inspectState.getSnapshot()),
312
- inspect: inspectState.getSnapshot(),
431
+ context: buildContextSelection(runState.getSnapshot()),
432
+ run: runState.getSnapshot(),
313
433
  productDir,
314
434
  messages: [...messages],
315
435
  composer: composerState.text,
@@ -321,8 +441,11 @@ export function createAssistantState({
321
441
  model: settings.model,
322
442
  effort: settings.effort,
323
443
  providerArgs: [...settings.providerArgs],
444
+ cliConfig,
324
445
  activeStatus,
325
446
  contextUsage,
447
+ liveRunSession: serializeRunSession(liveRunSession),
448
+ lastRunSession: serializeRunSession(lastRunSession),
326
449
  contextPaths: {
327
450
  contextPath: commandLog.contextPath,
328
451
  summaryPath: commandLog.summaryPath,
@@ -372,10 +495,17 @@ async function executeSlashCommand({
372
495
  return;
373
496
  }
374
497
  if (slash.type === "model") {
375
- state.setModel(slash.model);
498
+ state.setModel(slash.model, { custom: slash.custom });
376
499
  appendMessage({ role: "assistant", text: slash.model ? `Model set to ${slash.model}.` : "Model reset to provider default." });
377
500
  return;
378
501
  }
502
+ if (slash.type === "model-list") {
503
+ const snapshot = state.getSnapshot();
504
+ const provider = snapshot.resolvedProvider || resolveInitialProvider(snapshot.provider, env) || snapshot.provider;
505
+ const discovery = await discoverAssistantModels({ provider, productDir, env });
506
+ appendMessage({ role: "assistant", text: formatModelChoices(discovery, { currentModel: snapshot.model }) });
507
+ return;
508
+ }
379
509
  if (slash.type === "effort") {
380
510
  state.setEffort(slash.effort);
381
511
  appendMessage({ role: "assistant", text: slash.effort ? `Effort set to ${slash.effort}.` : "Effort reset to provider default." });
@@ -405,28 +535,32 @@ async function executeSlashCommand({
405
535
  appendMessage({ role: "assistant", text: "Assistant settings reset." });
406
536
  return;
407
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
+ }
408
555
 
409
556
  const result = await executeSlashTool(slash, {
410
557
  productDir,
411
- inspectState: state.inspectState,
558
+ runState: state.runState,
412
559
  configs,
413
560
  env,
414
561
  commandLog: state.commandLog,
415
562
  onEvent(event) {
416
- if (event.type === "tool-start") {
417
- appendMessage({
418
- role: "tool",
419
- status: "running",
420
- title: event.title || event.tool || "Tool",
421
- text: event.message,
422
- data: {
423
- command: event.command || null,
424
- testkitRelated: Boolean(event.testkitRelated),
425
- },
426
- });
427
- } else if (event.type === "tool-status") {
428
- state.setNotice(event.message);
429
- }
563
+ handleAssistantToolEvent(state, event, appendMessage);
430
564
  },
431
565
  provider: settings.provider,
432
566
  });
@@ -439,18 +573,32 @@ async function executeSlashCommand({
439
573
  });
440
574
  }
441
575
 
442
- function handleAssistantToolEvent(event, appendMessage) {
443
- if (!event || event.type !== "tool-start") return;
444
- appendMessage({
445
- role: "tool",
446
- status: "running",
447
- title: event.title || event.tool || "Tool",
448
- text: event.message || "Running tool",
449
- data: {
450
- command: event.command || null,
451
- testkitRelated: Boolean(event.testkitRelated),
452
- },
453
- });
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
+ }
454
602
  }
455
603
 
456
604
  function formatSettings(snapshot) {
@@ -483,39 +631,18 @@ async function executeSlashTool(slash, context) {
483
631
  case "service":
484
632
  return executeAssistantTool("read_context", { service: slash.service, mode: "detail" }, context);
485
633
  case "status":
486
- return executeAssistantTool("shell_exec", { command: "testkit status --dir ." }, context);
634
+ return executeAssistantTool("show_status", {}, context);
487
635
  case "discover":
488
- return executeAssistantTool("shell_exec", { command: "testkit discover --dir ." }, context);
636
+ return executeAssistantTool("discover_tests", {}, context);
489
637
  case "doctor":
490
- return executeAssistantTool("shell_exec", { command: "testkit doctor --dir ." }, context);
638
+ return executeAssistantTool("run_doctor", {}, context);
491
639
  case "run":
492
- return executeAssistantTool("shell_exec", { command: buildRunSlashCommand(slash.options) }, context);
640
+ return executeAssistantTool("run_tests", slash.options, context);
493
641
  default:
494
642
  throw new Error(`Unsupported slash command "${slash.type}"`);
495
643
  }
496
644
  }
497
645
 
498
- function buildRunSlashCommand(options = {}) {
499
- const parts = ["testkit", "run", "--dir", "."];
500
- for (const type of options.type || []) {
501
- parts.push("--type", type);
502
- }
503
- for (const suite of options.suite || []) {
504
- parts.push("--suite", suite);
505
- }
506
- for (const file of options.file || []) {
507
- parts.push("--file", file);
508
- }
509
- if (options.service) parts.push("--service", options.service);
510
- return parts.map(shellEscapeArg).join(" ");
511
- }
512
-
513
- function shellEscapeArg(value) {
514
- const stringValue = String(value);
515
- if (/^[a-zA-Z0-9._:/-]+$/.test(stringValue)) return stringValue;
516
- return `'${stringValue.replace(/'/g, `'\\''`)}'`;
517
- }
518
-
519
646
  function parseSlashCommandSafe(input) {
520
647
  try {
521
648
  return parseSlashCommand(input);
@@ -526,3 +653,42 @@ function parseSlashCommandSafe(input) {
526
653
  };
527
654
  }
528
655
  }
656
+
657
+ function sanitizeSettingsForResolvedProvider({ productDir, settings, resolvedProvider }) {
658
+ const mismatch = getModelProviderMismatch(resolvedProvider, settings.model);
659
+ if (!mismatch) return { settings, notice: null };
660
+ const previousModel = settings.model;
661
+ const sanitized = mergeAssistantSettings(settings, { model: null });
662
+ saveAssistantSettings(productDir, sanitized);
663
+ return {
664
+ settings: sanitized,
665
+ notice: `Cleared incompatible saved model "${previousModel}" for ${resolvedProvider}.`,
666
+ };
667
+ }
668
+
669
+ function routeLocalIntent(input) {
670
+ const normalized = String(input || "").trim().toLowerCase();
671
+ const runMatch = normalized.match(/^run\s+(int|e2e|scenario|dal|load|pw|all)(?:\s+tests?)?$/);
672
+ if (runMatch) {
673
+ return {
674
+ type: "run",
675
+ options: {
676
+ type: [runMatch[1]],
677
+ suite: [],
678
+ file: [],
679
+ service: null,
680
+ },
681
+ };
682
+ }
683
+ if (/^(show\s+)?latest\s+summary$/.test(normalized)) return { type: "status" };
684
+ if (/^list\s+test\s+files$/.test(normalized)) return { type: "discover" };
685
+ return null;
686
+ }
687
+
688
+ function serializeRunSession(session) {
689
+ if (!session) return null;
690
+ return {
691
+ productDir: session.productDir,
692
+ snapshot: session.getSnapshot(),
693
+ };
694
+ }