@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.
- package/README.md +14 -7
- package/lib/cli/agents/index.mjs +27 -19
- package/lib/cli/agents/providers/claude.mjs +3 -3
- package/lib/cli/agents/providers/codex.mjs +3 -3
- package/lib/cli/assistant/app.mjs +210 -0
- package/lib/cli/assistant/context-pack.mjs +191 -0
- package/lib/cli/assistant/interactive.mjs +53 -0
- package/lib/cli/assistant/prompt-builder.mjs +7 -9
- package/lib/cli/assistant/session.mjs +6 -1
- package/lib/cli/assistant/state.mjs +134 -46
- package/lib/cli/assistant/tool-registry.mjs +220 -230
- package/lib/cli/commands/assistant.mjs +50 -34
- package/lib/cli/{tui/detail-pane.mjs → context-resources.mjs} +81 -21
- package/lib/cli/entrypoint.mjs +12 -4
- package/lib/cli/presentation/tree-reporter.mjs +0 -101
- package/lib/cli/tui/inspect-app.mjs +7 -88
- package/lib/cli/tui/inspect-state.mjs +0 -117
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +5 -5
- package/lib/cli/agents/investigate.mjs +0 -75
- package/lib/cli/agents/investigation-context.mjs +0 -102
- package/lib/cli/agents/investigation-interpreter.mjs +0 -320
- package/lib/cli/agents/investigation-log.mjs +0 -37
- package/lib/cli/agents/prompt-builder.mjs +0 -25
- package/lib/cli/assistant/content.mjs +0 -60
- package/lib/cli/assistant/tool-run-reporter.mjs +0 -80
- package/lib/cli/tui/assistant-app.mjs +0 -82
- 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 {
|
|
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(() =>
|
|
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
|
-
|
|
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
|
-
|
|
81
|
+
const revealed = inspectState.revealFile(serviceName, filePath);
|
|
82
|
+
refreshContextPack();
|
|
83
|
+
return revealed;
|
|
65
84
|
},
|
|
66
85
|
|
|
67
86
|
revealService(serviceName) {
|
|
68
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
183
|
+
providerName,
|
|
133
184
|
configs,
|
|
134
|
-
|
|
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, "
|
|
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:
|
|
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
|
-
|
|
261
|
+
providerName,
|
|
203
262
|
configs,
|
|
204
|
-
|
|
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
|
|
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
|
|
247
|
-
|
|
248
|
-
:
|
|
309
|
+
return executeAssistantTool(
|
|
310
|
+
"read_context",
|
|
311
|
+
{ file: slash.file || null, mode: "detail" },
|
|
312
|
+
context
|
|
313
|
+
);
|
|
249
314
|
case "logs":
|
|
250
|
-
return executeAssistantTool("
|
|
315
|
+
return executeAssistantTool("read_context", { service: slash.service || null, mode: "logs" }, context);
|
|
251
316
|
case "artifacts":
|
|
252
|
-
return executeAssistantTool("
|
|
317
|
+
return executeAssistantTool("read_context", { file: slash.file || null, mode: "artifacts" }, context);
|
|
253
318
|
case "setup":
|
|
254
|
-
return executeAssistantTool("
|
|
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("
|
|
323
|
+
return executeAssistantTool("read_context", { service: slash.service, mode: "detail" }, context);
|
|
257
324
|
case "status":
|
|
258
|
-
return executeAssistantTool("
|
|
325
|
+
return executeAssistantTool("shell_exec", { command: "testkit status --dir ." }, context);
|
|
259
326
|
case "discover":
|
|
260
|
-
return executeAssistantTool("
|
|
327
|
+
return executeAssistantTool("shell_exec", { command: "testkit discover --dir ." }, context);
|
|
261
328
|
case "doctor":
|
|
262
|
-
return executeAssistantTool("
|
|
329
|
+
return executeAssistantTool("shell_exec", { command: "testkit doctor --dir ." }, context);
|
|
263
330
|
case "run":
|
|
264
|
-
return executeAssistantTool("
|
|
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);
|