@elench/testkit 0.1.105 → 0.1.106
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/app.mjs +23 -10
- package/lib/cli/assistant/code-block.mjs +3 -2
- package/lib/cli/assistant/transcript-text.mjs +20 -18
- package/lib/cli/assistant/view-model.mjs +19 -1
- package/lib/cli/terminal/layout.mjs +19 -0
- 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
|
@@ -6,6 +6,10 @@ import { CodeBlock } from "./code-block.mjs";
|
|
|
6
6
|
import { getComposerDisplayModel } from "./composer.mjs";
|
|
7
7
|
import { MarkdownBlock } from "./markdown-block.mjs";
|
|
8
8
|
import { buildAssistantViewModel } from "./view-model.mjs";
|
|
9
|
+
import { truncateText, wrapText } from "../terminal/layout.mjs";
|
|
10
|
+
|
|
11
|
+
const FALLBACK_COMMAND_BLOCK_WIDTH = 100;
|
|
12
|
+
const COMMAND_BLOCK_CHROME_WIDTH = 4;
|
|
9
13
|
|
|
10
14
|
export function AssistantApp({
|
|
11
15
|
assistantState,
|
|
@@ -180,13 +184,13 @@ function Transcript({ view }) {
|
|
|
180
184
|
createElement(Text, null, dim(view.welcome.rows.find(([label]) => label === "Provider")?.[1] || "")),
|
|
181
185
|
createElement(Text, null, ""),
|
|
182
186
|
view.notice ? createElement(Text, null, yellow(view.notice)) : null,
|
|
183
|
-
...view.blocks.flatMap((block) => renderBlock(block))
|
|
187
|
+
...view.blocks.flatMap((block) => renderBlock(block, view))
|
|
184
188
|
);
|
|
185
189
|
}
|
|
186
190
|
|
|
187
|
-
function renderBlock(block) {
|
|
191
|
+
function renderBlock(block, view) {
|
|
188
192
|
if (block.format === "markdown") return renderMarkdownBlock(block);
|
|
189
|
-
if (block.format === "command") return renderCommandBlock(block);
|
|
193
|
+
if (block.format === "command") return renderCommandBlock(block, view);
|
|
190
194
|
return renderPlainBlock(block);
|
|
191
195
|
}
|
|
192
196
|
|
|
@@ -227,7 +231,7 @@ function renderPlainBlock(block) {
|
|
|
227
231
|
return rendered;
|
|
228
232
|
}
|
|
229
233
|
|
|
230
|
-
function renderCommandBlock(block) {
|
|
234
|
+
function renderCommandBlock(block, view = {}) {
|
|
231
235
|
const marker = colorMarker(block);
|
|
232
236
|
const title = block.title ? bold(block.title) : bold("command");
|
|
233
237
|
const command = formatCommandLine(block);
|
|
@@ -237,6 +241,14 @@ function renderCommandBlock(block) {
|
|
|
237
241
|
const omitted = codeBlock
|
|
238
242
|
? codeBlock.omittedLineCount || 0
|
|
239
243
|
: block.outputPreview?.omittedLineCount || block.omittedOutputLineCount || 0;
|
|
244
|
+
const blockWidth = Math.max(1, Number(view.terminalWidth) || FALLBACK_COMMAND_BLOCK_WIDTH);
|
|
245
|
+
const contentWidth = Math.max(1, blockWidth - COMMAND_BLOCK_CHROME_WIDTH);
|
|
246
|
+
const commandLines = command ? wrapText(`${dim("$")} ${command}`, contentWidth) : [];
|
|
247
|
+
const statusLine = status ? truncateText(status, contentWidth) : null;
|
|
248
|
+
const previewLines = outputLines.map((line) => truncateText(line, contentWidth));
|
|
249
|
+
const omittedLine = omitted > 0
|
|
250
|
+
? truncateText(`… ${omitted} more line${omitted === 1 ? "" : "s"} omitted`, contentWidth)
|
|
251
|
+
: null;
|
|
240
252
|
|
|
241
253
|
return [
|
|
242
254
|
createElement(
|
|
@@ -248,18 +260,19 @@ function renderCommandBlock(block) {
|
|
|
248
260
|
paddingLeft: 1,
|
|
249
261
|
paddingRight: 1,
|
|
250
262
|
marginBottom: 1,
|
|
263
|
+
width: blockWidth,
|
|
251
264
|
},
|
|
252
265
|
createElement(Text, { key: "title" }, `${marker} ${title}`),
|
|
253
|
-
|
|
254
|
-
|
|
266
|
+
...commandLines.map((line, index) => createElement(Text, { key: `command-${index}` }, line)),
|
|
267
|
+
statusLine ? createElement(Text, { key: "status" }, colorCommandStatus(block, statusLine)) : null,
|
|
255
268
|
codeBlock ? createElement(Text, { key: "code-gap" }, "") : null,
|
|
256
|
-
...(codeBlock ? CodeBlock({ lines: codeBlock.lines, language: codeBlock.language }) : []),
|
|
257
|
-
...
|
|
269
|
+
...(codeBlock ? CodeBlock({ lines: codeBlock.lines, language: codeBlock.language, width: contentWidth }) : []),
|
|
270
|
+
...previewLines.map((line, index) => (
|
|
258
271
|
createElement(Text, { key: `output-${index}` }, dim(line))
|
|
259
272
|
)),
|
|
260
|
-
|
|
273
|
+
omittedLine ? createElement(Text, { key: "omitted" }, dim(omittedLine)) : null,
|
|
261
274
|
block.text && !command && outputLines.length === 0
|
|
262
|
-
? createElement(Text, { key: "text" }, colorBlockText(block, block.text))
|
|
275
|
+
? createElement(Text, { key: "text" }, colorBlockText(block, truncateText(block.text, contentWidth)))
|
|
263
276
|
: null
|
|
264
277
|
),
|
|
265
278
|
];
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React, { createElement } from "react";
|
|
2
2
|
import { Text } from "ink";
|
|
3
3
|
import { dim, green, red, cyan } from "../terminal/colors.mjs";
|
|
4
|
+
import { truncateText } from "../terminal/layout.mjs";
|
|
4
5
|
|
|
5
|
-
export function CodeBlock({ lines = [], language = "text" } = {}) {
|
|
6
|
+
export function CodeBlock({ lines = [], language = "text", width = null } = {}) {
|
|
6
7
|
return lines.map((line, index) => (
|
|
7
|
-
createElement(Text, { key: `code-${index}` }, colorCodeLine(line, language))
|
|
8
|
+
createElement(Text, { key: `code-${index}` }, colorCodeLine(width ? truncateText(line, width) : line, language))
|
|
8
9
|
));
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import stripAnsi from "strip-ansi";
|
|
2
|
+
import { truncateText, wrapText } from "../terminal/layout.mjs";
|
|
2
3
|
import { buildAssistantViewModel } from "./view-model.mjs";
|
|
3
4
|
import { renderCodeBlockText } from "./code-block.mjs";
|
|
4
5
|
import { renderMarkdownToAnsi } from "./markdown-block.mjs";
|
|
5
6
|
|
|
6
|
-
export function renderAssistantSnapshotText(snapshot, { cwd = process.cwd(), ansi = false } = {}) {
|
|
7
|
-
const view = buildAssistantViewModel(snapshot || {}, { cwd });
|
|
7
|
+
export function renderAssistantSnapshotText(snapshot, { cwd = process.cwd(), ansi = false, width = 100 } = {}) {
|
|
8
|
+
const view = buildAssistantViewModel(snapshot || {}, { cwd, terminalWidth: width });
|
|
8
9
|
const lines = [view.title, view.welcome.rows.find(([label]) => label === "Provider")?.[1] || ""]
|
|
9
10
|
.filter(Boolean);
|
|
10
11
|
for (const block of view.blocks || []) {
|
|
11
12
|
lines.push("");
|
|
12
|
-
lines.push(...renderBlockLines(block, { ansi }));
|
|
13
|
+
lines.push(...renderBlockLines(block, { ansi, width: view.terminalWidth || width }));
|
|
13
14
|
}
|
|
14
15
|
lines.push("");
|
|
15
16
|
lines.push(view.statusLine);
|
|
@@ -17,7 +18,7 @@ export function renderAssistantSnapshotText(snapshot, { cwd = process.cwd(), ans
|
|
|
17
18
|
return ansi ? text : stripAnsi(text);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
function renderBlockLines(block, { ansi = false } = {}) {
|
|
21
|
+
function renderBlockLines(block, { ansi = false, width = 100 } = {}) {
|
|
21
22
|
const marker = block.marker || "";
|
|
22
23
|
const title = block.title ? ` ${block.title}` : "";
|
|
23
24
|
const text = String(block.text || "").trimEnd();
|
|
@@ -27,28 +28,29 @@ function renderBlockLines(block, { ansi = false } = {}) {
|
|
|
27
28
|
return prefixLines(`${marker}${title}`, normalized);
|
|
28
29
|
}
|
|
29
30
|
if (block.format === "command") {
|
|
30
|
-
return renderCommandBlockLines(block);
|
|
31
|
+
return renderCommandBlockLines(block, { width });
|
|
31
32
|
}
|
|
32
33
|
return prefixLines(`${marker}${title}`, text);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
function renderCommandBlockLines(block) {
|
|
36
|
+
function renderCommandBlockLines(block, { width = 100 } = {}) {
|
|
36
37
|
const header = `${block.marker || ""}${block.title ? ` ${block.title}` : ""}`.trim();
|
|
37
38
|
const body = [];
|
|
39
|
+
const innerWidth = Math.max(1, width - 4);
|
|
38
40
|
const command = formatCommandLine(block.command);
|
|
39
41
|
const status = formatCommandStatus(block);
|
|
40
|
-
if (command) body.push(`$ ${command}
|
|
41
|
-
if (status) body.push(status);
|
|
42
|
+
if (command) body.push(...wrapText(`$ ${command}`, innerWidth));
|
|
43
|
+
if (status) body.push(truncateText(status, innerWidth));
|
|
42
44
|
if (block.codeBlock) {
|
|
43
45
|
body.push("");
|
|
44
|
-
body.push(...renderCodeBlockText(block.codeBlock));
|
|
46
|
+
body.push(...renderCodeBlockText(block.codeBlock).map((line) => truncateText(line, innerWidth)));
|
|
45
47
|
} else {
|
|
46
|
-
for (const line of block.outputPreview?.lines || []) body.push(
|
|
48
|
+
for (const line of block.outputPreview?.lines || []) body.push(truncateText(line, innerWidth));
|
|
47
49
|
const omitted = block.outputPreview?.omittedLineCount || block.omittedOutputLineCount || 0;
|
|
48
|
-
if (omitted > 0) body.push(`... ${omitted} more line${omitted === 1 ? "" : "s"} omitted
|
|
50
|
+
if (omitted > 0) body.push(truncateText(`... ${omitted} more line${omitted === 1 ? "" : "s"} omitted`, innerWidth));
|
|
49
51
|
}
|
|
50
|
-
if (!command && body.length === 0 && block.text) body.push(
|
|
51
|
-
return boxLines([header, ...body].filter(Boolean));
|
|
52
|
+
if (!command && body.length === 0 && block.text) body.push(truncateText(block.text, innerWidth));
|
|
53
|
+
return boxLines([header, ...body].filter(Boolean), { width });
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
function formatCommandLine(command) {
|
|
@@ -70,13 +72,13 @@ function formatCommandStatus(block) {
|
|
|
70
72
|
return null;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
function boxLines(lines) {
|
|
74
|
-
const
|
|
75
|
-
const top = `╭${"─".repeat(
|
|
76
|
-
const bottom = `╰${"─".repeat(
|
|
75
|
+
function boxLines(lines, { width = null } = {}) {
|
|
76
|
+
const contentWidth = width ? Math.max(1, width - 4) : Math.max(1, ...lines.map((line) => visibleWidth(line)));
|
|
77
|
+
const top = `╭${"─".repeat(contentWidth + 2)}╮`;
|
|
78
|
+
const bottom = `╰${"─".repeat(contentWidth + 2)}╯`;
|
|
77
79
|
return [
|
|
78
80
|
top,
|
|
79
|
-
...lines.map((line) => `│ ${line}${" ".repeat(
|
|
81
|
+
...lines.map((line) => `│ ${line}${" ".repeat(contentWidth - visibleWidth(line))} │`),
|
|
80
82
|
bottom,
|
|
81
83
|
];
|
|
82
84
|
}
|
|
@@ -2,6 +2,7 @@ import path from "path";
|
|
|
2
2
|
import { formatContextRemaining } from "./context-window.mjs";
|
|
3
3
|
|
|
4
4
|
const PROVIDER_COMMAND_OUTPUT_PREVIEW_LINES = 12;
|
|
5
|
+
const PROVIDER_FILE_READ_OUTPUT_PREVIEW_LINES = 8;
|
|
5
6
|
const PROVIDER_DIFF_PREVIEW_LINES = 80;
|
|
6
7
|
|
|
7
8
|
export function buildAssistantViewModel(snapshot, { cwd = process.cwd(), terminalWidth = 100 } = {}) {
|
|
@@ -160,7 +161,7 @@ function buildProviderCommandModel(message) {
|
|
|
160
161
|
const status = message.status || providerCommandStatus(event);
|
|
161
162
|
const diffText = extractProviderDiffText(event);
|
|
162
163
|
const diffPreview = diffText ? summarizeOutput(diffText, PROVIDER_DIFF_PREVIEW_LINES) : null;
|
|
163
|
-
const outputPreview = summarizeOutput(output,
|
|
164
|
+
const outputPreview = summarizeOutput(output, providerOutputPreviewLineLimit(event, rawCommand));
|
|
164
165
|
return {
|
|
165
166
|
command: diffPreview ? summarizeProviderEditCommand(event, rawCommand) : rawCommand,
|
|
166
167
|
exitCode,
|
|
@@ -178,6 +179,23 @@ function buildProviderCommandModel(message) {
|
|
|
178
179
|
};
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
function providerOutputPreviewLineLimit(event, rawCommand) {
|
|
183
|
+
if (isProviderFileReadCommand(event, rawCommand)) return PROVIDER_FILE_READ_OUTPUT_PREVIEW_LINES;
|
|
184
|
+
return PROVIDER_COMMAND_OUTPUT_PREVIEW_LINES;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isProviderFileReadCommand(event, rawCommand) {
|
|
188
|
+
if (event.name && event.name !== "command") {
|
|
189
|
+
return /^(read|view|cat)$/i.test(String(event.name));
|
|
190
|
+
}
|
|
191
|
+
const command = String(rawCommand || "").trim();
|
|
192
|
+
if (!command) return false;
|
|
193
|
+
if (/^(cat|sed|awk|head|tail|nl|less|more)\b/.test(command)) return true;
|
|
194
|
+
if (/\b(rg|grep)\b[\s\S]*\b--files\b/.test(command)) return false;
|
|
195
|
+
if (/^(rg|grep)\b/.test(command) && !/\s(-n|--line-number)\b/.test(command)) return false;
|
|
196
|
+
return /(\bsed\s+-n\b|\bhead\b|\btail\b|\bnl\b|\bcat\b)/.test(command);
|
|
197
|
+
}
|
|
198
|
+
|
|
181
199
|
function summarizeProviderEditCommand(event, rawCommand) {
|
|
182
200
|
const name = event.name ? String(event.name) : "edit";
|
|
183
201
|
if (name && name !== "command") return name;
|
|
@@ -32,6 +32,25 @@ export function wrapText(text, width) {
|
|
|
32
32
|
}).split("\n");
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export function truncateText(text, width, { ellipsis = "…" } = {}) {
|
|
36
|
+
const normalized = String(text ?? "");
|
|
37
|
+
if (width <= 0) return "";
|
|
38
|
+
if (measureWidth(normalized) <= width) return normalized;
|
|
39
|
+
const ellipsisWidth = measureWidth(ellipsis);
|
|
40
|
+
if (width <= ellipsisWidth) return ellipsis.slice(0, width);
|
|
41
|
+
|
|
42
|
+
const targetWidth = width - ellipsisWidth;
|
|
43
|
+
let rendered = "";
|
|
44
|
+
let renderedWidth = 0;
|
|
45
|
+
for (const char of normalized) {
|
|
46
|
+
const charWidth = measureWidth(char);
|
|
47
|
+
if (renderedWidth + charWidth > targetWidth) break;
|
|
48
|
+
rendered += char;
|
|
49
|
+
renderedWidth += charWidth;
|
|
50
|
+
}
|
|
51
|
+
return `${rendered}${ellipsis}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
export function renderIndentedBlock(text, { width, indent = " " } = {}) {
|
|
36
55
|
const visibleIndent = measureWidth(indent);
|
|
37
56
|
const contentWidth = Math.max(12, width - visibleIndent);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.106",
|
|
4
4
|
"description": "Browser bridge helpers for testkit",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@elench/testkit-protocol": "0.1.
|
|
25
|
+
"@elench/testkit-protocol": "0.1.106"
|
|
26
26
|
},
|
|
27
27
|
"private": false
|
|
28
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.106",
|
|
4
4
|
"description": "Assistant-first CLI for running, inspecting, and debugging local testkit suites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -90,10 +90,10 @@
|
|
|
90
90
|
},
|
|
91
91
|
"dependencies": {
|
|
92
92
|
"@babel/code-frame": "^7.29.0",
|
|
93
|
-
"@elench/next-analysis": "0.1.
|
|
94
|
-
"@elench/testkit-bridge": "0.1.
|
|
95
|
-
"@elench/testkit-protocol": "0.1.
|
|
96
|
-
"@elench/ts-analysis": "0.1.
|
|
93
|
+
"@elench/next-analysis": "0.1.106",
|
|
94
|
+
"@elench/testkit-bridge": "0.1.106",
|
|
95
|
+
"@elench/testkit-protocol": "0.1.106",
|
|
96
|
+
"@elench/ts-analysis": "0.1.106",
|
|
97
97
|
"@oclif/core": "^4.10.6",
|
|
98
98
|
"esbuild": "^0.25.11",
|
|
99
99
|
"execa": "^9.5.0",
|