@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.
@@ -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
- command ? createElement(Text, { key: "command" }, `${dim("$")} ${command}`) : null,
254
- status ? createElement(Text, { key: "status" }, colorCommandStatus(block, status)) : null,
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
- ...outputLines.map((line, index) => (
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
- omitted > 0 ? createElement(Text, { key: "omitted" }, dim(`… ${omitted} more line${omitted === 1 ? "" : "s"} omitted`)) : null,
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(String(line));
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(String(block.text));
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 width = Math.max(1, ...lines.map((line) => visibleWidth(line)));
75
- const top = `╭${"─".repeat(width + 2)}╮`;
76
- const bottom = `╰${"─".repeat(width + 2)}╯`;
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(width - visibleWidth(line))} │`),
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, PROVIDER_COMMAND_OUTPUT_PREVIEW_LINES);
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/next-analysis",
3
- "version": "0.1.105",
3
+ "version": "0.1.106",
4
4
  "description": "SWC-backed Next.js source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-bridge",
3
- "version": "0.1.105",
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.105"
25
+ "@elench/testkit-protocol": "0.1.106"
26
26
  },
27
27
  "private": false
28
28
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-protocol",
3
- "version": "0.1.105",
3
+ "version": "0.1.106",
4
4
  "description": "Shared browser protocol for testkit bridge and extension consumers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/ts-analysis",
3
- "version": "0.1.105",
3
+ "version": "0.1.106",
4
4
  "description": "TypeScript compiler-backed source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.105",
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.105",
94
- "@elench/testkit-bridge": "0.1.105",
95
- "@elench/testkit-protocol": "0.1.105",
96
- "@elench/ts-analysis": "0.1.105",
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",