@elench/testkit 0.1.88 → 0.1.90
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 +19 -8
- package/lib/cli/agents/index.mjs +48 -9
- package/lib/cli/agents/providers/claude.mjs +3 -3
- package/lib/cli/agents/providers/codex.mjs +3 -3
- package/lib/cli/assistant/bootstrap.mjs +248 -0
- package/lib/cli/assistant/interactive.mjs +52 -0
- package/lib/cli/assistant/prompt-builder.mjs +9 -9
- package/lib/cli/assistant/session.mjs +4 -1
- package/lib/cli/assistant/slash-commands.mjs +24 -10
- package/lib/cli/assistant/state.mjs +15 -13
- package/lib/cli/assistant/tool-registry.mjs +116 -37
- package/lib/cli/commands/assistant.mjs +44 -41
- package/lib/cli/{tui/detail-pane.mjs → context-resources.mjs} +81 -21
- package/lib/cli/entrypoint.mjs +12 -5
- 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/tui/assistant-app.mjs +0 -131
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadCurrentRunArtifact, loadLatestRunArtifact } from "../viewer.mjs";
|
|
2
2
|
import { createInspectState } from "../tui/inspect-state.mjs";
|
|
3
|
+
import { buildContextSelection } from "../context-resources.mjs";
|
|
3
4
|
import { parseSlashCommand, formatSlashHelpLines } from "./slash-commands.mjs";
|
|
4
5
|
import { executeAssistantTool } from "./tool-registry.mjs";
|
|
5
6
|
import { runAssistantConversationTurn } from "./session.mjs";
|
|
@@ -7,12 +8,11 @@ import { runAssistantConversationTurn } from "./session.mjs";
|
|
|
7
8
|
export function createAssistantState({
|
|
8
9
|
productDir,
|
|
9
10
|
provider = "auto",
|
|
10
|
-
initialPane = "detail",
|
|
11
11
|
dataSource = "artifact",
|
|
12
12
|
configs = [],
|
|
13
|
+
env = process.env,
|
|
13
14
|
} = {}) {
|
|
14
15
|
const inspectState = createInspectState({ dataSource });
|
|
15
|
-
inspectState.setPaneMode(initialPane);
|
|
16
16
|
|
|
17
17
|
const listeners = new Set();
|
|
18
18
|
const messages = [];
|
|
@@ -69,10 +69,6 @@ export function createAssistantState({
|
|
|
69
69
|
return inspectState.revealService(serviceName);
|
|
70
70
|
},
|
|
71
71
|
|
|
72
|
-
setPaneMode(pane) {
|
|
73
|
-
inspectState.setPaneMode(pane);
|
|
74
|
-
},
|
|
75
|
-
|
|
76
72
|
setComposer(value) {
|
|
77
73
|
composer = String(value || "");
|
|
78
74
|
notify();
|
|
@@ -165,6 +161,7 @@ export function createAssistantState({
|
|
|
165
161
|
transcript: messages.map((entry) => ({ role: entry.role, text: entry.text })),
|
|
166
162
|
userMessage: trimmed,
|
|
167
163
|
provider: providerName,
|
|
164
|
+
env,
|
|
168
165
|
configs,
|
|
169
166
|
onStatus(status) {
|
|
170
167
|
activeStatus = status;
|
|
@@ -189,7 +186,7 @@ export function createAssistantState({
|
|
|
189
186
|
|
|
190
187
|
getSnapshot() {
|
|
191
188
|
return {
|
|
192
|
-
|
|
189
|
+
context: buildContextSelection(inspectState.getSnapshot()),
|
|
193
190
|
messages: [...messages],
|
|
194
191
|
composer,
|
|
195
192
|
notice,
|
|
@@ -237,6 +234,7 @@ async function executeSlashCommand({
|
|
|
237
234
|
appendMessage({
|
|
238
235
|
role: "tool",
|
|
239
236
|
toolName: slash.type,
|
|
237
|
+
title: result.title || slash.type,
|
|
240
238
|
text: result.text || result.title || "Done.",
|
|
241
239
|
data: result.data || null,
|
|
242
240
|
});
|
|
@@ -244,16 +242,20 @@ async function executeSlashCommand({
|
|
|
244
242
|
|
|
245
243
|
async function executeSlashTool(slash, context) {
|
|
246
244
|
switch (slash.type) {
|
|
247
|
-
case "pane":
|
|
248
|
-
return executeAssistantTool("set_pane", { pane: slash.pane }, context);
|
|
249
245
|
case "file":
|
|
250
|
-
return executeAssistantTool("
|
|
246
|
+
return executeAssistantTool("focus_file", { file: slash.file }, context);
|
|
251
247
|
case "inspect":
|
|
252
248
|
return slash.file
|
|
253
|
-
? executeAssistantTool("
|
|
254
|
-
: executeAssistantTool("
|
|
249
|
+
? executeAssistantTool("focus_file", { file: slash.file }, context)
|
|
250
|
+
: executeAssistantTool("inspect_focus", {}, context);
|
|
251
|
+
case "logs":
|
|
252
|
+
return executeAssistantTool("read_logs", { service: slash.service || null }, context);
|
|
253
|
+
case "artifacts":
|
|
254
|
+
return executeAssistantTool("read_artifacts", { file: slash.file || null }, context);
|
|
255
|
+
case "setup":
|
|
256
|
+
return executeAssistantTool("read_setup", { service: slash.service || null }, context);
|
|
255
257
|
case "service":
|
|
256
|
-
return executeAssistantTool("
|
|
258
|
+
return executeAssistantTool("focus_service", { service: slash.service }, context);
|
|
257
259
|
case "status":
|
|
258
260
|
return executeAssistantTool("show_status", {}, context);
|
|
259
261
|
case "discover":
|
|
@@ -4,7 +4,10 @@ import { resolveProductDir } from "../../config/index.mjs";
|
|
|
4
4
|
import { resolveRequestedFiles } from "../args.mjs";
|
|
5
5
|
import { buildDiscoveryReportLines } from "../presentation/discovery-reporter.mjs";
|
|
6
6
|
import { loadCurrentRunArtifact, loadLatestRunArtifact, resolveFileSubject } from "../viewer.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
formatContextToolText,
|
|
9
|
+
readContextContent,
|
|
10
|
+
} from "../context-resources.mjs";
|
|
8
11
|
import { createAssistantRunReporter } from "./tool-run-reporter.mjs";
|
|
9
12
|
import { buildRunRequest } from "../command-helpers.mjs";
|
|
10
13
|
import * as runner from "../../runner/index.mjs";
|
|
@@ -12,10 +15,12 @@ import * as runner from "../../runner/index.mjs";
|
|
|
12
15
|
export function listAssistantTools() {
|
|
13
16
|
return [
|
|
14
17
|
{ name: "run_tests", description: "Run testkit-managed tests with optional type/service/file filters." },
|
|
15
|
-
{ name: "
|
|
16
|
-
{ name: "
|
|
17
|
-
{ name: "
|
|
18
|
-
{ name: "
|
|
18
|
+
{ name: "focus_file", description: "Focus a specific test file and summarize it." },
|
|
19
|
+
{ name: "focus_service", description: "Focus a specific service and summarize it." },
|
|
20
|
+
{ name: "inspect_focus", description: "Inspect the current focus inline in the conversation." },
|
|
21
|
+
{ name: "read_logs", description: "Read backend logs for the current focus or an explicit service." },
|
|
22
|
+
{ name: "read_artifacts", description: "Read persisted artifacts for the current focus or an explicit file." },
|
|
23
|
+
{ name: "read_setup", description: "Read setup operations for the current focus or an explicit service." },
|
|
19
24
|
{ name: "discover_tests", description: "Discover managed tests and summarize them." },
|
|
20
25
|
{ name: "show_status", description: "Show the current local testkit state for the product." },
|
|
21
26
|
{ name: "run_doctor", description: "Run built-in testkit doctor checks." },
|
|
@@ -27,14 +32,18 @@ export async function executeAssistantTool(name, argumentsObject, context) {
|
|
|
27
32
|
switch (name) {
|
|
28
33
|
case "run_tests":
|
|
29
34
|
return runTestsTool(args, context);
|
|
30
|
-
case "
|
|
31
|
-
return
|
|
32
|
-
case "
|
|
33
|
-
return
|
|
34
|
-
case "
|
|
35
|
-
return
|
|
36
|
-
case "
|
|
37
|
-
return
|
|
35
|
+
case "focus_file":
|
|
36
|
+
return focusFileTool(args, context);
|
|
37
|
+
case "focus_service":
|
|
38
|
+
return focusServiceTool(args, context);
|
|
39
|
+
case "inspect_focus":
|
|
40
|
+
return inspectFocusTool(args, context);
|
|
41
|
+
case "read_logs":
|
|
42
|
+
return readLogsTool(args, context);
|
|
43
|
+
case "read_artifacts":
|
|
44
|
+
return readArtifactsTool(args, context);
|
|
45
|
+
case "read_setup":
|
|
46
|
+
return readSetupTool(args, context);
|
|
38
47
|
case "discover_tests":
|
|
39
48
|
return discoverTestsTool(args, context);
|
|
40
49
|
case "show_status":
|
|
@@ -101,50 +110,120 @@ async function runTestsTool(args, context) {
|
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
112
|
|
|
104
|
-
function
|
|
113
|
+
function focusFileTool(args, context) {
|
|
105
114
|
const file = args.file || args.path || null;
|
|
106
|
-
if (!file) throw new Error("
|
|
115
|
+
if (!file) throw new Error("focus_file requires a file argument");
|
|
107
116
|
ensureArtifactLoaded(context);
|
|
108
117
|
const artifact = context.inspectState.getSnapshot().runArtifact;
|
|
109
118
|
const subject = resolveFileSubject(artifact, file, args.service || null);
|
|
110
119
|
context.inspectState.revealFile(subject.service.name, subject.file.path);
|
|
111
|
-
return
|
|
120
|
+
return inspectFocusTool({}, context);
|
|
112
121
|
}
|
|
113
122
|
|
|
114
|
-
function
|
|
123
|
+
function focusServiceTool(args, context) {
|
|
115
124
|
const service = args.service || args.name || null;
|
|
116
|
-
if (!service) throw new Error("
|
|
125
|
+
if (!service) throw new Error("focus_service requires a service argument");
|
|
117
126
|
ensureArtifactLoaded(context);
|
|
118
127
|
if (!context.inspectState.revealService(service)) {
|
|
119
128
|
throw new Error(`Unknown service "${service}"`);
|
|
120
129
|
}
|
|
121
|
-
return
|
|
130
|
+
return inspectFocusTool({}, context);
|
|
122
131
|
}
|
|
123
132
|
|
|
124
|
-
function
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
function inspectFocusTool(_args, context) {
|
|
134
|
+
const content = readContextContent({
|
|
135
|
+
productDir: context.productDir,
|
|
136
|
+
snapshot: context.inspectState.getSnapshot(),
|
|
137
|
+
mode: "detail",
|
|
138
|
+
logTail: 12,
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
title: content.title,
|
|
143
|
+
text: formatContextToolText(content.title, content.lines),
|
|
144
|
+
data: {
|
|
145
|
+
title: content.title,
|
|
146
|
+
lines: content.lines,
|
|
147
|
+
selection: content.selection,
|
|
148
|
+
mode: "detail",
|
|
149
|
+
},
|
|
150
|
+
};
|
|
128
151
|
}
|
|
129
152
|
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
153
|
+
function readLogsTool(args, context) {
|
|
154
|
+
if (args.service) {
|
|
155
|
+
ensureArtifactLoaded(context);
|
|
156
|
+
if (!context.inspectState.revealService(args.service)) {
|
|
157
|
+
throw new Error(`Unknown service "${args.service}"`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const content = readContextContent({
|
|
161
|
+
productDir: context.productDir,
|
|
162
|
+
snapshot: context.inspectState.getSnapshot(),
|
|
163
|
+
mode: "logs",
|
|
164
|
+
logTail: args.logTail == null ? 12 : Number(args.logTail),
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
ok: true,
|
|
168
|
+
title: content.title,
|
|
169
|
+
text: formatContextToolText(content.title, content.lines),
|
|
170
|
+
data: {
|
|
171
|
+
title: content.title,
|
|
172
|
+
lines: content.lines,
|
|
173
|
+
selection: content.selection,
|
|
174
|
+
mode: "logs",
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function readArtifactsTool(args, context) {
|
|
180
|
+
if (args.file || args.path) {
|
|
181
|
+
ensureArtifactLoaded(context);
|
|
182
|
+
const artifact = context.inspectState.getSnapshot().runArtifact;
|
|
183
|
+
const subject = resolveFileSubject(artifact, args.file || args.path, args.service || null);
|
|
184
|
+
context.inspectState.revealFile(subject.service.name, subject.file.path);
|
|
185
|
+
}
|
|
186
|
+
const content = readContextContent({
|
|
134
187
|
productDir: context.productDir,
|
|
135
|
-
snapshot,
|
|
136
|
-
|
|
188
|
+
snapshot: context.inspectState.getSnapshot(),
|
|
189
|
+
mode: "artifacts",
|
|
137
190
|
logTail: 12,
|
|
138
191
|
});
|
|
139
192
|
return {
|
|
140
193
|
ok: true,
|
|
141
|
-
title:
|
|
142
|
-
text:
|
|
194
|
+
title: content.title,
|
|
195
|
+
text: formatContextToolText(content.title, content.lines),
|
|
143
196
|
data: {
|
|
144
|
-
title:
|
|
145
|
-
lines:
|
|
146
|
-
selection:
|
|
147
|
-
|
|
197
|
+
title: content.title,
|
|
198
|
+
lines: content.lines,
|
|
199
|
+
selection: content.selection,
|
|
200
|
+
mode: "artifacts",
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function readSetupTool(args, context) {
|
|
206
|
+
if (args.service) {
|
|
207
|
+
ensureArtifactLoaded(context);
|
|
208
|
+
if (!context.inspectState.revealService(args.service)) {
|
|
209
|
+
throw new Error(`Unknown service "${args.service}"`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const content = readContextContent({
|
|
213
|
+
productDir: context.productDir,
|
|
214
|
+
snapshot: context.inspectState.getSnapshot(),
|
|
215
|
+
mode: "setup",
|
|
216
|
+
logTail: 12,
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
ok: true,
|
|
220
|
+
title: content.title,
|
|
221
|
+
text: formatContextToolText(content.title, content.lines),
|
|
222
|
+
data: {
|
|
223
|
+
title: content.title,
|
|
224
|
+
lines: content.lines,
|
|
225
|
+
selection: content.selection,
|
|
226
|
+
mode: "setup",
|
|
148
227
|
},
|
|
149
228
|
};
|
|
150
229
|
}
|
|
@@ -176,7 +255,7 @@ function showStatusTool(_args, context) {
|
|
|
176
255
|
return {
|
|
177
256
|
ok: true,
|
|
178
257
|
title: "Status",
|
|
179
|
-
text:
|
|
258
|
+
text: formatContextToolText("Status", lines),
|
|
180
259
|
data: { lines },
|
|
181
260
|
};
|
|
182
261
|
}
|
|
@@ -193,7 +272,7 @@ async function runDoctorTool(args, context) {
|
|
|
193
272
|
return {
|
|
194
273
|
ok: result.ok,
|
|
195
274
|
title: "Doctor",
|
|
196
|
-
text:
|
|
275
|
+
text: formatContextToolText("Doctor", lines),
|
|
197
276
|
data: result,
|
|
198
277
|
};
|
|
199
278
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import React, { createElement } from "react";
|
|
2
1
|
import { Command, Flags } from "@oclif/core";
|
|
3
|
-
import { render } from "ink";
|
|
4
2
|
import { sharedFlags, resolveConfigsForCommand } from "../command-helpers.mjs";
|
|
5
3
|
import { createAssistantState } from "../assistant/state.mjs";
|
|
6
|
-
import {
|
|
4
|
+
import { runInteractiveAssistant } from "../assistant/interactive.mjs";
|
|
7
5
|
|
|
8
6
|
export default class AssistantCommand extends Command {
|
|
9
|
-
static summary = "Launch
|
|
7
|
+
static summary = "Launch a native Codex or Claude session with testkit context";
|
|
10
8
|
|
|
11
9
|
static enableJsonFlag = true;
|
|
12
10
|
|
|
@@ -17,14 +15,12 @@ export default class AssistantCommand extends Command {
|
|
|
17
15
|
options: ["auto", "claude", "codex"],
|
|
18
16
|
default: "auto",
|
|
19
17
|
}),
|
|
20
|
-
pane: Flags.string({
|
|
21
|
-
description: "Initial workbench pane",
|
|
22
|
-
options: ["detail", "artifacts", "logs", "setup"],
|
|
23
|
-
default: "detail",
|
|
24
|
-
}),
|
|
25
18
|
file: Flags.string({
|
|
26
19
|
description: "Initial file selection",
|
|
27
20
|
}),
|
|
21
|
+
prompt: Flags.string({
|
|
22
|
+
description: "Initial interactive prompt for the provider session",
|
|
23
|
+
}),
|
|
28
24
|
message: Flags.string({
|
|
29
25
|
description: "Run one assistant turn non-interactively",
|
|
30
26
|
}),
|
|
@@ -32,30 +28,41 @@ export default class AssistantCommand extends Command {
|
|
|
32
28
|
|
|
33
29
|
async run() {
|
|
34
30
|
const { flags } = await this.parse(AssistantCommand);
|
|
31
|
+
if (flags.message && flags.prompt) {
|
|
32
|
+
this.error("Use either --message or --prompt, not both.");
|
|
33
|
+
}
|
|
35
34
|
const { allConfigs } = await resolveConfigsForCommand(flags);
|
|
36
35
|
const productDir = allConfigs[0]?.productDir || process.cwd();
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
await assistantState.loadLatestArtifact();
|
|
45
|
-
if (flags.file) {
|
|
46
|
-
try {
|
|
47
|
-
const artifact = loadLatestRunArtifact(productDir);
|
|
48
|
-
const subject = resolveFileSubject(artifact, flags.file, flags.service || null);
|
|
49
|
-
assistantState.revealFile(subject.service.name, subject.file.path);
|
|
50
|
-
} catch {
|
|
51
|
-
// Ignore missing initial selection.
|
|
52
|
-
}
|
|
53
|
-
} else if (flags.service) {
|
|
54
|
-
assistantState.revealService(flags.service);
|
|
36
|
+
const interactive =
|
|
37
|
+
(process.stdout.isTTY || process.env.TESTKIT_FORCE_INTERACTIVE_ASSISTANT === "1") &&
|
|
38
|
+
!this.jsonEnabled() &&
|
|
39
|
+
!flags.message;
|
|
40
|
+
if (flags.prompt && !interactive) {
|
|
41
|
+
this.error("--prompt requires an interactive assistant session.");
|
|
55
42
|
}
|
|
56
|
-
|
|
57
|
-
const interactive = process.stdout.isTTY && !this.jsonEnabled() && !flags.message;
|
|
58
43
|
if (!interactive) {
|
|
44
|
+
if (!flags.message) {
|
|
45
|
+
this.error("assistant requires an interactive tty; use --message for one non-interactive turn.");
|
|
46
|
+
}
|
|
47
|
+
const assistantState = createAssistantState({
|
|
48
|
+
productDir,
|
|
49
|
+
provider: flags.provider,
|
|
50
|
+
configs: allConfigs,
|
|
51
|
+
env: process.env,
|
|
52
|
+
});
|
|
53
|
+
await assistantState.loadLatestArtifact();
|
|
54
|
+
if (flags.file) {
|
|
55
|
+
try {
|
|
56
|
+
const { loadLatestRunArtifact, resolveFileSubject } = await import("../viewer.mjs");
|
|
57
|
+
const artifact = loadLatestRunArtifact(productDir);
|
|
58
|
+
const subject = resolveFileSubject(artifact, flags.file, flags.service || null);
|
|
59
|
+
assistantState.revealFile(subject.service.name, subject.file.path);
|
|
60
|
+
} catch {
|
|
61
|
+
// Ignore missing initial selection.
|
|
62
|
+
}
|
|
63
|
+
} else if (flags.service) {
|
|
64
|
+
assistantState.revealService(flags.service);
|
|
65
|
+
}
|
|
59
66
|
if (flags.message) {
|
|
60
67
|
await assistantState.submitInput(flags.message);
|
|
61
68
|
}
|
|
@@ -68,17 +75,13 @@ export default class AssistantCommand extends Command {
|
|
|
68
75
|
return snapshot;
|
|
69
76
|
}
|
|
70
77
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
await app.waitUntilExit();
|
|
82
|
-
return assistantState.getSnapshot();
|
|
78
|
+
return runInteractiveAssistant({
|
|
79
|
+
productDir,
|
|
80
|
+
provider: flags.provider,
|
|
81
|
+
file: flags.file || null,
|
|
82
|
+
service: flags.service || null,
|
|
83
|
+
prompt: flags.prompt || null,
|
|
84
|
+
env: process.env,
|
|
85
|
+
});
|
|
83
86
|
}
|
|
84
87
|
}
|
|
@@ -7,32 +7,87 @@ import {
|
|
|
7
7
|
getSetupOperationsForService,
|
|
8
8
|
loadCurrentRunArtifact,
|
|
9
9
|
resolveFileSubject,
|
|
10
|
-
} from "
|
|
11
|
-
import { formatDuration } from "
|
|
12
|
-
import { readLogTail } from "
|
|
10
|
+
} from "./viewer.mjs";
|
|
11
|
+
import { formatDuration } from "../runner/formatting.mjs";
|
|
12
|
+
import { readLogTail } from "../runner/logs.mjs";
|
|
13
13
|
|
|
14
|
-
export function
|
|
15
|
-
|
|
14
|
+
export function readContextContent({
|
|
15
|
+
productDir,
|
|
16
|
+
snapshot,
|
|
17
|
+
mode = "detail",
|
|
18
|
+
logTail = 12,
|
|
19
|
+
previewLength = 6,
|
|
20
|
+
} = {}) {
|
|
21
|
+
const normalizedMode = normalizeMode(mode);
|
|
22
|
+
const selectedEntry = snapshot?.selectedEntry || null;
|
|
23
|
+
const summaryRows = snapshot?.summaryData?.rows || [];
|
|
16
24
|
if (!selectedEntry) {
|
|
17
|
-
return {
|
|
25
|
+
return {
|
|
26
|
+
mode: normalizedMode,
|
|
27
|
+
title: "Selection",
|
|
28
|
+
lines: ["No entry selected."],
|
|
29
|
+
data: null,
|
|
30
|
+
selection: null,
|
|
31
|
+
summaryRows,
|
|
32
|
+
};
|
|
18
33
|
}
|
|
19
34
|
|
|
20
35
|
const runArtifact = resolveArtifact(productDir, snapshot);
|
|
21
36
|
const subject = resolveSubject(runArtifact, selectedEntry);
|
|
22
37
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
let content = null;
|
|
39
|
+
if (normalizedMode === "artifacts") {
|
|
40
|
+
content = buildArtifactsContent(productDir, runArtifact, selectedEntry, subject, previewLength);
|
|
41
|
+
} else if (normalizedMode === "logs") {
|
|
42
|
+
content = buildLogsContent(productDir, runArtifact, selectedEntry, logTail);
|
|
43
|
+
} else if (normalizedMode === "setup") {
|
|
44
|
+
content = buildSetupContent(runArtifact, selectedEntry);
|
|
45
|
+
} else {
|
|
46
|
+
content = buildDetailContent(productDir, runArtifact, selectedEntry, subject, logTail);
|
|
31
47
|
}
|
|
32
|
-
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
mode: normalizedMode,
|
|
51
|
+
title: content.title,
|
|
52
|
+
lines: content.lines || [],
|
|
53
|
+
data: content.data ?? null,
|
|
54
|
+
selection: selectedEntry,
|
|
55
|
+
summaryRows,
|
|
56
|
+
};
|
|
33
57
|
}
|
|
34
58
|
|
|
35
|
-
function
|
|
59
|
+
export function buildContextSelection(snapshot) {
|
|
60
|
+
const selectedEntry = snapshot?.selectedEntry || null;
|
|
61
|
+
const summaryRows = snapshot?.summaryData?.rows || [];
|
|
62
|
+
return {
|
|
63
|
+
selection: selectedEntry
|
|
64
|
+
? {
|
|
65
|
+
kind: selectedEntry.kind,
|
|
66
|
+
label: selectedEntry.label || selectedEntry.filePath || selectedEntry.serviceName || "selection",
|
|
67
|
+
serviceName: selectedEntry.serviceName || null,
|
|
68
|
+
type: selectedEntry.type || null,
|
|
69
|
+
suiteName: selectedEntry.suiteName || null,
|
|
70
|
+
filePath: selectedEntry.filePath || null,
|
|
71
|
+
status: selectedEntry.status || null,
|
|
72
|
+
}
|
|
73
|
+
: null,
|
|
74
|
+
summaryRows,
|
|
75
|
+
phase: snapshot?.phase || null,
|
|
76
|
+
hasArtifact: Boolean(snapshot?.runArtifact),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function formatContextToolText(title, lines) {
|
|
81
|
+
const normalizedTitle = String(title || "").trim();
|
|
82
|
+
const normalizedLines = Array.isArray(lines) ? lines.filter((line) => String(line).length > 0) : [];
|
|
83
|
+
if (normalizedTitle && normalizedLines.length > 0) {
|
|
84
|
+
return [normalizedTitle, ...normalizedLines].join("\n");
|
|
85
|
+
}
|
|
86
|
+
if (normalizedTitle) return normalizedTitle;
|
|
87
|
+
return normalizedLines.join("\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildDetailContent(productDir, runArtifact, entry, subject, logTail) {
|
|
36
91
|
if (subject && runArtifact) {
|
|
37
92
|
return {
|
|
38
93
|
title: "Detail",
|
|
@@ -48,7 +103,7 @@ function buildDetailPane(productDir, runArtifact, entry, subject, logTail) {
|
|
|
48
103
|
};
|
|
49
104
|
}
|
|
50
105
|
|
|
51
|
-
function
|
|
106
|
+
function buildArtifactsContent(productDir, runArtifact, entry, subject, previewLength) {
|
|
52
107
|
if (!runArtifact || !subject) {
|
|
53
108
|
return {
|
|
54
109
|
title: "Artifacts",
|
|
@@ -65,7 +120,7 @@ function buildArtifactsPane(productDir, runArtifact, entry, subject) {
|
|
|
65
120
|
kind: item.artifactRef.kind,
|
|
66
121
|
summary: item.artifactRef.summary,
|
|
67
122
|
path: item.artifactRef.path,
|
|
68
|
-
preview: formatArtifactPreview(item.payload,
|
|
123
|
+
preview: formatArtifactPreview(item.payload, previewLength),
|
|
69
124
|
}));
|
|
70
125
|
|
|
71
126
|
if (entries.length === 0) {
|
|
@@ -82,7 +137,7 @@ function buildArtifactsPane(productDir, runArtifact, entry, subject) {
|
|
|
82
137
|
return { title: "Artifacts", lines, data: entries };
|
|
83
138
|
}
|
|
84
139
|
|
|
85
|
-
function
|
|
140
|
+
function buildLogsContent(productDir, runArtifact, entry, tail) {
|
|
86
141
|
if (!runArtifact) {
|
|
87
142
|
return { title: "Logs", lines: ["Backend logs are available only from persisted run artifacts."], data: [] };
|
|
88
143
|
}
|
|
@@ -103,7 +158,7 @@ function buildLogsPane(productDir, runArtifact, entry, tail) {
|
|
|
103
158
|
return { title: "Logs", lines, data: logs };
|
|
104
159
|
}
|
|
105
160
|
|
|
106
|
-
function
|
|
161
|
+
function buildSetupContent(runArtifact, entry) {
|
|
107
162
|
if (!runArtifact) {
|
|
108
163
|
return { title: "Setup", lines: ["Setup operations are available only from persisted run artifacts."], data: [] };
|
|
109
164
|
}
|
|
@@ -123,7 +178,7 @@ function buildSetupPane(productDir, runArtifact, entry) {
|
|
|
123
178
|
}
|
|
124
179
|
|
|
125
180
|
function resolveArtifact(productDir, snapshot) {
|
|
126
|
-
if (snapshot
|
|
181
|
+
if (snapshot?.runArtifact) return snapshot.runArtifact;
|
|
127
182
|
try {
|
|
128
183
|
return loadCurrentRunArtifact(productDir);
|
|
129
184
|
} catch {
|
|
@@ -159,3 +214,8 @@ function formatAggregateDetail(entry) {
|
|
|
159
214
|
if (entry.error) lines.push(`Error: ${entry.error}`);
|
|
160
215
|
return lines;
|
|
161
216
|
}
|
|
217
|
+
|
|
218
|
+
function normalizeMode(mode) {
|
|
219
|
+
if (mode === "logs" || mode === "artifacts" || mode === "setup") return mode;
|
|
220
|
+
return "detail";
|
|
221
|
+
}
|
package/lib/cli/entrypoint.mjs
CHANGED
|
@@ -36,11 +36,13 @@ export function normalizeCliArgs(argv) {
|
|
|
36
36
|
"--log-tail",
|
|
37
37
|
"--provider",
|
|
38
38
|
"--message",
|
|
39
|
-
"--
|
|
39
|
+
"--prompt",
|
|
40
40
|
]);
|
|
41
41
|
const positionals = findPositionals(argv, valueFlags);
|
|
42
42
|
const firstPositional = positionals[0] || null;
|
|
43
|
-
const
|
|
43
|
+
const forcedInteractiveAssistant = process.env.TESTKIT_FORCE_INTERACTIVE_ASSISTANT === "1";
|
|
44
|
+
const interactiveTty = process.stdout.isTTY || forcedInteractiveAssistant;
|
|
45
|
+
const assistantDefaultDisabled = process.env.TESTKIT_NO_ASSISTANT_DEFAULT === "1";
|
|
44
46
|
const runFlagPresent = argv.some((value) =>
|
|
45
47
|
[
|
|
46
48
|
"--type",
|
|
@@ -66,7 +68,7 @@ export function normalizeCliArgs(argv) {
|
|
|
66
68
|
runFlagPresent ||
|
|
67
69
|
!topLevelCommands.has(firstPositional?.value);
|
|
68
70
|
|
|
69
|
-
if (!firstPositional && interactiveTty && !runFlagPresent) {
|
|
71
|
+
if (!firstPositional && interactiveTty && !runFlagPresent && !assistantDefaultDisabled) {
|
|
70
72
|
return ["assistant", ...argv];
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -74,8 +76,13 @@ export function normalizeCliArgs(argv) {
|
|
|
74
76
|
return reorderCommandArgs(argv, positionals);
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
if (!topLevelCommands.has(firstPositional?.value) && interactiveTty && !
|
|
78
|
-
|
|
79
|
+
if (!topLevelCommands.has(firstPositional?.value) && interactiveTty && !assistantDefaultDisabled) {
|
|
80
|
+
const shouldOpenAssistantPrompt =
|
|
81
|
+
forcedInteractiveAssistant ||
|
|
82
|
+
(!runFlagPresent && !runTypeShortcuts.has(firstPositional?.value));
|
|
83
|
+
if (shouldOpenAssistantPrompt) {
|
|
84
|
+
return ["assistant", "--prompt", argv.join(" ")];
|
|
85
|
+
}
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
if (shouldPrefixRun) {
|