@elench/testkit 0.1.105 → 0.1.107
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/interactive.mjs +8 -0
- package/lib/cli/assistant/state.mjs +36 -3
- package/lib/cli/assistant/transcript-text.mjs +20 -18
- package/lib/cli/assistant/view-model.mjs +73 -13
- 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
|
|
|
@@ -43,6 +43,8 @@ export async function runInteractiveAssistant({
|
|
|
43
43
|
assistantState.revealService(service);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
clearInteractiveScreen({ stdout, env });
|
|
47
|
+
|
|
46
48
|
const app = render(
|
|
47
49
|
createElement(AssistantApp, {
|
|
48
50
|
assistantState,
|
|
@@ -59,3 +61,9 @@ export async function runInteractiveAssistant({
|
|
|
59
61
|
|
|
60
62
|
return app.waitUntilExit();
|
|
61
63
|
}
|
|
64
|
+
|
|
65
|
+
export function clearInteractiveScreen({ stdout = process.stdout, env = process.env } = {}) {
|
|
66
|
+
if (env.TESTKIT_ASSISTANT_CLEAR_SCREEN === "0") return;
|
|
67
|
+
if (!stdout?.isTTY) return;
|
|
68
|
+
stdout.write("\x1b[2J\x1b[H");
|
|
69
|
+
}
|
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
} from "../config.mjs";
|
|
9
9
|
import { createRunState } from "../state/run/state.mjs";
|
|
10
10
|
import { buildContextSelection } from "../../results/context.mjs";
|
|
11
|
+
import { renderDiscoverResult } from "../renderers/discover/text.mjs";
|
|
12
|
+
import { renderDoctorResult } from "../renderers/doctor/text.mjs";
|
|
13
|
+
import { renderStatusResult } from "../renderers/status/text.mjs";
|
|
14
|
+
import { renderTypecheckResult } from "../renderers/typecheck/text.mjs";
|
|
11
15
|
import { isProviderInstalled } from "./providers/index.mjs";
|
|
12
16
|
import { parseSlashCommand, formatSlashHelpLines } from "./slash-commands.mjs";
|
|
13
17
|
import { executeAssistantAction } from "./actions.mjs";
|
|
@@ -627,7 +631,12 @@ function handleAssistantToolEvent(state, event, appendMessage) {
|
|
|
627
631
|
toolName: event.command?.kind || "testkit",
|
|
628
632
|
title: formatObservedCommandTitle(event.command),
|
|
629
633
|
text: formatObservedCommandText(event.command),
|
|
630
|
-
data:
|
|
634
|
+
data: {
|
|
635
|
+
...(event.command || {}),
|
|
636
|
+
command: formatObservedCommandLine(event.command),
|
|
637
|
+
exitCode: event.command?.exitCode ?? null,
|
|
638
|
+
testkitRelated: true,
|
|
639
|
+
},
|
|
631
640
|
});
|
|
632
641
|
}
|
|
633
642
|
}
|
|
@@ -893,11 +902,18 @@ function formatObservedCommandTitle(command) {
|
|
|
893
902
|
return `testkit ${kind}`;
|
|
894
903
|
}
|
|
895
904
|
|
|
905
|
+
function formatObservedCommandLine(command) {
|
|
906
|
+
if (!command) return "testkit";
|
|
907
|
+
const argv = Array.isArray(command.argv) ? command.argv : [];
|
|
908
|
+
return `testkit ${argv.join(" ") || command.kind || ""}`.trim();
|
|
909
|
+
}
|
|
910
|
+
|
|
896
911
|
function formatObservedCommandText(command) {
|
|
897
912
|
if (!command) return "Observed Testkit command.";
|
|
913
|
+
const rendered = renderObservedCommandResult(command);
|
|
914
|
+
if (rendered.length > 0) return rendered.join("\n");
|
|
915
|
+
|
|
898
916
|
const lines = [];
|
|
899
|
-
lines.push(`$ testkit ${command.argv?.join(" ") || command.kind || ""}`.trim());
|
|
900
|
-
if (Number.isInteger(command.exitCode)) lines.push(`exit code: ${command.exitCode}`);
|
|
901
917
|
if (command.kind === "run" && command.result?.runArtifact) {
|
|
902
918
|
for (const file of collectRunArtifactFiles(command.result.runArtifact)) {
|
|
903
919
|
lines.push(`${formatObservedFileStatus(file.status)} ${file.serviceName} ${file.type} ${file.path}`);
|
|
@@ -913,6 +929,23 @@ function formatObservedCommandText(command) {
|
|
|
913
929
|
return lines.join("\n");
|
|
914
930
|
}
|
|
915
931
|
|
|
932
|
+
function renderObservedCommandResult(command) {
|
|
933
|
+
const result = command?.result;
|
|
934
|
+
if (!result) return [];
|
|
935
|
+
if (command.kind === "discover") return normalizeRenderedLines(renderDiscoverResult(result, { outputMode: "compact" }));
|
|
936
|
+
if (command.kind === "status") {
|
|
937
|
+
return normalizeRenderedLines((result.results || []).flatMap((entry) => renderStatusResult(entry)));
|
|
938
|
+
}
|
|
939
|
+
if (command.kind === "doctor") return normalizeRenderedLines(renderDoctorResult(result));
|
|
940
|
+
if (command.kind === "typecheck") return normalizeRenderedLines(renderTypecheckResult(result));
|
|
941
|
+
return [];
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function normalizeRenderedLines(lines) {
|
|
945
|
+
return (Array.isArray(lines) ? lines : [])
|
|
946
|
+
.flatMap((line) => String(line ?? "").replace(/\r/g, "").split("\n"));
|
|
947
|
+
}
|
|
948
|
+
|
|
916
949
|
function collectRunArtifactFiles(artifact) {
|
|
917
950
|
const files = [];
|
|
918
951
|
for (const service of artifact?.services || []) {
|
|
@@ -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,7 +2,9 @@ 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;
|
|
7
|
+
const TESTKIT_COMMAND_OUTPUT_PREVIEW_LINES = 18;
|
|
6
8
|
|
|
7
9
|
export function buildAssistantViewModel(snapshot, { cwd = process.cwd(), terminalWidth = 100 } = {}) {
|
|
8
10
|
const providerLabel = buildProviderLabel(snapshot);
|
|
@@ -56,6 +58,7 @@ export function buildTranscriptBlocks(messages) {
|
|
|
56
58
|
return (messages || []).map((message) => {
|
|
57
59
|
const role = message.role || "system";
|
|
58
60
|
if (role === "tool") {
|
|
61
|
+
const outputPreview = summarizeOutput(message.text || "", TESTKIT_COMMAND_OUTPUT_PREVIEW_LINES);
|
|
59
62
|
return {
|
|
60
63
|
id: message.id,
|
|
61
64
|
kind: classifyToolBlock(message),
|
|
@@ -66,6 +69,9 @@ export function buildTranscriptBlocks(messages) {
|
|
|
66
69
|
status: message.status || null,
|
|
67
70
|
command: message.data?.command || null,
|
|
68
71
|
exitCode: message.data?.exitCode ?? null,
|
|
72
|
+
outputPreview,
|
|
73
|
+
outputLineCount: countLines(message.text || ""),
|
|
74
|
+
omittedOutputLineCount: outputPreview.omittedLineCount,
|
|
69
75
|
};
|
|
70
76
|
}
|
|
71
77
|
if (role === "provider-tool") {
|
|
@@ -158,9 +164,9 @@ function buildProviderCommandModel(message) {
|
|
|
158
164
|
const output = event.output || event.data?.aggregated_output || event.data?.output || "";
|
|
159
165
|
const exitCode = event.data?.exit_code ?? event.data?.exitCode ?? null;
|
|
160
166
|
const status = message.status || providerCommandStatus(event);
|
|
161
|
-
const diffText = extractProviderDiffText(event);
|
|
167
|
+
const diffText = extractProviderDiffText(event, rawCommand);
|
|
162
168
|
const diffPreview = diffText ? summarizeOutput(diffText, PROVIDER_DIFF_PREVIEW_LINES) : null;
|
|
163
|
-
const outputPreview = summarizeOutput(output,
|
|
169
|
+
const outputPreview = summarizeOutput(output, providerOutputPreviewLineLimit(event, rawCommand));
|
|
164
170
|
return {
|
|
165
171
|
command: diffPreview ? summarizeProviderEditCommand(event, rawCommand) : rawCommand,
|
|
166
172
|
exitCode,
|
|
@@ -178,6 +184,23 @@ function buildProviderCommandModel(message) {
|
|
|
178
184
|
};
|
|
179
185
|
}
|
|
180
186
|
|
|
187
|
+
function providerOutputPreviewLineLimit(event, rawCommand) {
|
|
188
|
+
if (isProviderFileReadCommand(event, rawCommand)) return PROVIDER_FILE_READ_OUTPUT_PREVIEW_LINES;
|
|
189
|
+
return PROVIDER_COMMAND_OUTPUT_PREVIEW_LINES;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isProviderFileReadCommand(event, rawCommand) {
|
|
193
|
+
if (event.name && event.name !== "command") {
|
|
194
|
+
return /^(read|view|cat)$/i.test(String(event.name));
|
|
195
|
+
}
|
|
196
|
+
const command = unwrapShellCommand(rawCommand);
|
|
197
|
+
if (!command) return false;
|
|
198
|
+
if (/^(cat|sed|awk|head|tail|nl|less|more)\b/.test(command)) return true;
|
|
199
|
+
if (/\b(rg|grep)\b[\s\S]*\b--files\b/.test(command)) return false;
|
|
200
|
+
if (/^(rg|grep)\b/.test(command) && !/\s(-n|--line-number)\b/.test(command)) return false;
|
|
201
|
+
return /(\bsed\s+-n\b|\bhead\b|\btail\b|\bnl\b|\bcat\b)/.test(command);
|
|
202
|
+
}
|
|
203
|
+
|
|
181
204
|
function summarizeProviderEditCommand(event, rawCommand) {
|
|
182
205
|
const name = event.name ? String(event.name) : "edit";
|
|
183
206
|
if (name && name !== "command") return name;
|
|
@@ -187,24 +210,61 @@ function summarizeProviderEditCommand(event, rawCommand) {
|
|
|
187
210
|
return command;
|
|
188
211
|
}
|
|
189
212
|
|
|
190
|
-
function extractProviderDiffText(event) {
|
|
191
|
-
const candidates =
|
|
213
|
+
function extractProviderDiffText(event, rawCommand) {
|
|
214
|
+
const candidates = providerDiffCandidates(event, rawCommand);
|
|
215
|
+
for (const candidate of candidates) {
|
|
216
|
+
const text = stringifyMaybe(candidate);
|
|
217
|
+
if (looksLikeDiff(text)) return text;
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function providerDiffCandidates(event, rawCommand) {
|
|
223
|
+
const name = String(event.name || "");
|
|
224
|
+
if (name && name !== "command") {
|
|
225
|
+
return [
|
|
226
|
+
event.input,
|
|
227
|
+
event.detail,
|
|
228
|
+
event.text,
|
|
229
|
+
event.output,
|
|
230
|
+
event.data?.arguments,
|
|
231
|
+
event.data?.input,
|
|
232
|
+
event.data?.patch,
|
|
233
|
+
event.data?.diff,
|
|
234
|
+
event.data?.aggregated_output,
|
|
235
|
+
event.data?.output,
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const command = String(rawCommand || event.data?.command || event.data?.input || "").trim();
|
|
240
|
+
const inputCandidates = [
|
|
192
241
|
event.input,
|
|
193
|
-
event.detail,
|
|
194
|
-
event.text,
|
|
195
|
-
event.output,
|
|
196
242
|
event.data?.arguments,
|
|
197
243
|
event.data?.input,
|
|
198
244
|
event.data?.patch,
|
|
199
245
|
event.data?.diff,
|
|
200
|
-
event.data?.aggregated_output,
|
|
201
|
-
event.data?.output,
|
|
202
246
|
];
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
247
|
+
if (isDiffProducingShellCommand(command)) {
|
|
248
|
+
return [
|
|
249
|
+
...inputCandidates,
|
|
250
|
+
event.output,
|
|
251
|
+
event.data?.aggregated_output,
|
|
252
|
+
event.data?.output,
|
|
253
|
+
];
|
|
206
254
|
}
|
|
207
|
-
return
|
|
255
|
+
return inputCandidates;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function isDiffProducingShellCommand(command) {
|
|
259
|
+
const normalized = unwrapShellCommand(command);
|
|
260
|
+
return /^((git\s+diff|git\s+show|diff|apply_patch)\b|.*\bapply_patch\b)/.test(normalized);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function unwrapShellCommand(command) {
|
|
264
|
+
const text = String(command || "").trim();
|
|
265
|
+
const match = text.match(/(?:^|\s)(?:[^\s'"]*\/)?(?:bash|sh|zsh)\s+-lc\s+(['"])([\s\S]*)\1\s*$/);
|
|
266
|
+
if (!match) return text;
|
|
267
|
+
return match[2].replace(/\\(["'\\$`])/g, "$1").trim();
|
|
208
268
|
}
|
|
209
269
|
|
|
210
270
|
function stringifyMaybe(value) {
|
|
@@ -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.107",
|
|
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",
|