@elench/testkit 0.1.96 → 0.1.98
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/lib/app/browser-bridge.mjs +1 -1
- package/lib/cli/assistant/app.mjs +49 -12
- package/lib/cli/assistant/composer.mjs +19 -1
- package/lib/cli/assistant/context-pack.mjs +9 -8
- package/lib/cli/assistant/interactive.mjs +1 -1
- package/lib/cli/assistant/model-discovery.mjs +243 -0
- package/lib/cli/assistant/prompt-builder.mjs +2 -5
- package/lib/cli/{agents → assistant}/providers/claude.mjs +41 -3
- package/lib/cli/{agents → assistant}/providers/codex.mjs +33 -14
- package/lib/cli/{agents → assistant/providers}/index.mjs +3 -3
- package/lib/cli/{agents → assistant}/providers/shared.mjs +6 -2
- package/lib/cli/assistant/session.mjs +31 -6
- package/lib/cli/assistant/slash-commands.mjs +30 -3
- package/lib/cli/assistant/state.mjs +237 -71
- package/lib/cli/assistant/tool-registry.mjs +325 -39
- package/lib/cli/assistant/view-model.mjs +1 -1
- package/lib/cli/commands/assistant.mjs +4 -3
- package/lib/cli/commands/browser/serve.mjs +5 -23
- package/lib/cli/commands/cleanup.mjs +8 -2
- package/lib/cli/commands/db/snapshot/capture.mjs +8 -4
- package/lib/cli/commands/destroy.mjs +8 -2
- package/lib/cli/commands/discover.mjs +5 -27
- package/lib/cli/commands/doctor.mjs +5 -5
- package/lib/cli/commands/flags.mjs +61 -0
- package/lib/cli/commands/run.mjs +10 -2
- package/lib/cli/commands/status.mjs +10 -2
- package/lib/cli/commands/typecheck.mjs +5 -5
- package/lib/cli/{tui/inspect-app.mjs → components/blocks/run-tree.mjs} +29 -54
- package/lib/cli/{tui → components/primitives}/filter-bar.mjs +1 -1
- package/lib/cli/{presentation → components/primitives}/summary-box.mjs +1 -1
- package/lib/cli/config.mjs +63 -0
- package/lib/cli/operations/browser/serve/operation.mjs +23 -0
- package/lib/cli/operations/cleanup/operation.mjs +8 -0
- package/lib/cli/{db.mjs → operations/db/snapshot/capture/operation.mjs} +15 -9
- package/lib/cli/operations/destroy/operation.mjs +12 -0
- package/lib/cli/operations/discover/operation.mjs +32 -0
- package/lib/cli/operations/doctor/operation.mjs +5 -0
- package/lib/cli/operations/run/operation.mjs +129 -0
- package/lib/cli/operations/status/operation.mjs +7 -0
- package/lib/cli/operations/typecheck/operation.mjs +5 -0
- package/lib/cli/renderers/browser-serve/text.mjs +6 -0
- package/lib/cli/renderers/cleanup/text.mjs +3 -0
- package/lib/cli/renderers/db-snapshot-capture/text.mjs +3 -0
- package/lib/cli/renderers/destroy/text.mjs +3 -0
- package/lib/cli/{presentation/discovery-reporter.mjs → renderers/discover/report.mjs} +3 -3
- package/lib/cli/renderers/discover/text.mjs +7 -0
- package/lib/cli/renderers/doctor/text.mjs +7 -0
- package/lib/cli/{presentation/failure-presentation.mjs → renderers/run/failure.mjs} +6 -6
- package/lib/cli/renderers/run/interactive.mjs +119 -0
- package/lib/cli/{presentation/run-reporter.mjs → renderers/run/text-reporter.mjs} +5 -5
- package/lib/cli/renderers/status/text.mjs +7 -0
- package/lib/cli/renderers/typecheck/text.mjs +7 -0
- package/lib/cli/{tui/inspect-model.mjs → state/run/model.mjs} +11 -26
- package/lib/cli/{tui/inspect-state.mjs → state/run/state.mjs} +11 -18
- package/lib/cli/{tui → state/tree}/fuzzy-match.mjs +1 -1
- package/lib/cli/terminal/capabilities.mjs +33 -0
- package/lib/database/index.mjs +9 -21
- package/lib/database/template-steps.mjs +3 -3
- package/lib/{cli/viewer.mjs → results/artifacts.mjs} +1 -1
- package/lib/{cli/context-resources.mjs → results/context.mjs} +1 -1
- package/lib/runner/maintenance.mjs +25 -14
- package/lib/runner/readiness.mjs +5 -4
- package/lib/runner/runtime-preparation.mjs +36 -0
- package/lib/runner/state-io.mjs +10 -4
- package/lib/runner/template.mjs +24 -3
- 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/assistant/command-plan.mjs +0 -227
- package/lib/cli/command-helpers.mjs +0 -191
- package/lib/cli/presentation/tree-reporter.mjs +0 -96
- package/lib/cli/tui/inspect-artifact-adapter.mjs +0 -3
- package/lib/cli/tui/inspect-live-adapter.mjs +0 -15
- /package/lib/cli/{presentation/events-reporter.mjs → renderers/run/events.mjs} +0 -0
- /package/lib/cli/{presentation → terminal}/colors.mjs +0 -0
- /package/lib/cli/{presentation/terminal-layout.mjs → terminal/layout.mjs} +0 -0
- /package/lib/{cli/presentation → results}/code-frames.mjs +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { startProviderSession, resolvePreferredProvider } from "./providers/index.mjs";
|
|
2
2
|
import { buildAssistantPrompt } from "./prompt-builder.mjs";
|
|
3
3
|
import { listAssistantTools, executeAssistantTool } from "./tool-registry.mjs";
|
|
4
4
|
import { parseAssistantEnvelope } from "./protocol.mjs";
|
|
5
5
|
|
|
6
6
|
export async function runAssistantConversationTurn({
|
|
7
7
|
productDir,
|
|
8
|
-
|
|
8
|
+
runState,
|
|
9
9
|
transcript,
|
|
10
10
|
userMessage,
|
|
11
11
|
provider = "auto",
|
|
@@ -21,7 +21,7 @@ export async function runAssistantConversationTurn({
|
|
|
21
21
|
const tools = listAssistantTools();
|
|
22
22
|
const toolContext = {
|
|
23
23
|
productDir,
|
|
24
|
-
|
|
24
|
+
runState,
|
|
25
25
|
configs,
|
|
26
26
|
env,
|
|
27
27
|
commandLog,
|
|
@@ -32,7 +32,7 @@ export async function runAssistantConversationTurn({
|
|
|
32
32
|
const emitted = [];
|
|
33
33
|
|
|
34
34
|
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
35
|
-
const snapshot =
|
|
35
|
+
const snapshot = runState.getSnapshot();
|
|
36
36
|
const prompt = buildAssistantPrompt({
|
|
37
37
|
productDir,
|
|
38
38
|
snapshot,
|
|
@@ -52,7 +52,7 @@ export async function runAssistantConversationTurn({
|
|
|
52
52
|
});
|
|
53
53
|
onStatus?.(`Thinking with ${resolvedProvider}...`);
|
|
54
54
|
const events = [];
|
|
55
|
-
const session =
|
|
55
|
+
const session = startProviderSession({
|
|
56
56
|
provider: runtimeSettings.provider || provider,
|
|
57
57
|
model: runtimeSettings.model || null,
|
|
58
58
|
effort: runtimeSettings.effort || null,
|
|
@@ -74,7 +74,24 @@ export async function runAssistantConversationTurn({
|
|
|
74
74
|
emitted.push({ role: "assistant", text: envelope.commentary });
|
|
75
75
|
currentTranscript.push({ role: "assistant", text: envelope.commentary });
|
|
76
76
|
}
|
|
77
|
-
|
|
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
|
+
}
|
|
78
95
|
const toolText = toolResult.text || `${envelope.tool} completed`;
|
|
79
96
|
emitted.push({
|
|
80
97
|
role: "tool",
|
|
@@ -105,6 +122,14 @@ export async function runAssistantConversationTurn({
|
|
|
105
122
|
return emitted;
|
|
106
123
|
}
|
|
107
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.";
|
|
129
|
+
}
|
|
130
|
+
return `Tool failed: ${message}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
108
133
|
function formatProviderEvent(event) {
|
|
109
134
|
if (event.type === "tool") {
|
|
110
135
|
return `${event.provider}: ${event.name}${event.detail ? ` (${event.detail})` : ""}`;
|
|
@@ -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
|
|
|
@@ -23,8 +23,14 @@ export function parseSlashCommand(input) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
if (command === "model") {
|
|
26
|
+
if (tokens.length === 0) return { type: "model-list" };
|
|
27
|
+
if (tokens[0] === "list") return { type: "model-list" };
|
|
28
|
+
if (tokens[0] === "custom") {
|
|
29
|
+
const customModel = tokens.slice(1).join(" ").trim();
|
|
30
|
+
if (!customModel) throw new Error("/model custom expects a model name");
|
|
31
|
+
return { type: "model", model: customModel, custom: true };
|
|
32
|
+
}
|
|
26
33
|
const model = tokens.join(" ").trim();
|
|
27
|
-
if (!model) throw new Error("/model expects a model name or default");
|
|
28
34
|
return { type: "model", model: model === "default" ? null : model };
|
|
29
35
|
}
|
|
30
36
|
|
|
@@ -56,6 +62,17 @@ export function parseSlashCommand(input) {
|
|
|
56
62
|
throw new Error('/settings expects "show" or "reset"');
|
|
57
63
|
}
|
|
58
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
|
+
|
|
59
76
|
if (command === "file" || command === "focus") {
|
|
60
77
|
if (!tokens[0]) throw new Error(`/${command} expects a file path`);
|
|
61
78
|
return { type: "file", file: tokens.join(" ") };
|
|
@@ -121,18 +138,28 @@ export function formatSlashHelpLines() {
|
|
|
121
138
|
"/status",
|
|
122
139
|
"/doctor",
|
|
123
140
|
"/provider <auto|claude|codex>",
|
|
124
|
-
"/model <model
|
|
141
|
+
"/model [list|default|custom <model>|model]",
|
|
125
142
|
"/effort <low|medium|high|xhigh|max|default>",
|
|
126
143
|
"/provider-arg add <arg>",
|
|
127
144
|
"/provider-arg list",
|
|
128
145
|
"/provider-arg clear",
|
|
129
146
|
"/settings",
|
|
130
147
|
"/settings reset",
|
|
148
|
+
"/config",
|
|
149
|
+
"/config auto-collapse-passed <on|off>",
|
|
150
|
+
"/config reset",
|
|
131
151
|
"/clear",
|
|
132
152
|
"/quit",
|
|
133
153
|
];
|
|
134
154
|
}
|
|
135
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
|
+
|
|
136
163
|
function parseRunCommandTokens(tokens) {
|
|
137
164
|
const options = {
|
|
138
165
|
type: [],
|
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
import { loadCurrentRunArtifact, loadLatestRunArtifact } from "
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { loadCurrentRunArtifact, loadLatestRunArtifact } from "../../results/artifacts.mjs";
|
|
2
|
+
import {
|
|
3
|
+
formatCliConfig,
|
|
4
|
+
loadCliConfig,
|
|
5
|
+
mergeCliConfig,
|
|
6
|
+
resetCliConfig,
|
|
7
|
+
saveCliConfig,
|
|
8
|
+
} from "../config.mjs";
|
|
9
|
+
import { createRunState } from "../state/run/state.mjs";
|
|
10
|
+
import { buildContextSelection } from "../../results/context.mjs";
|
|
11
|
+
import { isProviderInstalled } from "./providers/index.mjs";
|
|
5
12
|
import { parseSlashCommand, formatSlashHelpLines } from "./slash-commands.mjs";
|
|
6
13
|
import { executeAssistantTool } from "./tool-registry.mjs";
|
|
7
14
|
import { runAssistantConversationTurn } from "./session.mjs";
|
|
8
15
|
import { prepareAssistantContextPack } from "./context-pack.mjs";
|
|
16
|
+
import {
|
|
17
|
+
discoverAssistantModels,
|
|
18
|
+
formatModelChoices,
|
|
19
|
+
getModelProviderMismatch,
|
|
20
|
+
} from "./model-discovery.mjs";
|
|
9
21
|
import {
|
|
10
22
|
DEFAULT_ASSISTANT_SETTINGS,
|
|
11
23
|
loadAssistantSettings,
|
|
@@ -36,10 +48,12 @@ export function createAssistantState({
|
|
|
36
48
|
configs = [],
|
|
37
49
|
env = process.env,
|
|
38
50
|
} = {}) {
|
|
39
|
-
const
|
|
51
|
+
const runState = createRunState({ dataSource });
|
|
52
|
+
let cliConfig = loadCliConfig(productDir);
|
|
53
|
+
runState.setAutoCollapsePassedTreeBranches(cliConfig.autoCollapsePassedTreeBranches);
|
|
40
54
|
const commandLog = prepareAssistantContextPack({
|
|
41
55
|
productDir,
|
|
42
|
-
|
|
56
|
+
runState,
|
|
43
57
|
});
|
|
44
58
|
|
|
45
59
|
const listeners = new Set();
|
|
@@ -57,14 +71,25 @@ export function createAssistantState({
|
|
|
57
71
|
}
|
|
58
72
|
);
|
|
59
73
|
let resolvedProviderName = resolveInitialProvider(settings.provider, env);
|
|
74
|
+
const sanitizedStartup = sanitizeSettingsForResolvedProvider({
|
|
75
|
+
productDir,
|
|
76
|
+
settings,
|
|
77
|
+
resolvedProvider: resolvedProviderName,
|
|
78
|
+
});
|
|
79
|
+
settings = sanitizedStartup.settings;
|
|
80
|
+
if (sanitizedStartup.notice) notice = sanitizedStartup.notice;
|
|
60
81
|
let activeStatus = null;
|
|
82
|
+
let startupNoticeEmitted = false;
|
|
61
83
|
let contextUsage = buildContextUsage({
|
|
62
84
|
provider: resolvedProviderName || settings.provider,
|
|
63
85
|
model: settings.model,
|
|
64
86
|
prompt: "",
|
|
65
87
|
});
|
|
88
|
+
let liveRunSession = null;
|
|
89
|
+
let lastRunSession = null;
|
|
90
|
+
let liveRunSessionUnsubscribe = null;
|
|
66
91
|
|
|
67
|
-
|
|
92
|
+
runState.subscribe(() => {
|
|
68
93
|
commandLog.refresh();
|
|
69
94
|
notify();
|
|
70
95
|
});
|
|
@@ -91,13 +116,46 @@ export function createAssistantState({
|
|
|
91
116
|
commandLog.refresh();
|
|
92
117
|
}
|
|
93
118
|
|
|
119
|
+
function attachRunSession(session, { active = true } = {}) {
|
|
120
|
+
if (liveRunSessionUnsubscribe) {
|
|
121
|
+
liveRunSessionUnsubscribe();
|
|
122
|
+
liveRunSessionUnsubscribe = null;
|
|
123
|
+
}
|
|
124
|
+
if (!session) {
|
|
125
|
+
if (!active) notify();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
lastRunSession = session;
|
|
129
|
+
if (active) {
|
|
130
|
+
liveRunSession = session;
|
|
131
|
+
liveRunSessionUnsubscribe = session.runState.subscribe(() => {
|
|
132
|
+
notify();
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
notify();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function completeRunSession(session) {
|
|
139
|
+
if (liveRunSession === session) {
|
|
140
|
+
if (liveRunSessionUnsubscribe) {
|
|
141
|
+
liveRunSessionUnsubscribe();
|
|
142
|
+
liveRunSessionUnsubscribe = null;
|
|
143
|
+
}
|
|
144
|
+
liveRunSession = null;
|
|
145
|
+
}
|
|
146
|
+
if (session) lastRunSession = session;
|
|
147
|
+
notify();
|
|
148
|
+
}
|
|
149
|
+
|
|
94
150
|
const state = {
|
|
95
|
-
|
|
151
|
+
runState,
|
|
96
152
|
commandLog,
|
|
153
|
+
attachRunSession,
|
|
154
|
+
completeRunSession,
|
|
97
155
|
|
|
98
156
|
async loadLatestArtifact() {
|
|
99
157
|
try {
|
|
100
|
-
|
|
158
|
+
runState.hydrateFromArtifact(loadLatestRunArtifact(productDir));
|
|
101
159
|
} catch {
|
|
102
160
|
// No artifact yet.
|
|
103
161
|
}
|
|
@@ -106,7 +164,7 @@ export function createAssistantState({
|
|
|
106
164
|
|
|
107
165
|
async loadCurrentArtifact() {
|
|
108
166
|
try {
|
|
109
|
-
|
|
167
|
+
runState.hydrateFromArtifact(loadCurrentRunArtifact(productDir));
|
|
110
168
|
} catch {
|
|
111
169
|
// No artifact yet.
|
|
112
170
|
}
|
|
@@ -114,13 +172,13 @@ export function createAssistantState({
|
|
|
114
172
|
},
|
|
115
173
|
|
|
116
174
|
revealFile(serviceName, filePath) {
|
|
117
|
-
const revealed =
|
|
175
|
+
const revealed = runState.revealFile(serviceName, filePath);
|
|
118
176
|
refreshContextPack();
|
|
119
177
|
return revealed;
|
|
120
178
|
},
|
|
121
179
|
|
|
122
180
|
revealService(serviceName) {
|
|
123
|
-
const revealed =
|
|
181
|
+
const revealed = runState.revealService(serviceName);
|
|
124
182
|
refreshContextPack();
|
|
125
183
|
return revealed;
|
|
126
184
|
},
|
|
@@ -176,12 +234,20 @@ export function createAssistantState({
|
|
|
176
234
|
|
|
177
235
|
setProvider(nextProvider) {
|
|
178
236
|
settings = mergeAssistantSettings(settings, { provider: nextProvider || DEFAULT_ASSISTANT_SETTINGS.provider });
|
|
179
|
-
resolvedProviderName =
|
|
237
|
+
resolvedProviderName = resolveInitialProvider(settings.provider, env);
|
|
238
|
+
if (settings.model && getModelProviderMismatch(resolvedProviderName, settings.model)) {
|
|
239
|
+
settings = mergeAssistantSettings(settings, { model: null });
|
|
240
|
+
}
|
|
180
241
|
saveAssistantSettings(productDir, settings);
|
|
181
242
|
notify();
|
|
182
243
|
},
|
|
183
244
|
|
|
184
|
-
setModel(nextModel) {
|
|
245
|
+
setModel(nextModel, { custom = false } = {}) {
|
|
246
|
+
const resolvedProvider = resolveInitialProvider(settings.provider, env);
|
|
247
|
+
const mismatch = getModelProviderMismatch(resolvedProvider, nextModel);
|
|
248
|
+
if (mismatch && !custom) {
|
|
249
|
+
throw new Error(mismatch);
|
|
250
|
+
}
|
|
185
251
|
settings = mergeAssistantSettings(settings, { model: nextModel || null });
|
|
186
252
|
saveAssistantSettings(productDir, settings);
|
|
187
253
|
notify();
|
|
@@ -207,6 +273,19 @@ export function createAssistantState({
|
|
|
207
273
|
notify();
|
|
208
274
|
},
|
|
209
275
|
|
|
276
|
+
setCliConfig(nextConfig) {
|
|
277
|
+
cliConfig = mergeCliConfig(cliConfig, nextConfig);
|
|
278
|
+
saveCliConfig(productDir, cliConfig);
|
|
279
|
+
runState.setAutoCollapsePassedTreeBranches(cliConfig.autoCollapsePassedTreeBranches);
|
|
280
|
+
notify();
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
resetCliConfig() {
|
|
284
|
+
cliConfig = resetCliConfig(productDir);
|
|
285
|
+
runState.setAutoCollapsePassedTreeBranches(cliConfig.autoCollapsePassedTreeBranches);
|
|
286
|
+
notify();
|
|
287
|
+
},
|
|
288
|
+
|
|
210
289
|
resetSettings() {
|
|
211
290
|
settings = resetAssistantSettings(productDir);
|
|
212
291
|
resolvedProviderName = null;
|
|
@@ -218,6 +297,14 @@ export function createAssistantState({
|
|
|
218
297
|
notify();
|
|
219
298
|
},
|
|
220
299
|
|
|
300
|
+
getLiveRunSession() {
|
|
301
|
+
return liveRunSession;
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
getLastRunSession() {
|
|
305
|
+
return lastRunSession;
|
|
306
|
+
},
|
|
307
|
+
|
|
221
308
|
async submitCurrentComposer() {
|
|
222
309
|
const value = composerState.text.trim();
|
|
223
310
|
composerState = createComposerState();
|
|
@@ -229,6 +316,10 @@ export function createAssistantState({
|
|
|
229
316
|
async submitInput(input) {
|
|
230
317
|
const trimmed = String(input || "").trim();
|
|
231
318
|
if (!trimmed) return;
|
|
319
|
+
if (notice && !startupNoticeEmitted) {
|
|
320
|
+
startupNoticeEmitted = true;
|
|
321
|
+
appendMessage({ role: "system", text: notice });
|
|
322
|
+
}
|
|
232
323
|
appendMessage({ role: "user", text: trimmed });
|
|
233
324
|
|
|
234
325
|
const slash = parseSlashCommandSafe(trimmed);
|
|
@@ -238,6 +329,7 @@ export function createAssistantState({
|
|
|
238
329
|
}
|
|
239
330
|
if (slash) {
|
|
240
331
|
try {
|
|
332
|
+
setBusy(true, `Running ${slash.type}...`);
|
|
241
333
|
await executeSlashCommand({
|
|
242
334
|
slash,
|
|
243
335
|
state,
|
|
@@ -252,6 +344,34 @@ export function createAssistantState({
|
|
|
252
344
|
role: "system",
|
|
253
345
|
text: error instanceof Error ? error.message : String(error),
|
|
254
346
|
});
|
|
347
|
+
} finally {
|
|
348
|
+
setBusy(false, null);
|
|
349
|
+
}
|
|
350
|
+
refreshContextPack();
|
|
351
|
+
notify();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const routedSlash = routeLocalIntent(trimmed);
|
|
356
|
+
if (routedSlash) {
|
|
357
|
+
try {
|
|
358
|
+
setBusy(true, `Running ${routedSlash.type}...`);
|
|
359
|
+
await executeSlashCommand({
|
|
360
|
+
slash: routedSlash,
|
|
361
|
+
state,
|
|
362
|
+
productDir,
|
|
363
|
+
settings,
|
|
364
|
+
configs,
|
|
365
|
+
env,
|
|
366
|
+
appendMessage,
|
|
367
|
+
});
|
|
368
|
+
} catch (error) {
|
|
369
|
+
appendMessage({
|
|
370
|
+
role: "system",
|
|
371
|
+
text: error instanceof Error ? error.message : String(error),
|
|
372
|
+
});
|
|
373
|
+
} finally {
|
|
374
|
+
setBusy(false, null);
|
|
255
375
|
}
|
|
256
376
|
refreshContextPack();
|
|
257
377
|
notify();
|
|
@@ -262,7 +382,7 @@ export function createAssistantState({
|
|
|
262
382
|
setBusy(true, `Thinking with ${settings.provider === "auto" ? "provider" : settings.provider}...`);
|
|
263
383
|
const emitted = await runAssistantConversationTurn({
|
|
264
384
|
productDir,
|
|
265
|
-
|
|
385
|
+
runState,
|
|
266
386
|
transcript: messages.map((entry) => ({ role: entry.role, text: entry.text })),
|
|
267
387
|
userMessage: trimmed,
|
|
268
388
|
settings,
|
|
@@ -286,7 +406,7 @@ export function createAssistantState({
|
|
|
286
406
|
notify();
|
|
287
407
|
},
|
|
288
408
|
onToolEvent(event) {
|
|
289
|
-
handleAssistantToolEvent(event, appendMessage);
|
|
409
|
+
handleAssistantToolEvent(state, event, appendMessage);
|
|
290
410
|
},
|
|
291
411
|
});
|
|
292
412
|
for (const message of emitted) appendMessage(message);
|
|
@@ -308,8 +428,8 @@ export function createAssistantState({
|
|
|
308
428
|
|
|
309
429
|
getSnapshot() {
|
|
310
430
|
return {
|
|
311
|
-
context: buildContextSelection(
|
|
312
|
-
|
|
431
|
+
context: buildContextSelection(runState.getSnapshot()),
|
|
432
|
+
run: runState.getSnapshot(),
|
|
313
433
|
productDir,
|
|
314
434
|
messages: [...messages],
|
|
315
435
|
composer: composerState.text,
|
|
@@ -321,8 +441,11 @@ export function createAssistantState({
|
|
|
321
441
|
model: settings.model,
|
|
322
442
|
effort: settings.effort,
|
|
323
443
|
providerArgs: [...settings.providerArgs],
|
|
444
|
+
cliConfig,
|
|
324
445
|
activeStatus,
|
|
325
446
|
contextUsage,
|
|
447
|
+
liveRunSession: serializeRunSession(liveRunSession),
|
|
448
|
+
lastRunSession: serializeRunSession(lastRunSession),
|
|
326
449
|
contextPaths: {
|
|
327
450
|
contextPath: commandLog.contextPath,
|
|
328
451
|
summaryPath: commandLog.summaryPath,
|
|
@@ -372,10 +495,17 @@ async function executeSlashCommand({
|
|
|
372
495
|
return;
|
|
373
496
|
}
|
|
374
497
|
if (slash.type === "model") {
|
|
375
|
-
state.setModel(slash.model);
|
|
498
|
+
state.setModel(slash.model, { custom: slash.custom });
|
|
376
499
|
appendMessage({ role: "assistant", text: slash.model ? `Model set to ${slash.model}.` : "Model reset to provider default." });
|
|
377
500
|
return;
|
|
378
501
|
}
|
|
502
|
+
if (slash.type === "model-list") {
|
|
503
|
+
const snapshot = state.getSnapshot();
|
|
504
|
+
const provider = snapshot.resolvedProvider || resolveInitialProvider(snapshot.provider, env) || snapshot.provider;
|
|
505
|
+
const discovery = await discoverAssistantModels({ provider, productDir, env });
|
|
506
|
+
appendMessage({ role: "assistant", text: formatModelChoices(discovery, { currentModel: snapshot.model }) });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
379
509
|
if (slash.type === "effort") {
|
|
380
510
|
state.setEffort(slash.effort);
|
|
381
511
|
appendMessage({ role: "assistant", text: slash.effort ? `Effort set to ${slash.effort}.` : "Effort reset to provider default." });
|
|
@@ -405,28 +535,32 @@ async function executeSlashCommand({
|
|
|
405
535
|
appendMessage({ role: "assistant", text: "Assistant settings reset." });
|
|
406
536
|
return;
|
|
407
537
|
}
|
|
538
|
+
if (slash.type === "config-show") {
|
|
539
|
+
appendMessage({ role: "assistant", text: formatCliConfig(state.getSnapshot().cliConfig) });
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (slash.type === "config-reset") {
|
|
543
|
+
state.resetCliConfig();
|
|
544
|
+
appendMessage({ role: "assistant", text: "CLI config reset." });
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (slash.type === "config-set-auto-collapse") {
|
|
548
|
+
state.setCliConfig({ autoCollapsePassedTreeBranches: slash.value });
|
|
549
|
+
appendMessage({
|
|
550
|
+
role: "assistant",
|
|
551
|
+
text: `autoCollapsePassedTreeBranches set to ${slash.value}.`,
|
|
552
|
+
});
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
408
555
|
|
|
409
556
|
const result = await executeSlashTool(slash, {
|
|
410
557
|
productDir,
|
|
411
|
-
|
|
558
|
+
runState: state.runState,
|
|
412
559
|
configs,
|
|
413
560
|
env,
|
|
414
561
|
commandLog: state.commandLog,
|
|
415
562
|
onEvent(event) {
|
|
416
|
-
|
|
417
|
-
appendMessage({
|
|
418
|
-
role: "tool",
|
|
419
|
-
status: "running",
|
|
420
|
-
title: event.title || event.tool || "Tool",
|
|
421
|
-
text: event.message,
|
|
422
|
-
data: {
|
|
423
|
-
command: event.command || null,
|
|
424
|
-
testkitRelated: Boolean(event.testkitRelated),
|
|
425
|
-
},
|
|
426
|
-
});
|
|
427
|
-
} else if (event.type === "tool-status") {
|
|
428
|
-
state.setNotice(event.message);
|
|
429
|
-
}
|
|
563
|
+
handleAssistantToolEvent(state, event, appendMessage);
|
|
430
564
|
},
|
|
431
565
|
provider: settings.provider,
|
|
432
566
|
});
|
|
@@ -439,18 +573,32 @@ async function executeSlashCommand({
|
|
|
439
573
|
});
|
|
440
574
|
}
|
|
441
575
|
|
|
442
|
-
function handleAssistantToolEvent(event, appendMessage) {
|
|
443
|
-
if (!event
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
576
|
+
function handleAssistantToolEvent(state, event, appendMessage) {
|
|
577
|
+
if (!event) return;
|
|
578
|
+
if (event.type === "tool-start") {
|
|
579
|
+
appendMessage({
|
|
580
|
+
role: "tool",
|
|
581
|
+
status: "running",
|
|
582
|
+
title: event.title || event.tool || "Tool",
|
|
583
|
+
text: event.message || "Running tool",
|
|
584
|
+
data: {
|
|
585
|
+
command: event.command || null,
|
|
586
|
+
testkitRelated: Boolean(event.testkitRelated),
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (event.type === "tool-status") {
|
|
592
|
+
state.setNotice(event.message);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (event.type === "run-session-start") {
|
|
596
|
+
state.attachRunSession(event.session, { active: true });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (event.type === "run-session-end") {
|
|
600
|
+
state.completeRunSession(event.session);
|
|
601
|
+
}
|
|
454
602
|
}
|
|
455
603
|
|
|
456
604
|
function formatSettings(snapshot) {
|
|
@@ -483,39 +631,18 @@ async function executeSlashTool(slash, context) {
|
|
|
483
631
|
case "service":
|
|
484
632
|
return executeAssistantTool("read_context", { service: slash.service, mode: "detail" }, context);
|
|
485
633
|
case "status":
|
|
486
|
-
return executeAssistantTool("
|
|
634
|
+
return executeAssistantTool("show_status", {}, context);
|
|
487
635
|
case "discover":
|
|
488
|
-
return executeAssistantTool("
|
|
636
|
+
return executeAssistantTool("discover_tests", {}, context);
|
|
489
637
|
case "doctor":
|
|
490
|
-
return executeAssistantTool("
|
|
638
|
+
return executeAssistantTool("run_doctor", {}, context);
|
|
491
639
|
case "run":
|
|
492
|
-
return executeAssistantTool("
|
|
640
|
+
return executeAssistantTool("run_tests", slash.options, context);
|
|
493
641
|
default:
|
|
494
642
|
throw new Error(`Unsupported slash command "${slash.type}"`);
|
|
495
643
|
}
|
|
496
644
|
}
|
|
497
645
|
|
|
498
|
-
function buildRunSlashCommand(options = {}) {
|
|
499
|
-
const parts = ["testkit", "run", "--dir", "."];
|
|
500
|
-
for (const type of options.type || []) {
|
|
501
|
-
parts.push("--type", type);
|
|
502
|
-
}
|
|
503
|
-
for (const suite of options.suite || []) {
|
|
504
|
-
parts.push("--suite", suite);
|
|
505
|
-
}
|
|
506
|
-
for (const file of options.file || []) {
|
|
507
|
-
parts.push("--file", file);
|
|
508
|
-
}
|
|
509
|
-
if (options.service) parts.push("--service", options.service);
|
|
510
|
-
return parts.map(shellEscapeArg).join(" ");
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function shellEscapeArg(value) {
|
|
514
|
-
const stringValue = String(value);
|
|
515
|
-
if (/^[a-zA-Z0-9._:/-]+$/.test(stringValue)) return stringValue;
|
|
516
|
-
return `'${stringValue.replace(/'/g, `'\\''`)}'`;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
646
|
function parseSlashCommandSafe(input) {
|
|
520
647
|
try {
|
|
521
648
|
return parseSlashCommand(input);
|
|
@@ -526,3 +653,42 @@ function parseSlashCommandSafe(input) {
|
|
|
526
653
|
};
|
|
527
654
|
}
|
|
528
655
|
}
|
|
656
|
+
|
|
657
|
+
function sanitizeSettingsForResolvedProvider({ productDir, settings, resolvedProvider }) {
|
|
658
|
+
const mismatch = getModelProviderMismatch(resolvedProvider, settings.model);
|
|
659
|
+
if (!mismatch) return { settings, notice: null };
|
|
660
|
+
const previousModel = settings.model;
|
|
661
|
+
const sanitized = mergeAssistantSettings(settings, { model: null });
|
|
662
|
+
saveAssistantSettings(productDir, sanitized);
|
|
663
|
+
return {
|
|
664
|
+
settings: sanitized,
|
|
665
|
+
notice: `Cleared incompatible saved model "${previousModel}" for ${resolvedProvider}.`,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function routeLocalIntent(input) {
|
|
670
|
+
const normalized = String(input || "").trim().toLowerCase();
|
|
671
|
+
const runMatch = normalized.match(/^run\s+(int|e2e|scenario|dal|load|pw|all)(?:\s+tests?)?$/);
|
|
672
|
+
if (runMatch) {
|
|
673
|
+
return {
|
|
674
|
+
type: "run",
|
|
675
|
+
options: {
|
|
676
|
+
type: [runMatch[1]],
|
|
677
|
+
suite: [],
|
|
678
|
+
file: [],
|
|
679
|
+
service: null,
|
|
680
|
+
},
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
if (/^(show\s+)?latest\s+summary$/.test(normalized)) return { type: "status" };
|
|
684
|
+
if (/^list\s+test\s+files$/.test(normalized)) return { type: "discover" };
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function serializeRunSession(session) {
|
|
689
|
+
if (!session) return null;
|
|
690
|
+
return {
|
|
691
|
+
productDir: session.productDir,
|
|
692
|
+
snapshot: session.getSnapshot(),
|
|
693
|
+
};
|
|
694
|
+
}
|