@elench/testkit 0.1.90 → 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.
@@ -4,6 +4,7 @@ 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,
@@ -13,16 +14,24 @@ export function createAssistantState({
13
14
  env = process.env,
14
15
  } = {}) {
15
16
  const inspectState = createInspectState({ dataSource });
17
+ const commandLog = prepareAssistantContextPack({
18
+ productDir,
19
+ inspectState,
20
+ });
16
21
 
17
22
  const listeners = new Set();
18
23
  const messages = [];
19
24
  let composer = "";
25
+ let composerCursor = 0;
20
26
  let notice = null;
21
27
  let busy = false;
22
28
  let providerName = provider;
23
29
  let activeStatus = null;
24
30
 
25
- inspectState.subscribe(() => notify());
31
+ inspectState.subscribe(() => {
32
+ commandLog.refresh();
33
+ notify();
34
+ });
26
35
 
27
36
  function notify() {
28
37
  for (const callback of listeners) callback();
@@ -42,8 +51,13 @@ export function createAssistantState({
42
51
  notify();
43
52
  }
44
53
 
45
- return {
54
+ function refreshContextPack() {
55
+ commandLog.refresh();
56
+ }
57
+
58
+ const state = {
46
59
  inspectState,
60
+ commandLog,
47
61
 
48
62
  async loadLatestArtifact() {
49
63
  try {
@@ -51,6 +65,7 @@ export function createAssistantState({
51
65
  } catch {
52
66
  // No artifact yet.
53
67
  }
68
+ refreshContextPack();
54
69
  },
55
70
 
56
71
  async loadCurrentArtifact() {
@@ -59,28 +74,64 @@ export function createAssistantState({
59
74
  } catch {
60
75
  // No artifact yet.
61
76
  }
77
+ refreshContextPack();
62
78
  },
63
79
 
64
80
  revealFile(serviceName, filePath) {
65
- return inspectState.revealFile(serviceName, filePath);
81
+ const revealed = inspectState.revealFile(serviceName, filePath);
82
+ refreshContextPack();
83
+ return revealed;
66
84
  },
67
85
 
68
86
  revealService(serviceName) {
69
- return inspectState.revealService(serviceName);
87
+ const revealed = inspectState.revealService(serviceName);
88
+ refreshContextPack();
89
+ return revealed;
70
90
  },
71
91
 
72
92
  setComposer(value) {
73
93
  composer = String(value || "");
94
+ composerCursor = Math.max(0, Math.min(composer.length, composerCursor));
74
95
  notify();
75
96
  },
76
97
 
77
- appendComposer(text) {
78
- 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;
79
103
  notify();
80
104
  },
81
105
 
106
+ appendComposer(text) {
107
+ state.insertComposer(text);
108
+ },
109
+
82
110
  backspaceComposer() {
83
- 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;
84
135
  notify();
85
136
  },
86
137
 
@@ -107,9 +158,10 @@ export function createAssistantState({
107
158
  async submitCurrentComposer() {
108
159
  const value = composer.trim();
109
160
  composer = "";
161
+ composerCursor = 0;
110
162
  notify();
111
163
  if (!value) return;
112
- await this.submitInput(value);
164
+ await state.submitInput(value);
113
165
  },
114
166
 
115
167
  async submitInput(input) {
@@ -119,29 +171,19 @@ export function createAssistantState({
119
171
 
120
172
  const slash = parseSlashCommandSafe(trimmed);
121
173
  if (slash?.type === "__error__") {
122
- appendMessage({
123
- role: "system",
124
- text: slash.error,
125
- });
174
+ appendMessage({ role: "system", text: slash.error });
126
175
  return;
127
176
  }
128
177
  if (slash) {
129
178
  try {
130
179
  await executeSlashCommand({
131
180
  slash,
181
+ state,
132
182
  productDir,
133
- inspectState,
183
+ providerName,
134
184
  configs,
135
- setProvider: (value) => {
136
- providerName = value;
137
- },
185
+ env,
138
186
  appendMessage,
139
- setNotice: (value) => {
140
- notice = value;
141
- },
142
- clearMessages: () => {
143
- messages.length = 0;
144
- },
145
187
  });
146
188
  } catch (error) {
147
189
  appendMessage({
@@ -149,12 +191,13 @@ export function createAssistantState({
149
191
  text: error instanceof Error ? error.message : String(error),
150
192
  });
151
193
  }
194
+ refreshContextPack();
152
195
  notify();
153
196
  return;
154
197
  }
155
198
 
156
199
  try {
157
- setBusy(true, "Waiting for assistant...");
200
+ setBusy(true, `Thinking with ${providerName === "auto" ? "provider" : providerName}...`);
158
201
  const emitted = await runAssistantConversationTurn({
159
202
  productDir,
160
203
  inspectState,
@@ -163,6 +206,7 @@ export function createAssistantState({
163
206
  provider: providerName,
164
207
  env,
165
208
  configs,
209
+ commandLog,
166
210
  onStatus(status) {
167
211
  activeStatus = status;
168
212
  notify();
@@ -175,6 +219,7 @@ export function createAssistantState({
175
219
  text: error instanceof Error ? error.message : String(error),
176
220
  });
177
221
  } finally {
222
+ refreshContextPack();
178
223
  setBusy(false, null);
179
224
  }
180
225
  },
@@ -189,47 +234,65 @@ export function createAssistantState({
189
234
  context: buildContextSelection(inspectState.getSnapshot()),
190
235
  messages: [...messages],
191
236
  composer,
237
+ composerCursor,
192
238
  notice,
193
239
  busy,
194
240
  provider: providerName,
195
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
+ },
196
249
  };
197
250
  },
198
251
  };
252
+
253
+ refreshContextPack();
254
+ return state;
199
255
  }
200
256
 
201
257
  async function executeSlashCommand({
202
258
  slash,
259
+ state,
203
260
  productDir,
204
- inspectState,
261
+ providerName,
205
262
  configs,
206
- setProvider,
263
+ env,
207
264
  appendMessage,
208
- setNotice,
209
- clearMessages,
210
265
  } = {}) {
211
266
  if (slash.type === "help") {
212
267
  appendMessage({ role: "assistant", text: formatSlashHelpLines().join("\n") });
213
268
  return;
214
269
  }
215
270
  if (slash.type === "clear") {
216
- clearMessages?.();
271
+ state.clearMessages();
217
272
  return;
218
273
  }
219
274
  if (slash.type === "quit") {
220
- setNotice?.("Use q or Ctrl+C to quit the interactive assistant.");
275
+ state.setNotice("Use q or Ctrl+C to quit the interactive assistant.");
221
276
  return;
222
277
  }
223
278
  if (slash.type === "provider") {
224
- setProvider(slash.provider);
279
+ state.setProvider(slash.provider);
225
280
  appendMessage({ role: "assistant", text: `Provider set to ${slash.provider}.` });
226
281
  return;
227
282
  }
228
283
 
229
284
  const result = await executeSlashTool(slash, {
230
285
  productDir,
231
- inspectState,
286
+ inspectState: state.inspectState,
232
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,
233
296
  });
234
297
  appendMessage({
235
298
  role: "tool",
@@ -242,33 +305,56 @@ async function executeSlashCommand({
242
305
 
243
306
  async function executeSlashTool(slash, context) {
244
307
  switch (slash.type) {
245
- case "file":
246
- return executeAssistantTool("focus_file", { file: slash.file }, context);
247
308
  case "inspect":
248
- return slash.file
249
- ? executeAssistantTool("focus_file", { file: slash.file }, context)
250
- : executeAssistantTool("inspect_focus", {}, context);
309
+ return executeAssistantTool(
310
+ "read_context",
311
+ { file: slash.file || null, mode: "detail" },
312
+ context
313
+ );
251
314
  case "logs":
252
- return executeAssistantTool("read_logs", { service: slash.service || null }, context);
315
+ return executeAssistantTool("read_context", { service: slash.service || null, mode: "logs" }, context);
253
316
  case "artifacts":
254
- return executeAssistantTool("read_artifacts", { file: slash.file || null }, context);
317
+ return executeAssistantTool("read_context", { file: slash.file || null, mode: "artifacts" }, context);
255
318
  case "setup":
256
- 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);
257
322
  case "service":
258
- return executeAssistantTool("focus_service", { service: slash.service }, context);
323
+ return executeAssistantTool("read_context", { service: slash.service, mode: "detail" }, context);
259
324
  case "status":
260
- return executeAssistantTool("show_status", {}, context);
325
+ return executeAssistantTool("shell_exec", { command: "testkit status --dir ." }, context);
261
326
  case "discover":
262
- return executeAssistantTool("discover_tests", {}, context);
327
+ return executeAssistantTool("shell_exec", { command: "testkit discover --dir ." }, context);
263
328
  case "doctor":
264
- return executeAssistantTool("run_doctor", {}, context);
329
+ return executeAssistantTool("shell_exec", { command: "testkit doctor --dir ." }, context);
265
330
  case "run":
266
- return executeAssistantTool("run_tests", slash.options, context);
331
+ return executeAssistantTool("shell_exec", { command: buildRunSlashCommand(slash.options) }, context);
267
332
  default:
268
333
  throw new Error(`Unsupported slash command "${slash.type}"`);
269
334
  }
270
335
  }
271
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
+
272
358
  function parseSlashCommandSafe(input) {
273
359
  try {
274
360
  return parseSlashCommand(input);