@elench/testkit 0.1.109 → 0.1.111
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/cli/assistant/actions.mjs +10 -7
- package/lib/cli/assistant/app.mjs +19 -5
- package/lib/cli/assistant/command-classifier.d.mts +6 -0
- package/lib/cli/assistant/command-classifier.d.mts.map +1 -0
- package/lib/cli/assistant/command-classifier.mjs +48 -0
- package/lib/cli/assistant/command-classifier.mjs.map +1 -0
- package/lib/cli/assistant/command-observer.mjs +20 -11
- package/lib/cli/assistant/command-results.mjs +2 -34
- package/lib/cli/assistant/context-pack.mjs +53 -45
- package/lib/cli/assistant/prompt-builder.mjs +21 -13
- package/lib/cli/assistant/providers/claude.mjs +77 -19
- package/lib/cli/assistant/providers/codex.mjs +8 -12
- package/lib/cli/assistant/providers/index.mjs +3 -2
- package/lib/cli/assistant/providers/shared.mjs +22 -3
- package/lib/cli/assistant/quality-signal-strip.mjs +103 -0
- package/lib/cli/assistant/session-paths.d.mts +23 -0
- package/lib/cli/assistant/session-paths.d.mts.map +1 -0
- package/lib/cli/assistant/session-paths.mjs +31 -0
- package/lib/cli/assistant/session-paths.mjs.map +1 -0
- package/lib/cli/assistant/session.mjs +10 -2
- package/lib/cli/assistant/state.mjs +51 -2
- package/lib/cli/assistant/transcript-text.mjs +2 -1
- package/lib/cli/assistant/view-model.mjs +79 -0
- package/lib/cli/commands/assistant.mjs +3 -0
- package/lib/runner/maintenance.mjs +1 -1
- package/lib/runner/status-model.mjs +11 -2
- 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 +10 -9
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts +188 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +1 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js +293 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js.map +1 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +25 -0
- package/node_modules/es-toolkit/CHANGELOG.md +0 -801
- package/node_modules/es-toolkit/src/compat/_internal/Equals.d.ts +0 -1
- package/node_modules/es-toolkit/src/compat/_internal/IsWritable.d.ts +0 -3
- package/node_modules/es-toolkit/src/compat/_internal/MutableList.d.ts +0 -4
- package/node_modules/es-toolkit/src/compat/_internal/RejectReadonly.d.ts +0 -4
- package/node_modules/esprima/ChangeLog +0 -235
|
@@ -5,7 +5,12 @@ import {
|
|
|
5
5
|
buildToolEvent,
|
|
6
6
|
createHostedSessionRunner,
|
|
7
7
|
} from "./shared.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
providerAssistantDelta,
|
|
10
|
+
providerAssistantFinal,
|
|
11
|
+
providerToolStart,
|
|
12
|
+
providerToolUpdate,
|
|
13
|
+
} from "./events.mjs";
|
|
9
14
|
|
|
10
15
|
export function startClaudeHostedSession({
|
|
11
16
|
command = "claude",
|
|
@@ -13,6 +18,7 @@ export function startClaudeHostedSession({
|
|
|
13
18
|
prompt,
|
|
14
19
|
onEvent,
|
|
15
20
|
onRawLine,
|
|
21
|
+
timeoutMs = null,
|
|
16
22
|
purpose = "assistant",
|
|
17
23
|
model = null,
|
|
18
24
|
effort = null,
|
|
@@ -46,12 +52,16 @@ export function startClaudeHostedSession({
|
|
|
46
52
|
env,
|
|
47
53
|
});
|
|
48
54
|
|
|
55
|
+
const parserState = createClaudeParserState();
|
|
49
56
|
return createHostedSessionRunner({
|
|
50
57
|
provider: "claude",
|
|
51
58
|
child,
|
|
52
59
|
onEvent,
|
|
53
60
|
onRawLine,
|
|
54
|
-
|
|
61
|
+
timeoutMs,
|
|
62
|
+
parsePayload(payload) {
|
|
63
|
+
return parseClaudePayload(payload, parserState);
|
|
64
|
+
},
|
|
55
65
|
readFinalText(result) {
|
|
56
66
|
return readClaudeFinalText(result?.stdout || "") || null;
|
|
57
67
|
},
|
|
@@ -60,10 +70,17 @@ export function startClaudeHostedSession({
|
|
|
60
70
|
|
|
61
71
|
function normalizeProviderArgs(providerArgs) {
|
|
62
72
|
if (!Array.isArray(providerArgs)) return [];
|
|
63
|
-
return providerArgs.
|
|
73
|
+
return providerArgs.map((arg) => String(arg || "").trim()).filter(Boolean);
|
|
64
74
|
}
|
|
65
75
|
|
|
66
|
-
export function
|
|
76
|
+
export function createClaudeParserState() {
|
|
77
|
+
return {
|
|
78
|
+
currentMessageId: null,
|
|
79
|
+
contentBlocksByIndex: new Map(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function parseClaudePayload(payload, state = null) {
|
|
67
84
|
const events = [];
|
|
68
85
|
if (!payload || typeof payload !== "object") return events;
|
|
69
86
|
|
|
@@ -77,24 +94,54 @@ export function parseClaudePayload(payload) {
|
|
|
77
94
|
|
|
78
95
|
if (type === "stream_event") {
|
|
79
96
|
const streamEvent = payload.event || {};
|
|
97
|
+
if (streamEvent.type === "message_start") {
|
|
98
|
+
if (state) state.currentMessageId = streamEvent.message?.id || null;
|
|
99
|
+
return events;
|
|
100
|
+
}
|
|
80
101
|
if (streamEvent.type === "content_block_delta" && streamEvent.delta?.type === "text_delta") {
|
|
81
102
|
const text = String(streamEvent.delta.text || "");
|
|
82
|
-
const event = providerAssistantDelta(text);
|
|
103
|
+
const event = providerAssistantDelta(text, messageIdFields(state?.currentMessageId));
|
|
83
104
|
if (event) events.push(event);
|
|
84
105
|
return events;
|
|
85
106
|
}
|
|
86
107
|
if (streamEvent.type === "content_block_start" && streamEvent.content_block?.type === "tool_use") {
|
|
87
108
|
const tool = streamEvent.content_block;
|
|
109
|
+
const block = {
|
|
110
|
+
id: tool.id || streamEvent.index,
|
|
111
|
+
name: tool.name || tool.tool_name || "tool_use",
|
|
112
|
+
input: tool.input || null,
|
|
113
|
+
inputJson: "",
|
|
114
|
+
};
|
|
115
|
+
state?.contentBlocksByIndex?.set(streamEvent.index, block);
|
|
88
116
|
const event = providerToolStart(
|
|
89
|
-
|
|
117
|
+
block.name,
|
|
90
118
|
{
|
|
91
|
-
id:
|
|
92
|
-
input:
|
|
119
|
+
id: block.id,
|
|
120
|
+
input: block.input,
|
|
93
121
|
}
|
|
94
122
|
);
|
|
95
123
|
if (event) events.push(event);
|
|
96
124
|
return events;
|
|
97
125
|
}
|
|
126
|
+
if (streamEvent.type === "content_block_delta" && streamEvent.delta?.type === "input_json_delta") {
|
|
127
|
+
const block = state?.contentBlocksByIndex?.get(streamEvent.index);
|
|
128
|
+
if (block) block.inputJson += String(streamEvent.delta.partial_json || "");
|
|
129
|
+
return events;
|
|
130
|
+
}
|
|
131
|
+
if (streamEvent.type === "content_block_stop") {
|
|
132
|
+
const block = state?.contentBlocksByIndex?.get(streamEvent.index);
|
|
133
|
+
if (!block) return events;
|
|
134
|
+
state.contentBlocksByIndex.delete(streamEvent.index);
|
|
135
|
+
const parsedInput = parseClaudeToolInput(block.inputJson) ?? block.input;
|
|
136
|
+
if (parsedInput != null) {
|
|
137
|
+
const event = providerToolUpdate(block.name, {
|
|
138
|
+
id: block.id,
|
|
139
|
+
input: parsedInput,
|
|
140
|
+
});
|
|
141
|
+
if (event) events.push(event);
|
|
142
|
+
}
|
|
143
|
+
return events;
|
|
144
|
+
}
|
|
98
145
|
if (streamEvent.type === "tool_use" || streamEvent.content_block?.type === "tool_use") {
|
|
99
146
|
const tool = streamEvent.content_block || streamEvent;
|
|
100
147
|
const event = buildToolEvent(
|
|
@@ -107,18 +154,10 @@ export function parseClaudePayload(payload) {
|
|
|
107
154
|
return events;
|
|
108
155
|
}
|
|
109
156
|
|
|
110
|
-
if (type && /tool/i.test(type)) {
|
|
111
|
-
const event = buildToolEvent(
|
|
112
|
-
payload.name || payload.tool_name || payload.tool || type,
|
|
113
|
-
payload.detail || payload.summary || null
|
|
114
|
-
);
|
|
115
|
-
if (event) events.push(event);
|
|
116
|
-
return events;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
157
|
if (type === "assistant") {
|
|
120
158
|
const fragments = extractClaudeTextFragments(payload.message?.content || payload.content || []);
|
|
121
|
-
const
|
|
159
|
+
const id = payload.message?.id || state?.currentMessageId || null;
|
|
160
|
+
const event = providerAssistantFinal(fragments.join(""), messageIdFields(id));
|
|
122
161
|
if (event) events.push(event);
|
|
123
162
|
return events;
|
|
124
163
|
}
|
|
@@ -138,11 +177,30 @@ export function parseClaudePayload(payload) {
|
|
|
138
177
|
return events;
|
|
139
178
|
}
|
|
140
179
|
|
|
141
|
-
const statusEvent = buildStatusEvent(type ? `Claude event: ${type}` :
|
|
180
|
+
const statusEvent = buildStatusEvent(type ? `Claude event: ${type}` : "Claude emitted an unknown event");
|
|
181
|
+
if (statusEvent) {
|
|
182
|
+
statusEvent.transient = true;
|
|
183
|
+
statusEvent.display = false;
|
|
184
|
+
statusEvent.data = payload;
|
|
185
|
+
}
|
|
142
186
|
if (statusEvent) events.push(statusEvent);
|
|
143
187
|
return events;
|
|
144
188
|
}
|
|
145
189
|
|
|
190
|
+
function messageIdFields(id) {
|
|
191
|
+
return id ? { id: String(id) } : {};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function parseClaudeToolInput(value) {
|
|
195
|
+
const text = String(value || "").trim();
|
|
196
|
+
if (!text) return null;
|
|
197
|
+
try {
|
|
198
|
+
return JSON.parse(text);
|
|
199
|
+
} catch {
|
|
200
|
+
return text;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
146
204
|
export function readClaudeFinalText(stdout) {
|
|
147
205
|
const lines = String(stdout || "")
|
|
148
206
|
.split("\n")
|
|
@@ -4,7 +4,6 @@ import path from "path";
|
|
|
4
4
|
import { execa } from "execa";
|
|
5
5
|
import {
|
|
6
6
|
buildErrorEvent,
|
|
7
|
-
buildToolEvent,
|
|
8
7
|
createHostedSessionRunner,
|
|
9
8
|
readTextFileIfPresent,
|
|
10
9
|
} from "./shared.mjs";
|
|
@@ -23,6 +22,7 @@ export function startCodexHostedSession({
|
|
|
23
22
|
prompt,
|
|
24
23
|
onEvent,
|
|
25
24
|
onRawLine,
|
|
25
|
+
timeoutMs = null,
|
|
26
26
|
purpose = "assistant",
|
|
27
27
|
model = null,
|
|
28
28
|
providerArgs = [],
|
|
@@ -52,6 +52,7 @@ export function startCodexHostedSession({
|
|
|
52
52
|
child,
|
|
53
53
|
onEvent,
|
|
54
54
|
onRawLine,
|
|
55
|
+
timeoutMs,
|
|
55
56
|
parsePayload: parseCodexPayload,
|
|
56
57
|
shouldIgnoreStatus(message) {
|
|
57
58
|
return String(message || "").trim() === "Reading additional input from stdin...";
|
|
@@ -90,7 +91,7 @@ export function buildCodexArgs({
|
|
|
90
91
|
|
|
91
92
|
function normalizeProviderArgs(providerArgs) {
|
|
92
93
|
if (!Array.isArray(providerArgs)) return [];
|
|
93
|
-
return providerArgs.
|
|
94
|
+
return providerArgs.map((arg) => String(arg || "").trim()).filter(Boolean);
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
export function parseCodexPayload(payload) {
|
|
@@ -146,16 +147,11 @@ export function parseCodexPayload(payload) {
|
|
|
146
147
|
return events;
|
|
147
148
|
}
|
|
148
149
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (event) events.push(event);
|
|
155
|
-
return events;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const statusEvent = providerStatus(type ? `Codex event: ${type}` : JSON.stringify(payload));
|
|
150
|
+
const statusEvent = providerStatus(type ? `Codex event: ${type}` : "Codex emitted an unknown event", {
|
|
151
|
+
transient: true,
|
|
152
|
+
display: false,
|
|
153
|
+
data: payload,
|
|
154
|
+
});
|
|
159
155
|
if (statusEvent) events.push(statusEvent);
|
|
160
156
|
return events;
|
|
161
157
|
}
|
|
@@ -64,13 +64,14 @@ export function startProviderSession({
|
|
|
64
64
|
prompt,
|
|
65
65
|
onEvent,
|
|
66
66
|
onRawLine,
|
|
67
|
+
timeoutMs,
|
|
67
68
|
purpose = "assistant",
|
|
68
69
|
env = process.env,
|
|
69
70
|
} = {}) {
|
|
70
71
|
const resolvedProvider = resolvePreferredProvider(provider, env);
|
|
71
72
|
const command = resolveProviderBinary(resolvedProvider, env);
|
|
72
73
|
if (resolvedProvider === "claude") {
|
|
73
|
-
return startClaudeHostedSession({ command, cwd, prompt, onEvent, onRawLine, purpose, model, effort, providerArgs, env });
|
|
74
|
+
return startClaudeHostedSession({ command, cwd, prompt, onEvent, onRawLine, timeoutMs, purpose, model, effort, providerArgs, env });
|
|
74
75
|
}
|
|
75
|
-
return startCodexHostedSession({ command, cwd, prompt, onEvent, onRawLine, purpose, model, providerArgs, env });
|
|
76
|
+
return startCodexHostedSession({ command, cwd, prompt, onEvent, onRawLine, timeoutMs, purpose, model, providerArgs, env });
|
|
76
77
|
}
|
|
@@ -13,6 +13,7 @@ export function createHostedSessionRunner({
|
|
|
13
13
|
child,
|
|
14
14
|
onEvent,
|
|
15
15
|
onRawLine,
|
|
16
|
+
timeoutMs = null,
|
|
16
17
|
parsePayload,
|
|
17
18
|
readFinalText,
|
|
18
19
|
shouldIgnoreStatus,
|
|
@@ -22,6 +23,7 @@ export function createHostedSessionRunner({
|
|
|
22
23
|
let assistantText = "";
|
|
23
24
|
let finalText = null;
|
|
24
25
|
let lastErrorMessage = null;
|
|
26
|
+
let timedOut = false;
|
|
25
27
|
|
|
26
28
|
const emit = (event) => {
|
|
27
29
|
if (!event) return;
|
|
@@ -38,6 +40,18 @@ export function createHostedSessionRunner({
|
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
emit(providerEvent("session-start"));
|
|
43
|
+
const timeout = Number.isFinite(Number(timeoutMs)) && Number(timeoutMs) > 0
|
|
44
|
+
? setTimeout(() => {
|
|
45
|
+
timedOut = true;
|
|
46
|
+
lastErrorMessage = `${provider} timed out after ${Math.floor(Number(timeoutMs))}ms`;
|
|
47
|
+
emit(providerError(lastErrorMessage));
|
|
48
|
+
try {
|
|
49
|
+
child.kill("SIGTERM");
|
|
50
|
+
} catch {
|
|
51
|
+
// Ignore timeout cancellation races.
|
|
52
|
+
}
|
|
53
|
+
}, Math.floor(Number(timeoutMs)))
|
|
54
|
+
: null;
|
|
41
55
|
|
|
42
56
|
const stdoutReader = readline.createInterface({ input: child.stdout });
|
|
43
57
|
const stdoutClosed = waitForReaderClose(stdoutReader);
|
|
@@ -62,11 +76,16 @@ export function createHostedSessionRunner({
|
|
|
62
76
|
});
|
|
63
77
|
|
|
64
78
|
const completion = (async () => {
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
let result;
|
|
80
|
+
try {
|
|
81
|
+
result = await child;
|
|
82
|
+
await Promise.all([stdoutClosed, stderrClosed]);
|
|
83
|
+
} finally {
|
|
84
|
+
if (timeout) clearTimeout(timeout);
|
|
85
|
+
}
|
|
67
86
|
const fileFinalText = readFinalText ? readFinalText(result) : null;
|
|
68
87
|
const resolvedFinalText = fileFinalText || finalText || assistantText.trim() || null;
|
|
69
|
-
if ((result.exitCode ?? 0) !== 0) {
|
|
88
|
+
if (timedOut || (result.exitCode ?? 0) !== 0) {
|
|
70
89
|
const message = lastErrorMessage || result.stderr || `${provider} exited with code ${result.exitCode ?? 1}`;
|
|
71
90
|
emit(providerError(message));
|
|
72
91
|
throw new Error(message);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React, { createElement } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { cyan, dim, green, red, yellow } from "../terminal/colors.mjs";
|
|
4
|
+
import { measureWidth, truncateText } from "../terminal/layout.mjs";
|
|
5
|
+
|
|
6
|
+
const LABEL_ITEM_SEPARATOR = " ";
|
|
7
|
+
const ITEM_SEPARATOR = " · ";
|
|
8
|
+
const MIN_TRUNCATED_ITEM_WIDTH = 4;
|
|
9
|
+
|
|
10
|
+
export function fitQualitySignal(signal, { width = 100 } = {}) {
|
|
11
|
+
const availableWidth = Math.max(0, Number(width) || 0);
|
|
12
|
+
const label = normalizeText(signal?.label) || "Quality signal";
|
|
13
|
+
const items = normalizeItems(signal?.items);
|
|
14
|
+
if (availableWidth <= 0) return { label: "", items: [] };
|
|
15
|
+
if (items.length === 0) {
|
|
16
|
+
return {
|
|
17
|
+
label: truncateText(label, availableWidth),
|
|
18
|
+
items: [],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (let count = items.length; count >= 1; count -= 1) {
|
|
23
|
+
const candidate = {
|
|
24
|
+
label,
|
|
25
|
+
items: items.slice(0, count),
|
|
26
|
+
};
|
|
27
|
+
if (measureWidth(formatQualitySignalText(candidate)) <= availableWidth) return candidate;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const firstItemWidth = availableWidth - measureWidth(label) - measureWidth(LABEL_ITEM_SEPARATOR);
|
|
31
|
+
if (firstItemWidth >= MIN_TRUNCATED_ITEM_WIDTH) {
|
|
32
|
+
return {
|
|
33
|
+
label,
|
|
34
|
+
items: [
|
|
35
|
+
{
|
|
36
|
+
...items[0],
|
|
37
|
+
text: truncateText(items[0].text, firstItemWidth),
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
label: truncateText(label, availableWidth),
|
|
45
|
+
items: [],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatQualitySignalText(signal) {
|
|
50
|
+
const label = normalizeText(signal?.label) || "Quality signal";
|
|
51
|
+
const items = normalizeItems(signal?.items);
|
|
52
|
+
if (items.length === 0) return label;
|
|
53
|
+
return `${label}${LABEL_ITEM_SEPARATOR}${items.map((item) => item.text).join(ITEM_SEPARATOR)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function QualitySignalStrip({ signal, width = 100 } = {}) {
|
|
57
|
+
const fitted = fitQualitySignal(signal, { width });
|
|
58
|
+
if (!fitted.label) return null;
|
|
59
|
+
return createElement(
|
|
60
|
+
Box,
|
|
61
|
+
{
|
|
62
|
+
height: 1,
|
|
63
|
+
flexDirection: "row",
|
|
64
|
+
},
|
|
65
|
+
createElement(
|
|
66
|
+
Text,
|
|
67
|
+
null,
|
|
68
|
+
dim(fitted.label),
|
|
69
|
+
fitted.items.length > 0 ? LABEL_ITEM_SEPARATOR : "",
|
|
70
|
+
...fitted.items.flatMap((item, index) => [
|
|
71
|
+
index > 0 ? dim(ITEM_SEPARATOR) : "",
|
|
72
|
+
colorQualitySignalItem(item),
|
|
73
|
+
])
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeItems(items) {
|
|
79
|
+
return (Array.isArray(items) ? items : [])
|
|
80
|
+
.map((item) => ({
|
|
81
|
+
id: normalizeText(item?.id),
|
|
82
|
+
text: normalizeText(item?.text),
|
|
83
|
+
tone: normalizeTone(item?.tone),
|
|
84
|
+
}))
|
|
85
|
+
.filter((item) => item.text);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeText(value) {
|
|
89
|
+
return String(value ?? "").replace(/\s+/g, " ").trim();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeTone(value) {
|
|
93
|
+
if (value === "good" || value === "warning" || value === "danger" || value === "progress") return value;
|
|
94
|
+
return "neutral";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function colorQualitySignalItem(item) {
|
|
98
|
+
if (item.tone === "good") return green(item.text);
|
|
99
|
+
if (item.tone === "warning") return yellow(item.text);
|
|
100
|
+
if (item.tone === "danger") return red(item.text);
|
|
101
|
+
if (item.tone === "progress") return cyan(item.text);
|
|
102
|
+
return item.text;
|
|
103
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface AssistantSessionPaths {
|
|
2
|
+
assistantRoot: string;
|
|
3
|
+
sessionsDir: string;
|
|
4
|
+
contextDir: string;
|
|
5
|
+
contextPath: string;
|
|
6
|
+
currentPath: string;
|
|
7
|
+
summaryPath: string;
|
|
8
|
+
selectionPath: string;
|
|
9
|
+
commandsPath: string;
|
|
10
|
+
commandLogPath: string;
|
|
11
|
+
resultDir: string;
|
|
12
|
+
focusedDetailPath: string;
|
|
13
|
+
focusedLogsPath: string;
|
|
14
|
+
focusedArtifactsPath: string;
|
|
15
|
+
focusedSetupPath: string;
|
|
16
|
+
binDir: string;
|
|
17
|
+
wrapperPath: string;
|
|
18
|
+
providerEventsPath: string;
|
|
19
|
+
providerRawPath: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function createAssistantSessionId(now?: number, random?: () => number): string;
|
|
22
|
+
export declare function assistantSessionPaths(productDir: string, sessionId: string): AssistantSessionPaths;
|
|
23
|
+
//# sourceMappingURL=session-paths.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-paths.d.mts","sourceRoot":"","sources":["../../../src/cli/assistant/session-paths.mts"],"names":[],"mappings":"AAEA,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,wBAAwB,CAAC,GAAG,GAAE,MAAmB,EAAE,MAAM,GAAE,MAAM,MAAoB,GAAG,MAAM,CAE7G;AAED,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,qBAAqB,CAyBlG"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function createAssistantSessionId(now = Date.now(), random = Math.random) {
|
|
3
|
+
return `session-${now}-${random().toString(36).slice(2, 10)}`;
|
|
4
|
+
}
|
|
5
|
+
export function assistantSessionPaths(productDir, sessionId) {
|
|
6
|
+
const assistantRoot = path.join(productDir, ".testkit", "assistant");
|
|
7
|
+
const sessionsDir = path.join(assistantRoot, "sessions");
|
|
8
|
+
const contextDir = path.join(sessionsDir, sessionId);
|
|
9
|
+
const binDir = path.join(contextDir, "bin");
|
|
10
|
+
return {
|
|
11
|
+
assistantRoot,
|
|
12
|
+
sessionsDir,
|
|
13
|
+
contextDir,
|
|
14
|
+
contextPath: path.join(contextDir, "context.md"),
|
|
15
|
+
currentPath: path.join(assistantRoot, "current.json"),
|
|
16
|
+
summaryPath: path.join(contextDir, "latest-run-summary.json"),
|
|
17
|
+
selectionPath: path.join(contextDir, "current-selection.json"),
|
|
18
|
+
commandsPath: path.join(contextDir, "commands.md"),
|
|
19
|
+
commandLogPath: path.join(contextDir, "commands.jsonl"),
|
|
20
|
+
resultDir: path.join(contextDir, "command-results"),
|
|
21
|
+
focusedDetailPath: path.join(contextDir, "focused-detail.txt"),
|
|
22
|
+
focusedLogsPath: path.join(contextDir, "focused-logs.txt"),
|
|
23
|
+
focusedArtifactsPath: path.join(contextDir, "focused-artifacts.txt"),
|
|
24
|
+
focusedSetupPath: path.join(contextDir, "focused-setup.txt"),
|
|
25
|
+
binDir,
|
|
26
|
+
wrapperPath: path.join(binDir, "testkit"),
|
|
27
|
+
providerEventsPath: path.join(contextDir, "provider-events.jsonl"),
|
|
28
|
+
providerRawPath: path.join(contextDir, "provider-raw.jsonl"),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=session-paths.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-paths.mjs","sourceRoot":"","sources":["../../../src/cli/assistant/session-paths.mts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAuB7B,MAAM,UAAU,wBAAwB,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE,EAAE,SAAuB,IAAI,CAAC,MAAM;IACnG,OAAO,WAAW,GAAG,IAAI,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AAChE,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,UAAkB,EAAE,SAAiB;IACzE,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;IACrE,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;IACzD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAC5C,OAAO;QACL,aAAa;QACb,WAAW;QACX,UAAU;QACV,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC;QAChD,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,cAAc,CAAC;QACrD,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,yBAAyB,CAAC;QAC7D,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,wBAAwB,CAAC;QAC9D,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC;QAClD,cAAc,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,gBAAgB,CAAC;QACvD,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,iBAAiB,CAAC;QACnD,iBAAiB,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,oBAAoB,CAAC;QAC9D,eAAe,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC;QAC1D,oBAAoB,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,uBAAuB,CAAC;QACpE,gBAAgB,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,mBAAmB,CAAC;QAC5D,MAAM;QACN,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC;QACzC,kBAAkB,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,uBAAuB,CAAC;QAClE,eAAe,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,oBAAoB,CAAC;KAC7D,CAAC;AACJ,CAAC"}
|
|
@@ -39,11 +39,12 @@ export async function runAssistantConversationTurn({
|
|
|
39
39
|
const runtimeSettings = settings || { provider };
|
|
40
40
|
const resolvedProvider = resolvePreferredProvider(runtimeSettings.provider || provider, env);
|
|
41
41
|
const providerEnv = commandLog?.providerEnv?.(env) || env;
|
|
42
|
+
const timeoutMs = resolveProviderTimeoutMs(providerEnv);
|
|
42
43
|
const tracePath = shouldTraceProviderEvents(env, providerEnv)
|
|
43
|
-
? path.join(commandLog?.contextDir || path.join(productDir, ".testkit", "assistant"), "provider-events.jsonl")
|
|
44
|
+
? commandLog?.providerEventsPath || path.join(commandLog?.contextDir || path.join(productDir, ".testkit", "assistant"), "provider-events.jsonl")
|
|
44
45
|
: null;
|
|
45
46
|
const rawTracePath = shouldTraceProviderEvents(env, providerEnv)
|
|
46
|
-
? path.join(commandLog?.contextDir || path.join(productDir, ".testkit", "assistant"), "provider-raw.jsonl")
|
|
47
|
+
? commandLog?.providerRawPath || path.join(commandLog?.contextDir || path.join(productDir, ".testkit", "assistant"), "provider-raw.jsonl")
|
|
47
48
|
: null;
|
|
48
49
|
onResolvedProvider?.(resolvedProvider);
|
|
49
50
|
onPrompt?.({
|
|
@@ -67,6 +68,7 @@ export async function runAssistantConversationTurn({
|
|
|
67
68
|
model: runtimeSettings.model || null,
|
|
68
69
|
effort: runtimeSettings.effort || null,
|
|
69
70
|
providerArgs: runtimeSettings.providerArgs || [],
|
|
71
|
+
timeoutMs,
|
|
70
72
|
cwd: productDir,
|
|
71
73
|
prompt,
|
|
72
74
|
purpose: "assistant",
|
|
@@ -91,6 +93,12 @@ export async function runAssistantConversationTurn({
|
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
function resolveProviderTimeoutMs(env) {
|
|
97
|
+
const explicit = Number(env?.TESTKIT_ASSISTANT_PROVIDER_TIMEOUT_MS);
|
|
98
|
+
if (Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
|
|
99
|
+
return 600_000;
|
|
100
|
+
}
|
|
101
|
+
|
|
94
102
|
function formatProviderEvent(event) {
|
|
95
103
|
if (event.type === "tool-start") {
|
|
96
104
|
return `${event.provider}: ${event.name}${event.detail ? ` (${event.detail})` : ""}`;
|
|
@@ -41,6 +41,8 @@ import {
|
|
|
41
41
|
} from "./composer.mjs";
|
|
42
42
|
import { buildContextUsage } from "./context-window.mjs";
|
|
43
43
|
|
|
44
|
+
const SNAPSHOT_MESSAGE_TEXT_LIMIT = 20_000;
|
|
45
|
+
|
|
44
46
|
export function createAssistantState({
|
|
45
47
|
productDir,
|
|
46
48
|
provider,
|
|
@@ -62,6 +64,7 @@ export function createAssistantState({
|
|
|
62
64
|
|
|
63
65
|
const listeners = new Set();
|
|
64
66
|
const messages = [];
|
|
67
|
+
const diagnostics = [];
|
|
65
68
|
let composerState = createComposerState();
|
|
66
69
|
let notice = null;
|
|
67
70
|
let busy = false;
|
|
@@ -84,6 +87,7 @@ export function createAssistantState({
|
|
|
84
87
|
if (sanitizedStartup.notice) notice = sanitizedStartup.notice;
|
|
85
88
|
let activeStatus = null;
|
|
86
89
|
let startupNoticeEmitted = false;
|
|
90
|
+
let lastTurnError = null;
|
|
87
91
|
let contextUsage = buildContextUsage({
|
|
88
92
|
provider: resolvedProviderName || settings.provider,
|
|
89
93
|
model: settings.model,
|
|
@@ -130,6 +134,14 @@ export function createAssistantState({
|
|
|
130
134
|
notify();
|
|
131
135
|
}
|
|
132
136
|
|
|
137
|
+
function appendDiagnostic(diagnostic) {
|
|
138
|
+
diagnostics.push({
|
|
139
|
+
timestamp: new Date().toISOString(),
|
|
140
|
+
...diagnostic,
|
|
141
|
+
});
|
|
142
|
+
notify();
|
|
143
|
+
}
|
|
144
|
+
|
|
133
145
|
function refreshContextPack() {
|
|
134
146
|
commandLog.refresh();
|
|
135
147
|
}
|
|
@@ -334,6 +346,20 @@ export function createAssistantState({
|
|
|
334
346
|
async submitInput(input) {
|
|
335
347
|
const trimmed = String(input || "").trim();
|
|
336
348
|
if (!trimmed) return;
|
|
349
|
+
if (busy) {
|
|
350
|
+
const message = "Assistant is already handling a turn.";
|
|
351
|
+
lastTurnError = {
|
|
352
|
+
kind: "concurrency",
|
|
353
|
+
message,
|
|
354
|
+
};
|
|
355
|
+
appendDiagnostic({
|
|
356
|
+
level: "error",
|
|
357
|
+
code: "assistant_turn_already_active",
|
|
358
|
+
message,
|
|
359
|
+
});
|
|
360
|
+
throw new Error(message);
|
|
361
|
+
}
|
|
362
|
+
lastTurnError = null;
|
|
337
363
|
if (notice && !startupNoticeEmitted) {
|
|
338
364
|
startupNoticeEmitted = true;
|
|
339
365
|
appendMessage({ role: "system", text: notice });
|
|
@@ -413,9 +439,13 @@ export function createAssistantState({
|
|
|
413
439
|
},
|
|
414
440
|
});
|
|
415
441
|
} catch (error) {
|
|
442
|
+
lastTurnError = {
|
|
443
|
+
kind: "provider",
|
|
444
|
+
message: error instanceof Error ? error.message : String(error),
|
|
445
|
+
};
|
|
416
446
|
appendMessage({
|
|
417
447
|
role: "system",
|
|
418
|
-
text:
|
|
448
|
+
text: lastTurnError.message,
|
|
419
449
|
});
|
|
420
450
|
} finally {
|
|
421
451
|
refreshContextPack();
|
|
@@ -433,7 +463,7 @@ export function createAssistantState({
|
|
|
433
463
|
context: buildContextSelection(runState.getSnapshot()),
|
|
434
464
|
run: runState.getSnapshot(),
|
|
435
465
|
productDir,
|
|
436
|
-
messages:
|
|
466
|
+
messages: messages.map(serializeMessageForSnapshot),
|
|
437
467
|
composer: composerState.text,
|
|
438
468
|
composerCursor: composerState.cursor,
|
|
439
469
|
notice,
|
|
@@ -445,15 +475,23 @@ export function createAssistantState({
|
|
|
445
475
|
providerArgs: [...settings.providerArgs],
|
|
446
476
|
cliConfig,
|
|
447
477
|
activeStatus,
|
|
478
|
+
lastTurnError,
|
|
479
|
+
diagnostics: [...diagnostics],
|
|
448
480
|
contextUsage,
|
|
449
481
|
liveRunSession: serializeRunSession(liveRunSession),
|
|
450
482
|
lastRunSession: serializeRunSession(lastRunSession),
|
|
451
483
|
contextPaths: {
|
|
484
|
+
sessionId: commandLog.sessionId,
|
|
485
|
+
currentPath: commandLog.currentPath,
|
|
486
|
+
contextDir: commandLog.contextDir,
|
|
452
487
|
contextPath: commandLog.contextPath,
|
|
453
488
|
summaryPath: commandLog.summaryPath,
|
|
454
489
|
selectionPath: commandLog.selectionPath,
|
|
455
490
|
commandsPath: commandLog.commandsPath,
|
|
456
491
|
commandLogPath: commandLog.commandLogPath,
|
|
492
|
+
resultDir: commandLog.resultDir,
|
|
493
|
+
providerEventsPath: commandLog.providerEventsPath,
|
|
494
|
+
providerRawPath: commandLog.providerRawPath,
|
|
457
495
|
},
|
|
458
496
|
};
|
|
459
497
|
},
|
|
@@ -891,6 +929,17 @@ function serializeRunSession(session) {
|
|
|
891
929
|
};
|
|
892
930
|
}
|
|
893
931
|
|
|
932
|
+
function serializeMessageForSnapshot(message) {
|
|
933
|
+
const text = String(message?.text || "");
|
|
934
|
+
if (text.length <= SNAPSHOT_MESSAGE_TEXT_LIMIT) return { ...message };
|
|
935
|
+
return {
|
|
936
|
+
...message,
|
|
937
|
+
text: `${text.slice(0, SNAPSHOT_MESSAGE_TEXT_LIMIT)}\n... ${text.length - SNAPSHOT_MESSAGE_TEXT_LIMIT} characters omitted from snapshot`,
|
|
938
|
+
fullTextOmitted: true,
|
|
939
|
+
fullTextLength: text.length,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
894
943
|
function buildConversationTranscript(messages) {
|
|
895
944
|
return (messages || [])
|
|
896
945
|
.filter((entry) => !["provider-activity", "provider-tool", "provider-error"].includes(entry.role))
|
|
@@ -3,10 +3,11 @@ import { truncateText, wrapText } from "../terminal/layout.mjs";
|
|
|
3
3
|
import { buildAssistantViewModel } from "./view-model.mjs";
|
|
4
4
|
import { renderCodeBlockText } from "./code-block.mjs";
|
|
5
5
|
import { renderMarkdownToAnsi } from "./markdown-block.mjs";
|
|
6
|
+
import { formatQualitySignalText, fitQualitySignal } from "./quality-signal-strip.mjs";
|
|
6
7
|
|
|
7
8
|
export function renderAssistantSnapshotText(snapshot, { cwd = process.cwd(), ansi = false, width = 100 } = {}) {
|
|
8
9
|
const view = buildAssistantViewModel(snapshot || {}, { cwd, terminalWidth: width });
|
|
9
|
-
const lines = [view.title, view.welcome.rows.find(([label]) => label === "Provider")?.[1] || ""]
|
|
10
|
+
const lines = [view.title, view.welcome.rows.find(([label]) => label === "Provider")?.[1] || "", formatQualitySignalText(fitQualitySignal(view.qualitySignal, { width }))]
|
|
10
11
|
.filter(Boolean);
|
|
11
12
|
for (const block of view.blocks || []) {
|
|
12
13
|
lines.push("");
|