@elench/testkit 0.1.97 → 0.1.99

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 (80) hide show
  1. package/README.md +8 -8
  2. package/lib/app/browser-bridge.mjs +1 -1
  3. package/lib/cli/assistant/actions.mjs +333 -0
  4. package/lib/cli/assistant/app.mjs +25 -1
  5. package/lib/cli/assistant/command-observer.mjs +110 -0
  6. package/lib/cli/assistant/command-results.mjs +167 -0
  7. package/lib/cli/assistant/composer.mjs +1 -1
  8. package/lib/cli/assistant/context-pack.mjs +73 -6
  9. package/lib/cli/assistant/interactive.mjs +1 -1
  10. package/lib/cli/assistant/prompt-builder.mjs +15 -8
  11. package/lib/cli/{agents → assistant}/providers/claude.mjs +2 -3
  12. package/lib/cli/{agents → assistant}/providers/codex.mjs +2 -6
  13. package/lib/cli/{agents → assistant/providers}/index.mjs +5 -5
  14. package/lib/cli/assistant/session.mjs +36 -94
  15. package/lib/cli/assistant/slash-commands.mjs +22 -1
  16. package/lib/cli/assistant/state.mjs +187 -100
  17. package/lib/cli/assistant/view-model.mjs +1 -1
  18. package/lib/cli/command-flags.mjs +61 -0
  19. package/lib/cli/commands/assistant.mjs +4 -3
  20. package/lib/cli/commands/browser/serve.mjs +5 -23
  21. package/lib/cli/commands/cleanup.mjs +8 -2
  22. package/lib/cli/commands/db/snapshot/capture.mjs +8 -4
  23. package/lib/cli/commands/destroy.mjs +8 -2
  24. package/lib/cli/commands/discover.mjs +13 -32
  25. package/lib/cli/commands/doctor.mjs +17 -14
  26. package/lib/cli/commands/run.mjs +14 -3
  27. package/lib/cli/commands/status.mjs +14 -3
  28. package/lib/cli/commands/typecheck.mjs +12 -9
  29. package/lib/cli/{tui/inspect-app.mjs → components/blocks/run-tree.mjs} +29 -54
  30. package/lib/cli/{tui → components/primitives}/filter-bar.mjs +1 -1
  31. package/lib/cli/{presentation → components/primitives}/summary-box.mjs +1 -1
  32. package/lib/cli/config.mjs +63 -0
  33. package/lib/cli/entrypoint.mjs +14 -5
  34. package/lib/cli/operations/browser/serve/operation.mjs +23 -0
  35. package/lib/cli/operations/cleanup/operation.mjs +8 -0
  36. package/lib/cli/{db.mjs → operations/db/snapshot/capture/operation.mjs} +15 -9
  37. package/lib/cli/operations/destroy/operation.mjs +12 -0
  38. package/lib/cli/operations/discover/operation.mjs +32 -0
  39. package/lib/cli/operations/doctor/operation.mjs +5 -0
  40. package/lib/cli/operations/run/operation.mjs +129 -0
  41. package/lib/cli/operations/status/operation.mjs +7 -0
  42. package/lib/cli/operations/typecheck/operation.mjs +5 -0
  43. package/lib/cli/renderers/browser-serve/text.mjs +6 -0
  44. package/lib/cli/renderers/cleanup/text.mjs +3 -0
  45. package/lib/cli/renderers/db-snapshot-capture/text.mjs +3 -0
  46. package/lib/cli/renderers/destroy/text.mjs +3 -0
  47. package/lib/cli/{presentation/discovery-reporter.mjs → renderers/discover/report.mjs} +3 -3
  48. package/lib/cli/renderers/discover/text.mjs +7 -0
  49. package/lib/cli/renderers/doctor/text.mjs +7 -0
  50. package/lib/cli/{presentation/failure-presentation.mjs → renderers/run/failure.mjs} +6 -6
  51. package/lib/cli/renderers/run/interactive.mjs +119 -0
  52. package/lib/cli/{presentation/run-reporter.mjs → renderers/run/text-reporter.mjs} +5 -5
  53. package/lib/cli/renderers/status/text.mjs +7 -0
  54. package/lib/cli/renderers/typecheck/text.mjs +7 -0
  55. package/lib/cli/{tui/inspect-model.mjs → state/run/model.mjs} +11 -26
  56. package/lib/cli/{tui/inspect-state.mjs → state/run/state.mjs} +11 -18
  57. package/lib/cli/{tui → state/tree}/fuzzy-match.mjs +1 -1
  58. package/lib/cli/terminal/capabilities.mjs +33 -0
  59. package/lib/database/index.mjs +9 -21
  60. package/lib/{cli/viewer.mjs → results/artifacts.mjs} +1 -1
  61. package/lib/{cli/context-resources.mjs → results/context.mjs} +1 -1
  62. package/lib/runner/maintenance.mjs +25 -14
  63. package/lib/runner/readiness.mjs +5 -4
  64. package/lib/runner/state-io.mjs +10 -4
  65. package/node_modules/@elench/next-analysis/package.json +1 -1
  66. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  67. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  68. package/node_modules/@elench/ts-analysis/package.json +1 -1
  69. package/package.json +7 -8
  70. package/lib/cli/assistant/protocol.mjs +0 -67
  71. package/lib/cli/assistant/tool-registry.mjs +0 -318
  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/{agents → assistant}/providers/shared.mjs +0 -0
  77. /package/lib/cli/{presentation/events-reporter.mjs → renderers/run/events.mjs} +0 -0
  78. /package/lib/cli/{presentation → terminal}/colors.mjs +0 -0
  79. /package/lib/cli/{presentation/terminal-layout.mjs → terminal/layout.mjs} +0 -0
  80. /package/lib/{cli/presentation → results}/code-frames.mjs +0 -0
@@ -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,15 +1,26 @@
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
+ import {
6
+ ASSISTANT_COMMAND_ID_ENV,
7
+ ASSISTANT_COMMAND_LOG_ENV,
8
+ ASSISTANT_RESULT_DIR_ENV,
9
+ ASSISTANT_SESSION_ENV,
10
+ ASSISTANT_WRAPPER_LOGGED_ENV,
11
+ } from "./command-results.mjs";
5
12
 
6
13
  export function prepareAssistantContextPack({
7
14
  productDir,
8
- inspectState,
15
+ runState,
9
16
  } = {}) {
10
17
  const contextDir = path.join(productDir, ".testkit", "assistant");
11
18
  const binDir = path.join(contextDir, "bin");
19
+ const resultDir = path.join(contextDir, "command-results");
20
+ const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
12
21
  fs.mkdirSync(binDir, { recursive: true });
22
+ fs.rmSync(resultDir, { recursive: true, force: true });
23
+ fs.mkdirSync(resultDir, { recursive: true });
13
24
 
14
25
  const commandLogPath = path.join(contextDir, "commands.jsonl");
15
26
  const contextPath = path.join(contextDir, "context.md");
@@ -23,7 +34,7 @@ export function prepareAssistantContextPack({
23
34
  const wrapperPath = path.join(binDir, "testkit");
24
35
 
25
36
  function refresh() {
26
- const snapshot = inspectState?.getSnapshot?.() || {};
37
+ const snapshot = runState?.getSnapshot?.() || {};
27
38
  const detailContent = readContextContent({ productDir, snapshot, mode: "detail", logTail: 12 });
28
39
  const logsContent = readContextContent({ productDir, snapshot, mode: "logs", logTail: 12 });
29
40
  const artifactsContent = readContextContent({ productDir, snapshot, mode: "artifacts", logTail: 12 });
@@ -55,7 +66,7 @@ export function prepareAssistantContextPack({
55
66
  }),
56
67
  "utf8"
57
68
  );
58
- fs.writeFileSync(wrapperPath, buildWrapperScript({ cliPath: resolveCliPath() }), {
69
+ fs.writeFileSync(wrapperPath, buildWrapperScript({ cliPath: resolveCliPath(), sessionId, resultDir, commandLogPath }), {
59
70
  encoding: "utf8",
60
71
  mode: 0o755,
61
72
  });
@@ -71,12 +82,23 @@ export function prepareAssistantContextPack({
71
82
  selectionPath,
72
83
  commandsPath,
73
84
  commandLogPath,
85
+ resultDir,
86
+ sessionId,
74
87
  focusedDetailPath,
75
88
  focusedLogsPath,
76
89
  focusedArtifactsPath,
77
90
  focusedSetupPath,
78
91
  binDir,
79
92
  wrapperPath,
93
+ providerEnv(baseEnv = process.env) {
94
+ return {
95
+ ...baseEnv,
96
+ PATH: [binDir, baseEnv?.PATH, process.env.PATH].filter(Boolean).join(path.delimiter),
97
+ [ASSISTANT_SESSION_ENV]: sessionId,
98
+ [ASSISTANT_RESULT_DIR_ENV]: resultDir,
99
+ [ASSISTANT_COMMAND_LOG_ENV]: commandLogPath,
100
+ };
101
+ },
80
102
  refresh,
81
103
  appendCommandLog(event) {
82
104
  if (!event || typeof event !== "object") return;
@@ -97,15 +119,36 @@ function resolveCliPath() {
97
119
  return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "bin", "testkit.mjs");
98
120
  }
99
121
 
100
- function buildWrapperScript({ cliPath } = {}) {
122
+ function buildWrapperScript({ cliPath, sessionId, resultDir, commandLogPath } = {}) {
101
123
  return `#!/usr/bin/env node
102
124
  import { spawnSync } from "child_process";
125
+ import fs from "fs";
126
+ import path from "path";
127
+
128
+ const commandId = process.env.${ASSISTANT_COMMAND_ID_ENV} || \`cmd-\${Date.now()}-\${Math.random().toString(36).slice(2, 10)}\`;
129
+ const commandLogPath = process.env.${ASSISTANT_COMMAND_LOG_ENV} || ${JSON.stringify(commandLogPath)};
130
+ const sessionId = process.env.${ASSISTANT_SESSION_ENV} || ${JSON.stringify(sessionId)};
131
+ const argv = process.argv.slice(2);
132
+
133
+ appendCommandLog({
134
+ type: "command_start",
135
+ commandId,
136
+ command: "testkit",
137
+ kind: inferKind(argv),
138
+ argv,
139
+ cwd: process.cwd(),
140
+ });
103
141
 
104
142
  const result = spawnSync(process.execPath, [${JSON.stringify(cliPath)}, ...process.argv.slice(2)], {
105
143
  cwd: process.cwd(),
106
144
  env: {
107
145
  ...process.env,
108
146
  TESTKIT_NO_ASSISTANT_DEFAULT: "1",
147
+ ${ASSISTANT_SESSION_ENV}: sessionId,
148
+ ${ASSISTANT_RESULT_DIR_ENV}: process.env.${ASSISTANT_RESULT_DIR_ENV} || ${JSON.stringify(resultDir)},
149
+ ${ASSISTANT_COMMAND_LOG_ENV}: commandLogPath,
150
+ ${ASSISTANT_COMMAND_ID_ENV}: commandId,
151
+ ${ASSISTANT_WRAPPER_LOGGED_ENV}: "1",
109
152
  },
110
153
  stdio: "inherit",
111
154
  });
@@ -113,7 +156,30 @@ const result = spawnSync(process.execPath, [${JSON.stringify(cliPath)}, ...proce
113
156
  if (result.error) {
114
157
  throw result.error;
115
158
  }
159
+ appendCommandLog({
160
+ type: "command_exit",
161
+ commandId,
162
+ command: "testkit",
163
+ kind: inferKind(argv),
164
+ argv,
165
+ cwd: process.cwd(),
166
+ code: result.status ?? 0,
167
+ signal: result.signal ?? null,
168
+ });
116
169
  process.exit(result.status ?? 0);
170
+
171
+ function appendCommandLog(event) {
172
+ try {
173
+ fs.mkdirSync(path.dirname(commandLogPath), { recursive: true });
174
+ fs.appendFileSync(commandLogPath, \`\${JSON.stringify({ timestamp: new Date().toISOString(), sessionId, ...event })}\\n\`, "utf8");
175
+ } catch {
176
+ // Command observation must not affect command execution.
177
+ }
178
+ }
179
+
180
+ function inferKind(args) {
181
+ return args.find((arg) => !String(arg).startsWith("-")) || "run";
182
+ }
117
183
  `;
118
184
  }
119
185
 
@@ -160,7 +226,8 @@ function buildContextMarkdown(productDir, snapshot, paths) {
160
226
  lines.push(
161
227
  "",
162
228
  "## Guidance",
163
- "- Use shell commands like `npm run testkit`, `npx testkit`, or `testkit run <type> --dir .` when you need to execute tests.",
229
+ "- Work normally in the repository and run real commands. Testkit observes recognized Testkit commands and renders them in the assistant UI.",
230
+ "- Prefer `testkit ...` commands when using Testkit directly; `npx testkit ...` and project scripts are also valid.",
164
231
  "- Do not reinterpret CLI syntax after an execution failure unless `testkit run --help` confirms a syntax problem.",
165
232
  "- Use the command log and focused context files before rereading artifacts manually.",
166
233
  "- 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,12 +1,11 @@
1
- import { readContextContent } from "../context-resources.mjs";
2
- import { buildAssistantResponseContract } from "./protocol.mjs";
1
+ import { readContextContent } from "../../results/context.mjs";
3
2
 
4
3
  export function buildAssistantPrompt({
5
4
  productDir,
6
5
  snapshot,
7
6
  transcript = [],
8
- tools = [],
9
7
  userMessage,
8
+ commandLog = null,
10
9
  } = {}) {
11
10
  const selectionSummary = buildSelectionSummary(snapshot);
12
11
  const focusPreview = buildFocusPreview(productDir, snapshot);
@@ -14,11 +13,19 @@ export function buildAssistantPrompt({
14
13
 
15
14
  return [
16
15
  "You are Testkit Assistant.",
17
- "You help users run tests, inspect failures, read logs and artifacts, and navigate the current local test state.",
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.",
20
- "Use read_context before repeating artifact/log inspection work, and use read_file/search_repo when you need codebase context.",
21
- buildAssistantResponseContract({ tools }),
16
+ "You are running as a coding agent inside the user's repository.",
17
+ "Work normally: inspect files, edit files, run real shell commands, and iterate until the user's request is handled.",
18
+ "When using Testkit, run real commands such as `testkit discover`, `testkit run int`, `npx testkit run int`, or the repository's package scripts.",
19
+ "Testkit observes recognized Testkit commands and renders rich assistant UI from the real command output, sidecars, and artifacts.",
20
+ "Do not respond with a JSON tool envelope. Give the user a normal final answer when you are done.",
21
+ "",
22
+ "Assistant context files:",
23
+ ...(commandLog ? [
24
+ `- Context: ${commandLog.contextPath}`,
25
+ `- Command reference: ${commandLog.commandsPath}`,
26
+ `- Command log: ${commandLog.commandLogPath}`,
27
+ `- Current selection: ${commandLog.selectionPath}`,
28
+ ] : ["- No assistant context pack is available."]),
22
29
  "",
23
30
  "Current run summary:",
24
31
  ...(summaryRows.length > 0 ? summaryRows.map(([label, value]) => `- ${label}: ${value}`) : ["- No run artifact is currently loaded."]),
@@ -16,6 +16,7 @@ export function startClaudeHostedSession({
16
16
  model = null,
17
17
  effort = null,
18
18
  providerArgs = [],
19
+ env = process.env,
19
20
  } = {}) {
20
21
  const args = [
21
22
  "-p",
@@ -25,9 +26,6 @@ export function startClaudeHostedSession({
25
26
  "--include-partial-messages",
26
27
  ];
27
28
 
28
- if (purpose === "assistant") {
29
- args.push("--permission-mode", "plan");
30
- }
31
29
  if (model) {
32
30
  args.push("--model", String(model));
33
31
  }
@@ -44,6 +42,7 @@ export function startClaudeHostedSession({
44
42
  stdout: "pipe",
45
43
  stderr: "pipe",
46
44
  reject: false,
45
+ env,
47
46
  });
48
47
 
49
48
  return createHostedSessionRunner({
@@ -19,6 +19,7 @@ export function startCodexHostedSession({
19
19
  purpose = "assistant",
20
20
  model = null,
21
21
  providerArgs = [],
22
+ env = process.env,
22
23
  } = {}) {
23
24
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-codex-"));
24
25
  const outputFile = path.join(tempDir, "final-message.txt");
@@ -28,7 +29,6 @@ export function startCodexHostedSession({
28
29
  model,
29
30
  providerArgs,
30
31
  prompt,
31
- sandbox: process.env.TESTKIT_CODEX_SANDBOX,
32
32
  });
33
33
 
34
34
  const child = execa(command, args, {
@@ -37,6 +37,7 @@ export function startCodexHostedSession({
37
37
  stdout: "pipe",
38
38
  stderr: "pipe",
39
39
  reject: false,
40
+ env,
40
41
  });
41
42
 
42
43
  const session = createHostedSessionRunner({
@@ -64,18 +65,13 @@ export function startCodexHostedSession({
64
65
 
65
66
  export function buildCodexArgs({
66
67
  outputFile,
67
- purpose = "assistant",
68
68
  model = null,
69
69
  providerArgs = [],
70
70
  prompt = "",
71
- sandbox = null,
72
71
  } = {}) {
73
72
  const args = ["exec", "--json"];
74
73
  if (outputFile) args.push("-o", outputFile);
75
74
 
76
- if (purpose === "assistant") {
77
- args.push("-s", String(sandbox || "workspace-write"));
78
- }
79
75
  if (model) {
80
76
  args.push("--model", String(model));
81
77
  }
@@ -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,
@@ -69,7 +69,7 @@ export function startAgentSession({
69
69
  const resolvedProvider = resolvePreferredProvider(provider, env);
70
70
  const command = resolveProviderBinary(resolvedProvider, env);
71
71
  if (resolvedProvider === "claude") {
72
- return startClaudeHostedSession({ command, cwd, prompt, onEvent, purpose, model, effort, providerArgs });
72
+ return startClaudeHostedSession({ command, cwd, prompt, onEvent, purpose, model, effort, providerArgs, env });
73
73
  }
74
- return startCodexHostedSession({ command, cwd, prompt, onEvent, purpose, model, providerArgs });
74
+ return startCodexHostedSession({ command, cwd, prompt, onEvent, purpose, model, providerArgs, env });
75
75
  }
@@ -1,11 +1,10 @@
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
- import { listAssistantTools, executeAssistantTool } from "./tool-registry.mjs";
4
- import { parseAssistantEnvelope } from "./protocol.mjs";
3
+ import { createAssistantCommandObserver } from "./command-observer.mjs";
5
4
 
6
5
  export async function runAssistantConversationTurn({
7
6
  productDir,
8
- inspectState,
7
+ runState,
9
8
  transcript,
10
9
  userMessage,
11
10
  provider = "auto",
@@ -18,41 +17,37 @@ export async function runAssistantConversationTurn({
18
17
  onResolvedProvider,
19
18
  onPrompt,
20
19
  } = {}) {
21
- const tools = listAssistantTools();
22
- const toolContext = {
20
+ const observer = createAssistantCommandObserver({
23
21
  productDir,
24
- inspectState,
25
- configs,
26
- env,
22
+ runState,
27
23
  commandLog,
28
24
  onEvent: onToolEvent,
29
- };
25
+ });
30
26
 
31
- let currentTranscript = [...(transcript || []), { role: "user", text: userMessage }];
32
- const emitted = [];
27
+ const snapshot = runState.getSnapshot();
28
+ const prompt = buildAssistantPrompt({
29
+ productDir,
30
+ snapshot,
31
+ transcript: [...(transcript || []), { role: "user", text: userMessage }],
32
+ userMessage,
33
+ commandLog,
34
+ });
33
35
 
34
- for (let attempt = 0; attempt < 6; attempt += 1) {
35
- const snapshot = inspectState.getSnapshot();
36
- const prompt = buildAssistantPrompt({
37
- productDir,
38
- snapshot,
39
- transcript: currentTranscript,
40
- tools,
41
- userMessage,
42
- });
36
+ const runtimeSettings = settings || { provider };
37
+ const resolvedProvider = resolvePreferredProvider(runtimeSettings.provider || provider, env);
38
+ const providerEnv = commandLog?.providerEnv?.(env) || env;
39
+ onResolvedProvider?.(resolvedProvider);
40
+ onPrompt?.({
41
+ prompt,
42
+ provider: resolvedProvider,
43
+ model: runtimeSettings.model || null,
44
+ effort: runtimeSettings.effort || null,
45
+ });
46
+ onStatus?.(`Thinking with ${resolvedProvider}...`);
43
47
 
44
- const runtimeSettings = settings || { provider };
45
- const resolvedProvider = resolvePreferredProvider(runtimeSettings.provider || provider, env);
46
- onResolvedProvider?.(resolvedProvider);
47
- onPrompt?.({
48
- prompt,
49
- provider: resolvedProvider,
50
- model: runtimeSettings.model || null,
51
- effort: runtimeSettings.effort || null,
52
- });
53
- onStatus?.(`Thinking with ${resolvedProvider}...`);
54
- const events = [];
55
- const session = startAgentSession({
48
+ observer.start();
49
+ try {
50
+ const session = startProviderSession({
56
51
  provider: runtimeSettings.provider || provider,
57
52
  model: runtimeSettings.model || null,
58
53
  effort: runtimeSettings.effort || null,
@@ -60,74 +55,21 @@ export async function runAssistantConversationTurn({
60
55
  cwd: productDir,
61
56
  prompt,
62
57
  purpose: "assistant",
63
- env,
58
+ env: providerEnv,
64
59
  onEvent(event) {
65
- events.push(event);
66
60
  if (event.type === "status" || event.type === "tool") onStatus?.(formatProviderEvent(event));
67
61
  },
68
62
  });
69
63
  const result = await session.completion;
70
- const envelope = parseAssistantEnvelope(result.finalText || "");
71
-
72
- if (envelope.type === "tool") {
73
- if (envelope.commentary) {
74
- emitted.push({ role: "assistant", text: envelope.commentary });
75
- currentTranscript.push({ role: "assistant", text: envelope.commentary });
76
- }
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
- }
95
- const toolText = toolResult.text || `${envelope.tool} completed`;
96
- emitted.push({
97
- role: "tool",
98
- text: toolText,
99
- toolName: envelope.tool,
100
- title: toolResult.title || envelope.tool,
101
- data: toolResult.data || null,
102
- });
103
- currentTranscript.push({
104
- role: "tool",
105
- text: `${envelope.tool}: ${toolText}`,
106
- });
107
- onToolEvent?.({ type: "tool-result", tool: envelope.tool, text: toolText });
108
- continue;
109
- }
110
-
111
- emitted.push({
64
+ observer.scan();
65
+ commandLog?.refresh?.();
66
+ return [{
112
67
  role: "assistant",
113
- text: envelope.message || result.finalText || "",
114
- });
115
- return emitted;
116
- }
117
-
118
- emitted.push({
119
- role: "assistant",
120
- text: "I hit the assistant tool-call limit before reaching a final answer.",
121
- });
122
- return emitted;
123
- }
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.";
68
+ text: result.finalText || "",
69
+ }];
70
+ } finally {
71
+ observer.stop();
129
72
  }
130
- return `Tool failed: ${message}`;
131
73
  }
132
74
 
133
75
  function formatProviderEvent(event) {
@@ -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: [],