@elench/testkit 0.1.104 → 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 +83 -4
- package/lib/cli/assistant/code-block.mjs +30 -0
- package/lib/cli/assistant/state.mjs +67 -10
- package/lib/cli/assistant/transcript-text.mjs +63 -4
- package/lib/cli/assistant/view-model.mjs +135 -2
- 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
|
@@ -2,9 +2,14 @@ import React, { createElement, useEffect, useMemo, useRef, useState } from "reac
|
|
|
2
2
|
import { Box, Text, useApp, useBoxMetrics, useCursor, useInput, useStdout } from "ink";
|
|
3
3
|
import { bold, cyan, dim, green, red, yellow } from "../terminal/colors.mjs";
|
|
4
4
|
import { RunTreeView } from "../components/blocks/run-tree.mjs";
|
|
5
|
+
import { CodeBlock } from "./code-block.mjs";
|
|
5
6
|
import { getComposerDisplayModel } from "./composer.mjs";
|
|
6
7
|
import { MarkdownBlock } from "./markdown-block.mjs";
|
|
7
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;
|
|
8
13
|
|
|
9
14
|
export function AssistantApp({
|
|
10
15
|
assistantState,
|
|
@@ -179,12 +184,13 @@ function Transcript({ view }) {
|
|
|
179
184
|
createElement(Text, null, dim(view.welcome.rows.find(([label]) => label === "Provider")?.[1] || "")),
|
|
180
185
|
createElement(Text, null, ""),
|
|
181
186
|
view.notice ? createElement(Text, null, yellow(view.notice)) : null,
|
|
182
|
-
...view.blocks.flatMap((block) => renderBlock(block))
|
|
187
|
+
...view.blocks.flatMap((block) => renderBlock(block, view))
|
|
183
188
|
);
|
|
184
189
|
}
|
|
185
190
|
|
|
186
|
-
function renderBlock(block) {
|
|
191
|
+
function renderBlock(block, view) {
|
|
187
192
|
if (block.format === "markdown") return renderMarkdownBlock(block);
|
|
193
|
+
if (block.format === "command") return renderCommandBlock(block, view);
|
|
188
194
|
return renderPlainBlock(block);
|
|
189
195
|
}
|
|
190
196
|
|
|
@@ -225,6 +231,79 @@ function renderPlainBlock(block) {
|
|
|
225
231
|
return rendered;
|
|
226
232
|
}
|
|
227
233
|
|
|
234
|
+
function renderCommandBlock(block, view = {}) {
|
|
235
|
+
const marker = colorMarker(block);
|
|
236
|
+
const title = block.title ? bold(block.title) : bold("command");
|
|
237
|
+
const command = formatCommandLine(block);
|
|
238
|
+
const status = formatCommandStatus(block);
|
|
239
|
+
const codeBlock = block.codeBlock || null;
|
|
240
|
+
const outputLines = codeBlock ? [] : block.outputPreview?.lines || [];
|
|
241
|
+
const omitted = codeBlock
|
|
242
|
+
? codeBlock.omittedLineCount || 0
|
|
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;
|
|
252
|
+
|
|
253
|
+
return [
|
|
254
|
+
createElement(
|
|
255
|
+
Box,
|
|
256
|
+
{
|
|
257
|
+
key: `${block.id}-command`,
|
|
258
|
+
borderStyle: "round",
|
|
259
|
+
flexDirection: "column",
|
|
260
|
+
paddingLeft: 1,
|
|
261
|
+
paddingRight: 1,
|
|
262
|
+
marginBottom: 1,
|
|
263
|
+
width: blockWidth,
|
|
264
|
+
},
|
|
265
|
+
createElement(Text, { key: "title" }, `${marker} ${title}`),
|
|
266
|
+
...commandLines.map((line, index) => createElement(Text, { key: `command-${index}` }, line)),
|
|
267
|
+
statusLine ? createElement(Text, { key: "status" }, colorCommandStatus(block, statusLine)) : null,
|
|
268
|
+
codeBlock ? createElement(Text, { key: "code-gap" }, "") : null,
|
|
269
|
+
...(codeBlock ? CodeBlock({ lines: codeBlock.lines, language: codeBlock.language, width: contentWidth }) : []),
|
|
270
|
+
...previewLines.map((line, index) => (
|
|
271
|
+
createElement(Text, { key: `output-${index}` }, dim(line))
|
|
272
|
+
)),
|
|
273
|
+
omittedLine ? createElement(Text, { key: "omitted" }, dim(omittedLine)) : null,
|
|
274
|
+
block.text && !command && outputLines.length === 0
|
|
275
|
+
? createElement(Text, { key: "text" }, colorBlockText(block, truncateText(block.text, contentWidth)))
|
|
276
|
+
: null
|
|
277
|
+
),
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function formatCommandLine(block) {
|
|
282
|
+
if (!block.command) return null;
|
|
283
|
+
if (typeof block.command === "string") return block.command;
|
|
284
|
+
try {
|
|
285
|
+
return JSON.stringify(block.command);
|
|
286
|
+
} catch {
|
|
287
|
+
return String(block.command);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function formatCommandStatus(block) {
|
|
292
|
+
const status = block.status || null;
|
|
293
|
+
if (status === "running") return "running";
|
|
294
|
+
if (status === "error") return block.exitCode == null ? "failed" : `failed · exit code ${block.exitCode}`;
|
|
295
|
+
if (block.exitCode != null) return `completed · exit code ${block.exitCode}`;
|
|
296
|
+
if (status === "done") return "completed";
|
|
297
|
+
if (block.text && !block.command) return null;
|
|
298
|
+
return block.text || null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function colorCommandStatus(block, status) {
|
|
302
|
+
if (block.status === "error") return red(status);
|
|
303
|
+
if (block.status === "running") return yellow(status);
|
|
304
|
+
return green(status);
|
|
305
|
+
}
|
|
306
|
+
|
|
228
307
|
function ComposerBar({ view, busy }) {
|
|
229
308
|
const ref = useRef(null);
|
|
230
309
|
const metrics = useBoxMetrics(ref);
|
|
@@ -283,7 +362,7 @@ function colorMarker(block) {
|
|
|
283
362
|
if (block.kind === "system") return red(block.marker);
|
|
284
363
|
if (block.kind === "provider-error") return red(block.marker);
|
|
285
364
|
if (block.kind === "provider-activity") return dim(block.marker);
|
|
286
|
-
if (block.kind === "provider-
|
|
365
|
+
if (block.kind === "provider-command") return yellow(block.marker);
|
|
287
366
|
if (block.kind === "tool-running") return yellow(block.marker);
|
|
288
367
|
if (block.kind === "testkit-run") return green(block.marker);
|
|
289
368
|
return block.marker;
|
|
@@ -294,7 +373,7 @@ function colorBlockText(block, text) {
|
|
|
294
373
|
if (block.kind === "system") return red(text);
|
|
295
374
|
if (block.kind === "provider-error") return red(text);
|
|
296
375
|
if (block.kind === "provider-activity") return dim(text);
|
|
297
|
-
if (block.kind === "provider-
|
|
376
|
+
if (block.kind === "provider-command") return yellow(text);
|
|
298
377
|
if (block.kind === "tool-running") return yellow(text);
|
|
299
378
|
return text;
|
|
300
379
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React, { createElement } from "react";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
import { dim, green, red, cyan } from "../terminal/colors.mjs";
|
|
4
|
+
import { truncateText } from "../terminal/layout.mjs";
|
|
5
|
+
|
|
6
|
+
export function CodeBlock({ lines = [], language = "text", width = null } = {}) {
|
|
7
|
+
return lines.map((line, index) => (
|
|
8
|
+
createElement(Text, { key: `code-${index}` }, colorCodeLine(width ? truncateText(line, width) : line, language))
|
|
9
|
+
));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function renderCodeBlockText({ lines = [], omittedLineCount = 0 } = {}) {
|
|
13
|
+
const rendered = [...lines.map((line) => String(line))];
|
|
14
|
+
if (omittedLineCount > 0) {
|
|
15
|
+
rendered.push(`... ${omittedLineCount} more line${omittedLineCount === 1 ? "" : "s"} omitted`);
|
|
16
|
+
}
|
|
17
|
+
return rendered;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function colorCodeLine(line, language = "text") {
|
|
21
|
+
const text = String(line ?? "");
|
|
22
|
+
if (language !== "diff") return dim(text);
|
|
23
|
+
if (/^\+\+\+/.test(text) || /^---/.test(text) || /^diff --git\b/.test(text) || /^index\b/.test(text)) {
|
|
24
|
+
return cyan(text);
|
|
25
|
+
}
|
|
26
|
+
if (/^\+/.test(text)) return green(text);
|
|
27
|
+
if (/^-/.test(text)) return red(text);
|
|
28
|
+
if (/^@@/.test(text) || /^\*\*\*/.test(text)) return cyan(text);
|
|
29
|
+
return dim(text);
|
|
30
|
+
}
|
|
@@ -636,6 +636,7 @@ function createProviderTurnState() {
|
|
|
636
636
|
return {
|
|
637
637
|
assistantMessageId: null,
|
|
638
638
|
assistantMessageIdsByProviderItem: new Map(),
|
|
639
|
+
providerToolMessageIdsByProviderItem: new Map(),
|
|
639
640
|
lastActivityText: null,
|
|
640
641
|
};
|
|
641
642
|
}
|
|
@@ -674,12 +675,7 @@ function handleProviderEvent(turn, event, { appendMessage, updateMessage, setSta
|
|
|
674
675
|
return;
|
|
675
676
|
}
|
|
676
677
|
if (event.type === "tool-start" || event.type === "tool-update" || event.type === "tool-end") {
|
|
677
|
-
|
|
678
|
-
role: "provider-tool",
|
|
679
|
-
title: formatProviderToolTitle(event),
|
|
680
|
-
text: formatProviderToolText(event),
|
|
681
|
-
data: event,
|
|
682
|
-
}, { appendMessage, setStatus });
|
|
678
|
+
upsertProviderToolActivity(turn, event, { appendMessage, updateMessage, setStatus });
|
|
683
679
|
return;
|
|
684
680
|
}
|
|
685
681
|
if (event.type === "error") {
|
|
@@ -737,6 +733,64 @@ function providerAssistantMessageKey(event) {
|
|
|
737
733
|
return `${event.provider || "provider"}:${event.id}`;
|
|
738
734
|
}
|
|
739
735
|
|
|
736
|
+
function upsertProviderToolActivity(turn, event, { appendMessage, updateMessage, setStatus }) {
|
|
737
|
+
const messageKey = providerToolMessageKey(event);
|
|
738
|
+
const status = providerToolStatus(event);
|
|
739
|
+
const buildMessage = (data) => ({
|
|
740
|
+
role: "provider-tool",
|
|
741
|
+
title: formatProviderToolTitle(data),
|
|
742
|
+
text: formatProviderToolText(data),
|
|
743
|
+
status,
|
|
744
|
+
data,
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
if (!messageKey) {
|
|
748
|
+
appendProviderActivity(turn, buildMessage(event), { appendMessage, setStatus });
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const existingMessageId = turn.providerToolMessageIdsByProviderItem.get(messageKey);
|
|
753
|
+
if (!existingMessageId) {
|
|
754
|
+
const messageId = appendMessage(buildMessage(event));
|
|
755
|
+
turn.providerToolMessageIdsByProviderItem.set(messageKey, messageId);
|
|
756
|
+
setStatus?.(formatProviderToolStatusLine(event));
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
updateMessage(existingMessageId, (message) => {
|
|
761
|
+
const data = mergeProviderToolEventData(message.data, event);
|
|
762
|
+
return buildMessage(data);
|
|
763
|
+
});
|
|
764
|
+
setStatus?.(formatProviderToolStatusLine(event));
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function providerToolMessageKey(event) {
|
|
768
|
+
if (!event?.id) return null;
|
|
769
|
+
return `${event.provider || "provider"}:${event.id}`;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function mergeProviderToolEventData(previous, event) {
|
|
773
|
+
const merged = {
|
|
774
|
+
...(previous || {}),
|
|
775
|
+
...(event || {}),
|
|
776
|
+
};
|
|
777
|
+
if (previous?.input != null && event?.input == null) merged.input = previous.input;
|
|
778
|
+
if (previous?.detail != null && event?.detail == null) merged.detail = previous.detail;
|
|
779
|
+
if (previous?.output != null && event?.output == null) merged.output = previous.output;
|
|
780
|
+
if (previous?.text != null && event?.text == null) merged.text = previous.text;
|
|
781
|
+
merged.data = {
|
|
782
|
+
...(previous?.data || {}),
|
|
783
|
+
...(event?.data || {}),
|
|
784
|
+
};
|
|
785
|
+
return merged;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function providerToolStatus(event) {
|
|
789
|
+
if (event.type === "tool-start" || event.type === "tool-update") return "running";
|
|
790
|
+
if (event.status === "error") return "error";
|
|
791
|
+
return "done";
|
|
792
|
+
}
|
|
793
|
+
|
|
740
794
|
function appendProviderActivity(turn, message, { appendMessage, setStatus }) {
|
|
741
795
|
const text = String(message.text || "").trim();
|
|
742
796
|
if (!text) return;
|
|
@@ -754,21 +808,24 @@ function formatProviderName(provider) {
|
|
|
754
808
|
function formatProviderToolTitle(event) {
|
|
755
809
|
const provider = formatProviderName(event.provider);
|
|
756
810
|
const name = event.name || "tool";
|
|
757
|
-
|
|
758
|
-
if (event.type === "tool-update") return `${provider} updated ${name}`;
|
|
759
|
-
return `${provider} started ${name}`;
|
|
811
|
+
return `${provider} ${name}`;
|
|
760
812
|
}
|
|
761
813
|
|
|
762
814
|
function formatProviderToolText(event) {
|
|
763
815
|
const lines = [];
|
|
764
816
|
if (event.detail) lines.push(String(event.detail));
|
|
765
|
-
if (event.text) lines.push(String(event.text));
|
|
766
817
|
if (event.input) lines.push(formatProviderData("input", event.input));
|
|
767
818
|
if (event.output) lines.push(formatProviderData("output", event.output));
|
|
768
819
|
if (event.status) lines.push(`status: ${event.status}`);
|
|
769
820
|
return lines.filter(Boolean).join("\n") || event.name || "Provider tool activity";
|
|
770
821
|
}
|
|
771
822
|
|
|
823
|
+
function formatProviderToolStatusLine(event) {
|
|
824
|
+
const title = formatProviderToolTitle(event);
|
|
825
|
+
const status = providerToolStatus(event);
|
|
826
|
+
return `${title}: ${status}`;
|
|
827
|
+
}
|
|
828
|
+
|
|
772
829
|
function formatProviderData(label, value) {
|
|
773
830
|
if (value == null) return null;
|
|
774
831
|
if (typeof value === "string") return `${label}: ${value}`;
|
|
@@ -1,14 +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";
|
|
4
|
+
import { renderCodeBlockText } from "./code-block.mjs";
|
|
3
5
|
import { renderMarkdownToAnsi } from "./markdown-block.mjs";
|
|
4
6
|
|
|
5
|
-
export function renderAssistantSnapshotText(snapshot, { cwd = process.cwd(), ansi = false } = {}) {
|
|
6
|
-
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 });
|
|
7
9
|
const lines = [view.title, view.welcome.rows.find(([label]) => label === "Provider")?.[1] || ""]
|
|
8
10
|
.filter(Boolean);
|
|
9
11
|
for (const block of view.blocks || []) {
|
|
10
12
|
lines.push("");
|
|
11
|
-
lines.push(...renderBlockLines(block, { ansi }));
|
|
13
|
+
lines.push(...renderBlockLines(block, { ansi, width: view.terminalWidth || width }));
|
|
12
14
|
}
|
|
13
15
|
lines.push("");
|
|
14
16
|
lines.push(view.statusLine);
|
|
@@ -16,7 +18,7 @@ export function renderAssistantSnapshotText(snapshot, { cwd = process.cwd(), ans
|
|
|
16
18
|
return ansi ? text : stripAnsi(text);
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
function renderBlockLines(block, { ansi = false } = {}) {
|
|
21
|
+
function renderBlockLines(block, { ansi = false, width = 100 } = {}) {
|
|
20
22
|
const marker = block.marker || "";
|
|
21
23
|
const title = block.title ? ` ${block.title}` : "";
|
|
22
24
|
const text = String(block.text || "").trimEnd();
|
|
@@ -25,9 +27,66 @@ function renderBlockLines(block, { ansi = false } = {}) {
|
|
|
25
27
|
const normalized = ansi ? rendered : stripAnsi(rendered);
|
|
26
28
|
return prefixLines(`${marker}${title}`, normalized);
|
|
27
29
|
}
|
|
30
|
+
if (block.format === "command") {
|
|
31
|
+
return renderCommandBlockLines(block, { width });
|
|
32
|
+
}
|
|
28
33
|
return prefixLines(`${marker}${title}`, text);
|
|
29
34
|
}
|
|
30
35
|
|
|
36
|
+
function renderCommandBlockLines(block, { width = 100 } = {}) {
|
|
37
|
+
const header = `${block.marker || ""}${block.title ? ` ${block.title}` : ""}`.trim();
|
|
38
|
+
const body = [];
|
|
39
|
+
const innerWidth = Math.max(1, width - 4);
|
|
40
|
+
const command = formatCommandLine(block.command);
|
|
41
|
+
const status = formatCommandStatus(block);
|
|
42
|
+
if (command) body.push(...wrapText(`$ ${command}`, innerWidth));
|
|
43
|
+
if (status) body.push(truncateText(status, innerWidth));
|
|
44
|
+
if (block.codeBlock) {
|
|
45
|
+
body.push("");
|
|
46
|
+
body.push(...renderCodeBlockText(block.codeBlock).map((line) => truncateText(line, innerWidth)));
|
|
47
|
+
} else {
|
|
48
|
+
for (const line of block.outputPreview?.lines || []) body.push(truncateText(line, innerWidth));
|
|
49
|
+
const omitted = block.outputPreview?.omittedLineCount || block.omittedOutputLineCount || 0;
|
|
50
|
+
if (omitted > 0) body.push(truncateText(`... ${omitted} more line${omitted === 1 ? "" : "s"} omitted`, innerWidth));
|
|
51
|
+
}
|
|
52
|
+
if (!command && body.length === 0 && block.text) body.push(truncateText(block.text, innerWidth));
|
|
53
|
+
return boxLines([header, ...body].filter(Boolean), { width });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatCommandLine(command) {
|
|
57
|
+
if (!command) return null;
|
|
58
|
+
if (typeof command === "string") return command;
|
|
59
|
+
try {
|
|
60
|
+
return JSON.stringify(command);
|
|
61
|
+
} catch {
|
|
62
|
+
return String(command);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatCommandStatus(block) {
|
|
67
|
+
if (block.status === "running") return "running";
|
|
68
|
+
if (block.status === "error") return block.exitCode == null ? "failed" : `failed · exit code ${block.exitCode}`;
|
|
69
|
+
if (block.exitCode != null) return `completed · exit code ${block.exitCode}`;
|
|
70
|
+
if (block.status === "done") return "completed";
|
|
71
|
+
if (block.text && !block.command) return block.text;
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
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)}╯`;
|
|
79
|
+
return [
|
|
80
|
+
top,
|
|
81
|
+
...lines.map((line) => `│ ${line}${" ".repeat(contentWidth - visibleWidth(line))} │`),
|
|
82
|
+
bottom,
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function visibleWidth(value) {
|
|
87
|
+
return stripAnsi(String(value || "")).length;
|
|
88
|
+
}
|
|
89
|
+
|
|
31
90
|
function prefixLines(prefix, text) {
|
|
32
91
|
const lines = String(text || "").split(/\r?\n/);
|
|
33
92
|
if (lines.length === 0 || (lines.length === 1 && lines[0] === "")) return [prefix];
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { formatContextRemaining } from "./context-window.mjs";
|
|
3
3
|
|
|
4
|
+
const PROVIDER_COMMAND_OUTPUT_PREVIEW_LINES = 12;
|
|
5
|
+
const PROVIDER_FILE_READ_OUTPUT_PREVIEW_LINES = 8;
|
|
6
|
+
const PROVIDER_DIFF_PREVIEW_LINES = 80;
|
|
7
|
+
|
|
4
8
|
export function buildAssistantViewModel(snapshot, { cwd = process.cwd(), terminalWidth = 100 } = {}) {
|
|
5
9
|
const providerLabel = buildProviderLabel(snapshot);
|
|
6
10
|
const repoName = path.basename(cwd || process.cwd()) || "repository";
|
|
@@ -56,7 +60,7 @@ export function buildTranscriptBlocks(messages) {
|
|
|
56
60
|
return {
|
|
57
61
|
id: message.id,
|
|
58
62
|
kind: classifyToolBlock(message),
|
|
59
|
-
format: "
|
|
63
|
+
format: "command",
|
|
60
64
|
marker: "●",
|
|
61
65
|
title: message.title || message.toolName || "Tool",
|
|
62
66
|
text: message.text || "",
|
|
@@ -65,7 +69,19 @@ export function buildTranscriptBlocks(messages) {
|
|
|
65
69
|
exitCode: message.data?.exitCode ?? null,
|
|
66
70
|
};
|
|
67
71
|
}
|
|
68
|
-
if (role === "provider-
|
|
72
|
+
if (role === "provider-tool") {
|
|
73
|
+
return {
|
|
74
|
+
id: message.id,
|
|
75
|
+
kind: "provider-command",
|
|
76
|
+
format: "command",
|
|
77
|
+
marker: "◌",
|
|
78
|
+
title: message.title || "provider command",
|
|
79
|
+
text: message.text || "",
|
|
80
|
+
status: message.status || null,
|
|
81
|
+
...buildProviderCommandModel(message),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (role === "provider-activity" || role === "provider-error") {
|
|
69
85
|
return {
|
|
70
86
|
id: message.id,
|
|
71
87
|
kind: role,
|
|
@@ -137,6 +153,123 @@ function classifyToolBlock(message) {
|
|
|
137
153
|
return "tool-result";
|
|
138
154
|
}
|
|
139
155
|
|
|
156
|
+
function buildProviderCommandModel(message) {
|
|
157
|
+
const event = message.data || {};
|
|
158
|
+
const rawCommand = event.input || event.data?.command || event.data?.input || null;
|
|
159
|
+
const output = event.output || event.data?.aggregated_output || event.data?.output || "";
|
|
160
|
+
const exitCode = event.data?.exit_code ?? event.data?.exitCode ?? null;
|
|
161
|
+
const status = message.status || providerCommandStatus(event);
|
|
162
|
+
const diffText = extractProviderDiffText(event);
|
|
163
|
+
const diffPreview = diffText ? summarizeOutput(diffText, PROVIDER_DIFF_PREVIEW_LINES) : null;
|
|
164
|
+
const outputPreview = summarizeOutput(output, providerOutputPreviewLineLimit(event, rawCommand));
|
|
165
|
+
return {
|
|
166
|
+
command: diffPreview ? summarizeProviderEditCommand(event, rawCommand) : rawCommand,
|
|
167
|
+
exitCode,
|
|
168
|
+
codeBlock: diffPreview
|
|
169
|
+
? {
|
|
170
|
+
language: "diff",
|
|
171
|
+
lines: diffPreview.lines,
|
|
172
|
+
omittedLineCount: diffPreview.omittedLineCount,
|
|
173
|
+
}
|
|
174
|
+
: null,
|
|
175
|
+
outputPreview,
|
|
176
|
+
outputLineCount: countLines(output),
|
|
177
|
+
omittedOutputLineCount: outputPreview.omittedLineCount,
|
|
178
|
+
status,
|
|
179
|
+
};
|
|
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
|
+
|
|
199
|
+
function summarizeProviderEditCommand(event, rawCommand) {
|
|
200
|
+
const name = event.name ? String(event.name) : "edit";
|
|
201
|
+
if (name && name !== "command") return name;
|
|
202
|
+
const command = String(rawCommand || "").trim();
|
|
203
|
+
if (!command) return "file edit";
|
|
204
|
+
if (looksLikeDiff(command)) return "file edit";
|
|
205
|
+
return command;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function extractProviderDiffText(event) {
|
|
209
|
+
const candidates = [
|
|
210
|
+
event.input,
|
|
211
|
+
event.detail,
|
|
212
|
+
event.text,
|
|
213
|
+
event.output,
|
|
214
|
+
event.data?.arguments,
|
|
215
|
+
event.data?.input,
|
|
216
|
+
event.data?.patch,
|
|
217
|
+
event.data?.diff,
|
|
218
|
+
event.data?.aggregated_output,
|
|
219
|
+
event.data?.output,
|
|
220
|
+
];
|
|
221
|
+
for (const candidate of candidates) {
|
|
222
|
+
const text = stringifyMaybe(candidate);
|
|
223
|
+
if (looksLikeDiff(text)) return text;
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function stringifyMaybe(value) {
|
|
229
|
+
if (value == null) return "";
|
|
230
|
+
if (typeof value === "string") return value;
|
|
231
|
+
try {
|
|
232
|
+
return JSON.stringify(value, null, 2);
|
|
233
|
+
} catch {
|
|
234
|
+
return String(value);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function looksLikeDiff(value) {
|
|
239
|
+
const text = String(value || "");
|
|
240
|
+
if (!text.trim()) return false;
|
|
241
|
+
if (/\*\*\* Begin Patch[\s\S]*\*\*\* End Patch/.test(text)) return true;
|
|
242
|
+
if (/^diff --git\b/m.test(text)) return true;
|
|
243
|
+
if (/^@@\s/m.test(text)) return true;
|
|
244
|
+
if (/^\+\+\+\s.+\n---\s.+/m.test(text) || /^---\s.+\n\+\+\+\s.+/m.test(text)) return true;
|
|
245
|
+
const changedLines = text.split(/\r?\n/).filter((line) => /^[+-](?![+-]{2})/.test(line));
|
|
246
|
+
return changedLines.length >= 2 && /(patch|diff|apply_patch|edit|update file|add file|delete file)/i.test(text);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function providerCommandStatus(event) {
|
|
250
|
+
if (event.type === "tool-start" || event.type === "tool-update") return "running";
|
|
251
|
+
if (event.status === "error") return "error";
|
|
252
|
+
return event.status ? "done" : null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function summarizeOutput(value, maxLines) {
|
|
256
|
+
const text = String(value || "").replace(/\r/g, "");
|
|
257
|
+
if (!text.trim()) return { lines: [], omittedLineCount: 0 };
|
|
258
|
+
const lines = text.split("\n");
|
|
259
|
+
const trimmedLines = lines.at(-1) === "" ? lines.slice(0, -1) : lines;
|
|
260
|
+
return {
|
|
261
|
+
lines: trimmedLines.slice(0, maxLines),
|
|
262
|
+
omittedLineCount: Math.max(0, trimmedLines.length - maxLines),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function countLines(value) {
|
|
267
|
+
const text = String(value || "").replace(/\r/g, "");
|
|
268
|
+
if (!text.trim()) return 0;
|
|
269
|
+
const lines = text.split("\n");
|
|
270
|
+
return lines.at(-1) === "" ? lines.length - 1 : lines.length;
|
|
271
|
+
}
|
|
272
|
+
|
|
140
273
|
function shortenHome(value) {
|
|
141
274
|
const text = String(value || "");
|
|
142
275
|
const home = process.env.HOME;
|
|
@@ -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",
|