@elench/testkit 0.1.89 → 0.1.91

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 (31) hide show
  1. package/README.md +14 -7
  2. package/lib/cli/agents/index.mjs +27 -19
  3. package/lib/cli/agents/providers/claude.mjs +3 -3
  4. package/lib/cli/agents/providers/codex.mjs +3 -3
  5. package/lib/cli/assistant/app.mjs +210 -0
  6. package/lib/cli/assistant/context-pack.mjs +191 -0
  7. package/lib/cli/assistant/interactive.mjs +53 -0
  8. package/lib/cli/assistant/prompt-builder.mjs +7 -9
  9. package/lib/cli/assistant/session.mjs +6 -1
  10. package/lib/cli/assistant/state.mjs +134 -46
  11. package/lib/cli/assistant/tool-registry.mjs +220 -230
  12. package/lib/cli/commands/assistant.mjs +50 -34
  13. package/lib/cli/{tui/detail-pane.mjs → context-resources.mjs} +81 -21
  14. package/lib/cli/entrypoint.mjs +12 -4
  15. package/lib/cli/presentation/tree-reporter.mjs +0 -101
  16. package/lib/cli/tui/inspect-app.mjs +7 -88
  17. package/lib/cli/tui/inspect-state.mjs +0 -117
  18. package/node_modules/@elench/next-analysis/package.json +1 -1
  19. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  20. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  21. package/node_modules/@elench/ts-analysis/package.json +1 -1
  22. package/package.json +5 -5
  23. package/lib/cli/agents/investigate.mjs +0 -75
  24. package/lib/cli/agents/investigation-context.mjs +0 -102
  25. package/lib/cli/agents/investigation-interpreter.mjs +0 -320
  26. package/lib/cli/agents/investigation-log.mjs +0 -37
  27. package/lib/cli/agents/prompt-builder.mjs +0 -25
  28. package/lib/cli/assistant/content.mjs +0 -60
  29. package/lib/cli/assistant/tool-run-reporter.mjs +0 -80
  30. package/lib/cli/tui/assistant-app.mjs +0 -82
  31. package/lib/cli/tui/assistant-render.mjs +0 -99
@@ -1,27 +1,37 @@
1
1
  import { loadCurrentRunArtifact, loadLatestRunArtifact } from "../viewer.mjs";
2
2
  import { createInspectState } from "../tui/inspect-state.mjs";
3
- import { buildAssistantContext } from "./content.mjs";
3
+ import { buildContextSelection } from "../context-resources.mjs";
4
4
  import { parseSlashCommand, formatSlashHelpLines } from "./slash-commands.mjs";
5
5
  import { executeAssistantTool } from "./tool-registry.mjs";
6
6
  import { runAssistantConversationTurn } from "./session.mjs";
7
+ import { prepareAssistantContextPack } from "./context-pack.mjs";
7
8
 
8
9
  export function createAssistantState({
9
10
  productDir,
10
11
  provider = "auto",
11
12
  dataSource = "artifact",
12
13
  configs = [],
14
+ env = process.env,
13
15
  } = {}) {
14
16
  const inspectState = createInspectState({ dataSource });
17
+ const commandLog = prepareAssistantContextPack({
18
+ productDir,
19
+ inspectState,
20
+ });
15
21
 
16
22
  const listeners = new Set();
17
23
  const messages = [];
18
24
  let composer = "";
25
+ let composerCursor = 0;
19
26
  let notice = null;
20
27
  let busy = false;
21
28
  let providerName = provider;
22
29
  let activeStatus = null;
23
30
 
24
- inspectState.subscribe(() => notify());
31
+ inspectState.subscribe(() => {
32
+ commandLog.refresh();
33
+ notify();
34
+ });
25
35
 
26
36
  function notify() {
27
37
  for (const callback of listeners) callback();
@@ -41,8 +51,13 @@ export function createAssistantState({
41
51
  notify();
42
52
  }
43
53
 
44
- return {
54
+ function refreshContextPack() {
55
+ commandLog.refresh();
56
+ }
57
+
58
+ const state = {
45
59
  inspectState,
60
+ commandLog,
46
61
 
47
62
  async loadLatestArtifact() {
48
63
  try {
@@ -50,6 +65,7 @@ export function createAssistantState({
50
65
  } catch {
51
66
  // No artifact yet.
52
67
  }
68
+ refreshContextPack();
53
69
  },
54
70
 
55
71
  async loadCurrentArtifact() {
@@ -58,28 +74,64 @@ export function createAssistantState({
58
74
  } catch {
59
75
  // No artifact yet.
60
76
  }
77
+ refreshContextPack();
61
78
  },
62
79
 
63
80
  revealFile(serviceName, filePath) {
64
- return inspectState.revealFile(serviceName, filePath);
81
+ const revealed = inspectState.revealFile(serviceName, filePath);
82
+ refreshContextPack();
83
+ return revealed;
65
84
  },
66
85
 
67
86
  revealService(serviceName) {
68
- return inspectState.revealService(serviceName);
87
+ const revealed = inspectState.revealService(serviceName);
88
+ refreshContextPack();
89
+ return revealed;
69
90
  },
70
91
 
71
92
  setComposer(value) {
72
93
  composer = String(value || "");
94
+ composerCursor = Math.max(0, Math.min(composer.length, composerCursor));
73
95
  notify();
74
96
  },
75
97
 
76
- appendComposer(text) {
77
- composer += String(text || "");
98
+ insertComposer(text) {
99
+ const nextText = String(text || "");
100
+ if (!nextText) return;
101
+ composer = `${composer.slice(0, composerCursor)}${nextText}${composer.slice(composerCursor)}`;
102
+ composerCursor += nextText.length;
78
103
  notify();
79
104
  },
80
105
 
106
+ appendComposer(text) {
107
+ state.insertComposer(text);
108
+ },
109
+
81
110
  backspaceComposer() {
82
- composer = composer.slice(0, -1);
111
+ if (composerCursor === 0) return;
112
+ composer = `${composer.slice(0, composerCursor - 1)}${composer.slice(composerCursor)}`;
113
+ composerCursor -= 1;
114
+ notify();
115
+ },
116
+
117
+ deleteComposer() {
118
+ if (composerCursor >= composer.length) return;
119
+ composer = `${composer.slice(0, composerCursor)}${composer.slice(composerCursor + 1)}`;
120
+ notify();
121
+ },
122
+
123
+ moveComposerCursor(delta) {
124
+ composerCursor = Math.max(0, Math.min(composer.length, composerCursor + delta));
125
+ notify();
126
+ },
127
+
128
+ moveComposerCursorToStart() {
129
+ composerCursor = 0;
130
+ notify();
131
+ },
132
+
133
+ moveComposerCursorToEnd() {
134
+ composerCursor = composer.length;
83
135
  notify();
84
136
  },
85
137
 
@@ -106,9 +158,10 @@ export function createAssistantState({
106
158
  async submitCurrentComposer() {
107
159
  const value = composer.trim();
108
160
  composer = "";
161
+ composerCursor = 0;
109
162
  notify();
110
163
  if (!value) return;
111
- await this.submitInput(value);
164
+ await state.submitInput(value);
112
165
  },
113
166
 
114
167
  async submitInput(input) {
@@ -118,29 +171,19 @@ export function createAssistantState({
118
171
 
119
172
  const slash = parseSlashCommandSafe(trimmed);
120
173
  if (slash?.type === "__error__") {
121
- appendMessage({
122
- role: "system",
123
- text: slash.error,
124
- });
174
+ appendMessage({ role: "system", text: slash.error });
125
175
  return;
126
176
  }
127
177
  if (slash) {
128
178
  try {
129
179
  await executeSlashCommand({
130
180
  slash,
181
+ state,
131
182
  productDir,
132
- inspectState,
183
+ providerName,
133
184
  configs,
134
- setProvider: (value) => {
135
- providerName = value;
136
- },
185
+ env,
137
186
  appendMessage,
138
- setNotice: (value) => {
139
- notice = value;
140
- },
141
- clearMessages: () => {
142
- messages.length = 0;
143
- },
144
187
  });
145
188
  } catch (error) {
146
189
  appendMessage({
@@ -148,19 +191,22 @@ export function createAssistantState({
148
191
  text: error instanceof Error ? error.message : String(error),
149
192
  });
150
193
  }
194
+ refreshContextPack();
151
195
  notify();
152
196
  return;
153
197
  }
154
198
 
155
199
  try {
156
- setBusy(true, "Waiting for assistant...");
200
+ setBusy(true, `Thinking with ${providerName === "auto" ? "provider" : providerName}...`);
157
201
  const emitted = await runAssistantConversationTurn({
158
202
  productDir,
159
203
  inspectState,
160
204
  transcript: messages.map((entry) => ({ role: entry.role, text: entry.text })),
161
205
  userMessage: trimmed,
162
206
  provider: providerName,
207
+ env,
163
208
  configs,
209
+ commandLog,
164
210
  onStatus(status) {
165
211
  activeStatus = status;
166
212
  notify();
@@ -173,6 +219,7 @@ export function createAssistantState({
173
219
  text: error instanceof Error ? error.message : String(error),
174
220
  });
175
221
  } finally {
222
+ refreshContextPack();
176
223
  setBusy(false, null);
177
224
  }
178
225
  },
@@ -184,50 +231,68 @@ export function createAssistantState({
184
231
 
185
232
  getSnapshot() {
186
233
  return {
187
- context: buildAssistantContext(inspectState.getSnapshot()),
234
+ context: buildContextSelection(inspectState.getSnapshot()),
188
235
  messages: [...messages],
189
236
  composer,
237
+ composerCursor,
190
238
  notice,
191
239
  busy,
192
240
  provider: providerName,
193
241
  activeStatus,
242
+ contextPaths: {
243
+ contextPath: commandLog.contextPath,
244
+ summaryPath: commandLog.summaryPath,
245
+ selectionPath: commandLog.selectionPath,
246
+ commandsPath: commandLog.commandsPath,
247
+ commandLogPath: commandLog.commandLogPath,
248
+ },
194
249
  };
195
250
  },
196
251
  };
252
+
253
+ refreshContextPack();
254
+ return state;
197
255
  }
198
256
 
199
257
  async function executeSlashCommand({
200
258
  slash,
259
+ state,
201
260
  productDir,
202
- inspectState,
261
+ providerName,
203
262
  configs,
204
- setProvider,
263
+ env,
205
264
  appendMessage,
206
- setNotice,
207
- clearMessages,
208
265
  } = {}) {
209
266
  if (slash.type === "help") {
210
267
  appendMessage({ role: "assistant", text: formatSlashHelpLines().join("\n") });
211
268
  return;
212
269
  }
213
270
  if (slash.type === "clear") {
214
- clearMessages?.();
271
+ state.clearMessages();
215
272
  return;
216
273
  }
217
274
  if (slash.type === "quit") {
218
- setNotice?.("Use q or Ctrl+C to quit the interactive assistant.");
275
+ state.setNotice("Use q or Ctrl+C to quit the interactive assistant.");
219
276
  return;
220
277
  }
221
278
  if (slash.type === "provider") {
222
- setProvider(slash.provider);
279
+ state.setProvider(slash.provider);
223
280
  appendMessage({ role: "assistant", text: `Provider set to ${slash.provider}.` });
224
281
  return;
225
282
  }
226
283
 
227
284
  const result = await executeSlashTool(slash, {
228
285
  productDir,
229
- inspectState,
286
+ inspectState: state.inspectState,
230
287
  configs,
288
+ env,
289
+ commandLog: state.commandLog,
290
+ onEvent(event) {
291
+ if (event.type === "tool-status") {
292
+ state.setNotice(event.message);
293
+ }
294
+ },
295
+ provider: providerName,
231
296
  });
232
297
  appendMessage({
233
298
  role: "tool",
@@ -240,33 +305,56 @@ async function executeSlashCommand({
240
305
 
241
306
  async function executeSlashTool(slash, context) {
242
307
  switch (slash.type) {
243
- case "file":
244
- return executeAssistantTool("focus_file", { file: slash.file }, context);
245
308
  case "inspect":
246
- return slash.file
247
- ? executeAssistantTool("focus_file", { file: slash.file }, context)
248
- : executeAssistantTool("inspect_focus", {}, context);
309
+ return executeAssistantTool(
310
+ "read_context",
311
+ { file: slash.file || null, mode: "detail" },
312
+ context
313
+ );
249
314
  case "logs":
250
- return executeAssistantTool("read_logs", { service: slash.service || null }, context);
315
+ return executeAssistantTool("read_context", { service: slash.service || null, mode: "logs" }, context);
251
316
  case "artifacts":
252
- return executeAssistantTool("read_artifacts", { file: slash.file || null }, context);
317
+ return executeAssistantTool("read_context", { file: slash.file || null, mode: "artifacts" }, context);
253
318
  case "setup":
254
- return executeAssistantTool("read_setup", { service: slash.service || null }, context);
319
+ return executeAssistantTool("read_context", { service: slash.service || null, mode: "setup" }, context);
320
+ case "file":
321
+ return executeAssistantTool("read_file", { path: slash.file }, context);
255
322
  case "service":
256
- return executeAssistantTool("focus_service", { service: slash.service }, context);
323
+ return executeAssistantTool("read_context", { service: slash.service, mode: "detail" }, context);
257
324
  case "status":
258
- return executeAssistantTool("show_status", {}, context);
325
+ return executeAssistantTool("shell_exec", { command: "testkit status --dir ." }, context);
259
326
  case "discover":
260
- return executeAssistantTool("discover_tests", {}, context);
327
+ return executeAssistantTool("shell_exec", { command: "testkit discover --dir ." }, context);
261
328
  case "doctor":
262
- return executeAssistantTool("run_doctor", {}, context);
329
+ return executeAssistantTool("shell_exec", { command: "testkit doctor --dir ." }, context);
263
330
  case "run":
264
- return executeAssistantTool("run_tests", slash.options, context);
331
+ return executeAssistantTool("shell_exec", { command: buildRunSlashCommand(slash.options) }, context);
265
332
  default:
266
333
  throw new Error(`Unsupported slash command "${slash.type}"`);
267
334
  }
268
335
  }
269
336
 
337
+ function buildRunSlashCommand(options = {}) {
338
+ const parts = ["testkit", "run", "--dir", "."];
339
+ for (const type of options.type || []) {
340
+ parts.push("--type", type);
341
+ }
342
+ for (const suite of options.suite || []) {
343
+ parts.push("--suite", suite);
344
+ }
345
+ for (const file of options.file || []) {
346
+ parts.push("--file", file);
347
+ }
348
+ if (options.service) parts.push("--service", options.service);
349
+ return parts.map(shellEscapeArg).join(" ");
350
+ }
351
+
352
+ function shellEscapeArg(value) {
353
+ const stringValue = String(value);
354
+ if (/^[a-zA-Z0-9._:/-]+$/.test(stringValue)) return stringValue;
355
+ return `'${stringValue.replace(/'/g, `'\\''`)}'`;
356
+ }
357
+
270
358
  function parseSlashCommandSafe(input) {
271
359
  try {
272
360
  return parseSlashCommand(input);