@elench/testkit 0.1.103 → 0.1.105
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 +68 -2
- package/lib/cli/assistant/code-block.mjs +29 -0
- package/lib/cli/assistant/providers/codex.mjs +6 -2
- package/lib/cli/assistant/state.mjs +87 -16
- package/lib/cli/assistant/transcript-text.mjs +57 -0
- package/lib/cli/assistant/view-model.mjs +117 -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 +5 -5
|
@@ -2,6 +2,7 @@ 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";
|
|
@@ -185,6 +186,7 @@ function Transcript({ view }) {
|
|
|
185
186
|
|
|
186
187
|
function renderBlock(block) {
|
|
187
188
|
if (block.format === "markdown") return renderMarkdownBlock(block);
|
|
189
|
+
if (block.format === "command") return renderCommandBlock(block);
|
|
188
190
|
return renderPlainBlock(block);
|
|
189
191
|
}
|
|
190
192
|
|
|
@@ -225,6 +227,70 @@ function renderPlainBlock(block) {
|
|
|
225
227
|
return rendered;
|
|
226
228
|
}
|
|
227
229
|
|
|
230
|
+
function renderCommandBlock(block) {
|
|
231
|
+
const marker = colorMarker(block);
|
|
232
|
+
const title = block.title ? bold(block.title) : bold("command");
|
|
233
|
+
const command = formatCommandLine(block);
|
|
234
|
+
const status = formatCommandStatus(block);
|
|
235
|
+
const codeBlock = block.codeBlock || null;
|
|
236
|
+
const outputLines = codeBlock ? [] : block.outputPreview?.lines || [];
|
|
237
|
+
const omitted = codeBlock
|
|
238
|
+
? codeBlock.omittedLineCount || 0
|
|
239
|
+
: block.outputPreview?.omittedLineCount || block.omittedOutputLineCount || 0;
|
|
240
|
+
|
|
241
|
+
return [
|
|
242
|
+
createElement(
|
|
243
|
+
Box,
|
|
244
|
+
{
|
|
245
|
+
key: `${block.id}-command`,
|
|
246
|
+
borderStyle: "round",
|
|
247
|
+
flexDirection: "column",
|
|
248
|
+
paddingLeft: 1,
|
|
249
|
+
paddingRight: 1,
|
|
250
|
+
marginBottom: 1,
|
|
251
|
+
},
|
|
252
|
+
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,
|
|
255
|
+
codeBlock ? createElement(Text, { key: "code-gap" }, "") : null,
|
|
256
|
+
...(codeBlock ? CodeBlock({ lines: codeBlock.lines, language: codeBlock.language }) : []),
|
|
257
|
+
...outputLines.map((line, index) => (
|
|
258
|
+
createElement(Text, { key: `output-${index}` }, dim(line))
|
|
259
|
+
)),
|
|
260
|
+
omitted > 0 ? createElement(Text, { key: "omitted" }, dim(`… ${omitted} more line${omitted === 1 ? "" : "s"} omitted`)) : null,
|
|
261
|
+
block.text && !command && outputLines.length === 0
|
|
262
|
+
? createElement(Text, { key: "text" }, colorBlockText(block, block.text))
|
|
263
|
+
: null
|
|
264
|
+
),
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function formatCommandLine(block) {
|
|
269
|
+
if (!block.command) return null;
|
|
270
|
+
if (typeof block.command === "string") return block.command;
|
|
271
|
+
try {
|
|
272
|
+
return JSON.stringify(block.command);
|
|
273
|
+
} catch {
|
|
274
|
+
return String(block.command);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function formatCommandStatus(block) {
|
|
279
|
+
const status = block.status || null;
|
|
280
|
+
if (status === "running") return "running";
|
|
281
|
+
if (status === "error") return block.exitCode == null ? "failed" : `failed · exit code ${block.exitCode}`;
|
|
282
|
+
if (block.exitCode != null) return `completed · exit code ${block.exitCode}`;
|
|
283
|
+
if (status === "done") return "completed";
|
|
284
|
+
if (block.text && !block.command) return null;
|
|
285
|
+
return block.text || null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function colorCommandStatus(block, status) {
|
|
289
|
+
if (block.status === "error") return red(status);
|
|
290
|
+
if (block.status === "running") return yellow(status);
|
|
291
|
+
return green(status);
|
|
292
|
+
}
|
|
293
|
+
|
|
228
294
|
function ComposerBar({ view, busy }) {
|
|
229
295
|
const ref = useRef(null);
|
|
230
296
|
const metrics = useBoxMetrics(ref);
|
|
@@ -283,7 +349,7 @@ function colorMarker(block) {
|
|
|
283
349
|
if (block.kind === "system") return red(block.marker);
|
|
284
350
|
if (block.kind === "provider-error") return red(block.marker);
|
|
285
351
|
if (block.kind === "provider-activity") return dim(block.marker);
|
|
286
|
-
if (block.kind === "provider-
|
|
352
|
+
if (block.kind === "provider-command") return yellow(block.marker);
|
|
287
353
|
if (block.kind === "tool-running") return yellow(block.marker);
|
|
288
354
|
if (block.kind === "testkit-run") return green(block.marker);
|
|
289
355
|
return block.marker;
|
|
@@ -294,7 +360,7 @@ function colorBlockText(block, text) {
|
|
|
294
360
|
if (block.kind === "system") return red(text);
|
|
295
361
|
if (block.kind === "provider-error") return red(text);
|
|
296
362
|
if (block.kind === "provider-activity") return dim(text);
|
|
297
|
-
if (block.kind === "provider-
|
|
363
|
+
if (block.kind === "provider-command") return yellow(text);
|
|
298
364
|
if (block.kind === "tool-running") return yellow(text);
|
|
299
365
|
return text;
|
|
300
366
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React, { createElement } from "react";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
import { dim, green, red, cyan } from "../terminal/colors.mjs";
|
|
4
|
+
|
|
5
|
+
export function CodeBlock({ lines = [], language = "text" } = {}) {
|
|
6
|
+
return lines.map((line, index) => (
|
|
7
|
+
createElement(Text, { key: `code-${index}` }, colorCodeLine(line, language))
|
|
8
|
+
));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function renderCodeBlockText({ lines = [], omittedLineCount = 0 } = {}) {
|
|
12
|
+
const rendered = [...lines.map((line) => String(line))];
|
|
13
|
+
if (omittedLineCount > 0) {
|
|
14
|
+
rendered.push(`... ${omittedLineCount} more line${omittedLineCount === 1 ? "" : "s"} omitted`);
|
|
15
|
+
}
|
|
16
|
+
return rendered;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function colorCodeLine(line, language = "text") {
|
|
20
|
+
const text = String(line ?? "");
|
|
21
|
+
if (language !== "diff") return dim(text);
|
|
22
|
+
if (/^\+\+\+/.test(text) || /^---/.test(text) || /^diff --git\b/.test(text) || /^index\b/.test(text)) {
|
|
23
|
+
return cyan(text);
|
|
24
|
+
}
|
|
25
|
+
if (/^\+/.test(text)) return green(text);
|
|
26
|
+
if (/^-/.test(text)) return red(text);
|
|
27
|
+
if (/^@@/.test(text) || /^\*\*\*/.test(text)) return cyan(text);
|
|
28
|
+
return dim(text);
|
|
29
|
+
}
|
|
@@ -182,7 +182,9 @@ function codexItemStartedEvent(item) {
|
|
|
182
182
|
function codexItemUpdatedEvent(item) {
|
|
183
183
|
if (!item || typeof item !== "object") return null;
|
|
184
184
|
if (item.type === "agent_message" && typeof item.text === "string") {
|
|
185
|
-
return providerAssistantDelta(item.text
|
|
185
|
+
return providerAssistantDelta(item.text, {
|
|
186
|
+
id: item.id || null,
|
|
187
|
+
});
|
|
186
188
|
}
|
|
187
189
|
if (item.type === "command_execution") {
|
|
188
190
|
return providerToolUpdate("command", {
|
|
@@ -204,7 +206,9 @@ function codexItemUpdatedEvent(item) {
|
|
|
204
206
|
function codexItemCompletedEvent(item) {
|
|
205
207
|
if (!item || typeof item !== "object") return null;
|
|
206
208
|
if (item.type === "agent_message" && typeof item.text === "string") {
|
|
207
|
-
return providerAssistantFinal(item.text
|
|
209
|
+
return providerAssistantFinal(item.text, {
|
|
210
|
+
id: item.id || null,
|
|
211
|
+
});
|
|
208
212
|
}
|
|
209
213
|
if (item.type === "command_execution") {
|
|
210
214
|
return providerToolEnd("command", {
|
|
@@ -635,6 +635,8 @@ function handleAssistantToolEvent(state, event, appendMessage) {
|
|
|
635
635
|
function createProviderTurnState() {
|
|
636
636
|
return {
|
|
637
637
|
assistantMessageId: null,
|
|
638
|
+
assistantMessageIdsByProviderItem: new Map(),
|
|
639
|
+
providerToolMessageIdsByProviderItem: new Map(),
|
|
638
640
|
lastActivityText: null,
|
|
639
641
|
};
|
|
640
642
|
}
|
|
@@ -673,12 +675,7 @@ function handleProviderEvent(turn, event, { appendMessage, updateMessage, setSta
|
|
|
673
675
|
return;
|
|
674
676
|
}
|
|
675
677
|
if (event.type === "tool-start" || event.type === "tool-update" || event.type === "tool-end") {
|
|
676
|
-
|
|
677
|
-
role: "provider-tool",
|
|
678
|
-
title: formatProviderToolTitle(event),
|
|
679
|
-
text: formatProviderToolText(event),
|
|
680
|
-
data: event,
|
|
681
|
-
}, { appendMessage, setStatus });
|
|
678
|
+
upsertProviderToolActivity(turn, event, { appendMessage, updateMessage, setStatus });
|
|
682
679
|
return;
|
|
683
680
|
}
|
|
684
681
|
if (event.type === "error") {
|
|
@@ -693,15 +690,19 @@ function handleProviderEvent(turn, event, { appendMessage, updateMessage, setSta
|
|
|
693
690
|
}
|
|
694
691
|
|
|
695
692
|
function appendAssistantDelta(turn, event, { appendMessage, updateMessage }) {
|
|
696
|
-
|
|
697
|
-
|
|
693
|
+
const messageKey = providerAssistantMessageKey(event);
|
|
694
|
+
let messageId = messageKey ? turn.assistantMessageIdsByProviderItem.get(messageKey) : turn.assistantMessageId;
|
|
695
|
+
if (!messageId) {
|
|
696
|
+
messageId = appendMessage({
|
|
698
697
|
role: "assistant",
|
|
699
698
|
status: "streaming",
|
|
700
699
|
provider: event.provider || null,
|
|
701
700
|
text: "",
|
|
702
701
|
});
|
|
702
|
+
if (messageKey) turn.assistantMessageIdsByProviderItem.set(messageKey, messageId);
|
|
703
|
+
else turn.assistantMessageId = messageId;
|
|
703
704
|
}
|
|
704
|
-
updateMessage(
|
|
705
|
+
updateMessage(messageId, (message) => ({
|
|
705
706
|
text: `${message.text || ""}${event.text || ""}`,
|
|
706
707
|
status: "streaming",
|
|
707
708
|
}));
|
|
@@ -709,20 +710,87 @@ function appendAssistantDelta(turn, event, { appendMessage, updateMessage }) {
|
|
|
709
710
|
|
|
710
711
|
function finalizeAssistantMessage(turn, event, { appendMessage, updateMessage }) {
|
|
711
712
|
const finalText = event.text || "";
|
|
712
|
-
|
|
713
|
-
|
|
713
|
+
const messageKey = providerAssistantMessageKey(event);
|
|
714
|
+
let messageId = messageKey ? turn.assistantMessageIdsByProviderItem.get(messageKey) : turn.assistantMessageId;
|
|
715
|
+
if (!messageId) {
|
|
716
|
+
messageId = appendMessage({
|
|
714
717
|
role: "assistant",
|
|
715
718
|
provider: event.provider || null,
|
|
716
719
|
text: finalText,
|
|
717
720
|
});
|
|
721
|
+
if (messageKey) turn.assistantMessageIdsByProviderItem.set(messageKey, messageId);
|
|
722
|
+
else turn.assistantMessageId = messageId;
|
|
718
723
|
return;
|
|
719
724
|
}
|
|
720
|
-
updateMessage(
|
|
725
|
+
updateMessage(messageId, (message) => ({
|
|
721
726
|
text: finalText || message.text || "",
|
|
722
727
|
status: null,
|
|
723
728
|
}));
|
|
724
729
|
}
|
|
725
730
|
|
|
731
|
+
function providerAssistantMessageKey(event) {
|
|
732
|
+
if (!event?.id) return null;
|
|
733
|
+
return `${event.provider || "provider"}:${event.id}`;
|
|
734
|
+
}
|
|
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
|
+
|
|
726
794
|
function appendProviderActivity(turn, message, { appendMessage, setStatus }) {
|
|
727
795
|
const text = String(message.text || "").trim();
|
|
728
796
|
if (!text) return;
|
|
@@ -740,21 +808,24 @@ function formatProviderName(provider) {
|
|
|
740
808
|
function formatProviderToolTitle(event) {
|
|
741
809
|
const provider = formatProviderName(event.provider);
|
|
742
810
|
const name = event.name || "tool";
|
|
743
|
-
|
|
744
|
-
if (event.type === "tool-update") return `${provider} updated ${name}`;
|
|
745
|
-
return `${provider} started ${name}`;
|
|
811
|
+
return `${provider} ${name}`;
|
|
746
812
|
}
|
|
747
813
|
|
|
748
814
|
function formatProviderToolText(event) {
|
|
749
815
|
const lines = [];
|
|
750
816
|
if (event.detail) lines.push(String(event.detail));
|
|
751
|
-
if (event.text) lines.push(String(event.text));
|
|
752
817
|
if (event.input) lines.push(formatProviderData("input", event.input));
|
|
753
818
|
if (event.output) lines.push(formatProviderData("output", event.output));
|
|
754
819
|
if (event.status) lines.push(`status: ${event.status}`);
|
|
755
820
|
return lines.filter(Boolean).join("\n") || event.name || "Provider tool activity";
|
|
756
821
|
}
|
|
757
822
|
|
|
823
|
+
function formatProviderToolStatusLine(event) {
|
|
824
|
+
const title = formatProviderToolTitle(event);
|
|
825
|
+
const status = providerToolStatus(event);
|
|
826
|
+
return `${title}: ${status}`;
|
|
827
|
+
}
|
|
828
|
+
|
|
758
829
|
function formatProviderData(label, value) {
|
|
759
830
|
if (value == null) return null;
|
|
760
831
|
if (typeof value === "string") return `${label}: ${value}`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import stripAnsi from "strip-ansi";
|
|
2
2
|
import { buildAssistantViewModel } from "./view-model.mjs";
|
|
3
|
+
import { renderCodeBlockText } from "./code-block.mjs";
|
|
3
4
|
import { renderMarkdownToAnsi } from "./markdown-block.mjs";
|
|
4
5
|
|
|
5
6
|
export function renderAssistantSnapshotText(snapshot, { cwd = process.cwd(), ansi = false } = {}) {
|
|
@@ -25,9 +26,65 @@ function renderBlockLines(block, { ansi = false } = {}) {
|
|
|
25
26
|
const normalized = ansi ? rendered : stripAnsi(rendered);
|
|
26
27
|
return prefixLines(`${marker}${title}`, normalized);
|
|
27
28
|
}
|
|
29
|
+
if (block.format === "command") {
|
|
30
|
+
return renderCommandBlockLines(block);
|
|
31
|
+
}
|
|
28
32
|
return prefixLines(`${marker}${title}`, text);
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
function renderCommandBlockLines(block) {
|
|
36
|
+
const header = `${block.marker || ""}${block.title ? ` ${block.title}` : ""}`.trim();
|
|
37
|
+
const body = [];
|
|
38
|
+
const command = formatCommandLine(block.command);
|
|
39
|
+
const status = formatCommandStatus(block);
|
|
40
|
+
if (command) body.push(`$ ${command}`);
|
|
41
|
+
if (status) body.push(status);
|
|
42
|
+
if (block.codeBlock) {
|
|
43
|
+
body.push("");
|
|
44
|
+
body.push(...renderCodeBlockText(block.codeBlock));
|
|
45
|
+
} else {
|
|
46
|
+
for (const line of block.outputPreview?.lines || []) body.push(String(line));
|
|
47
|
+
const omitted = block.outputPreview?.omittedLineCount || block.omittedOutputLineCount || 0;
|
|
48
|
+
if (omitted > 0) body.push(`... ${omitted} more line${omitted === 1 ? "" : "s"} omitted`);
|
|
49
|
+
}
|
|
50
|
+
if (!command && body.length === 0 && block.text) body.push(String(block.text));
|
|
51
|
+
return boxLines([header, ...body].filter(Boolean));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatCommandLine(command) {
|
|
55
|
+
if (!command) return null;
|
|
56
|
+
if (typeof command === "string") return command;
|
|
57
|
+
try {
|
|
58
|
+
return JSON.stringify(command);
|
|
59
|
+
} catch {
|
|
60
|
+
return String(command);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatCommandStatus(block) {
|
|
65
|
+
if (block.status === "running") return "running";
|
|
66
|
+
if (block.status === "error") return block.exitCode == null ? "failed" : `failed · exit code ${block.exitCode}`;
|
|
67
|
+
if (block.exitCode != null) return `completed · exit code ${block.exitCode}`;
|
|
68
|
+
if (block.status === "done") return "completed";
|
|
69
|
+
if (block.text && !block.command) return block.text;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
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)}╯`;
|
|
77
|
+
return [
|
|
78
|
+
top,
|
|
79
|
+
...lines.map((line) => `│ ${line}${" ".repeat(width - visibleWidth(line))} │`),
|
|
80
|
+
bottom,
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function visibleWidth(value) {
|
|
85
|
+
return stripAnsi(String(value || "")).length;
|
|
86
|
+
}
|
|
87
|
+
|
|
31
88
|
function prefixLines(prefix, text) {
|
|
32
89
|
const lines = String(text || "").split(/\r?\n/);
|
|
33
90
|
if (lines.length === 0 || (lines.length === 1 && lines[0] === "")) return [prefix];
|
|
@@ -1,6 +1,9 @@
|
|
|
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_DIFF_PREVIEW_LINES = 80;
|
|
6
|
+
|
|
4
7
|
export function buildAssistantViewModel(snapshot, { cwd = process.cwd(), terminalWidth = 100 } = {}) {
|
|
5
8
|
const providerLabel = buildProviderLabel(snapshot);
|
|
6
9
|
const repoName = path.basename(cwd || process.cwd()) || "repository";
|
|
@@ -56,7 +59,7 @@ export function buildTranscriptBlocks(messages) {
|
|
|
56
59
|
return {
|
|
57
60
|
id: message.id,
|
|
58
61
|
kind: classifyToolBlock(message),
|
|
59
|
-
format: "
|
|
62
|
+
format: "command",
|
|
60
63
|
marker: "●",
|
|
61
64
|
title: message.title || message.toolName || "Tool",
|
|
62
65
|
text: message.text || "",
|
|
@@ -65,7 +68,19 @@ export function buildTranscriptBlocks(messages) {
|
|
|
65
68
|
exitCode: message.data?.exitCode ?? null,
|
|
66
69
|
};
|
|
67
70
|
}
|
|
68
|
-
if (role === "provider-
|
|
71
|
+
if (role === "provider-tool") {
|
|
72
|
+
return {
|
|
73
|
+
id: message.id,
|
|
74
|
+
kind: "provider-command",
|
|
75
|
+
format: "command",
|
|
76
|
+
marker: "◌",
|
|
77
|
+
title: message.title || "provider command",
|
|
78
|
+
text: message.text || "",
|
|
79
|
+
status: message.status || null,
|
|
80
|
+
...buildProviderCommandModel(message),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (role === "provider-activity" || role === "provider-error") {
|
|
69
84
|
return {
|
|
70
85
|
id: message.id,
|
|
71
86
|
kind: role,
|
|
@@ -137,6 +152,106 @@ function classifyToolBlock(message) {
|
|
|
137
152
|
return "tool-result";
|
|
138
153
|
}
|
|
139
154
|
|
|
155
|
+
function buildProviderCommandModel(message) {
|
|
156
|
+
const event = message.data || {};
|
|
157
|
+
const rawCommand = event.input || event.data?.command || event.data?.input || null;
|
|
158
|
+
const output = event.output || event.data?.aggregated_output || event.data?.output || "";
|
|
159
|
+
const exitCode = event.data?.exit_code ?? event.data?.exitCode ?? null;
|
|
160
|
+
const status = message.status || providerCommandStatus(event);
|
|
161
|
+
const diffText = extractProviderDiffText(event);
|
|
162
|
+
const diffPreview = diffText ? summarizeOutput(diffText, PROVIDER_DIFF_PREVIEW_LINES) : null;
|
|
163
|
+
const outputPreview = summarizeOutput(output, PROVIDER_COMMAND_OUTPUT_PREVIEW_LINES);
|
|
164
|
+
return {
|
|
165
|
+
command: diffPreview ? summarizeProviderEditCommand(event, rawCommand) : rawCommand,
|
|
166
|
+
exitCode,
|
|
167
|
+
codeBlock: diffPreview
|
|
168
|
+
? {
|
|
169
|
+
language: "diff",
|
|
170
|
+
lines: diffPreview.lines,
|
|
171
|
+
omittedLineCount: diffPreview.omittedLineCount,
|
|
172
|
+
}
|
|
173
|
+
: null,
|
|
174
|
+
outputPreview,
|
|
175
|
+
outputLineCount: countLines(output),
|
|
176
|
+
omittedOutputLineCount: outputPreview.omittedLineCount,
|
|
177
|
+
status,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function summarizeProviderEditCommand(event, rawCommand) {
|
|
182
|
+
const name = event.name ? String(event.name) : "edit";
|
|
183
|
+
if (name && name !== "command") return name;
|
|
184
|
+
const command = String(rawCommand || "").trim();
|
|
185
|
+
if (!command) return "file edit";
|
|
186
|
+
if (looksLikeDiff(command)) return "file edit";
|
|
187
|
+
return command;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function extractProviderDiffText(event) {
|
|
191
|
+
const candidates = [
|
|
192
|
+
event.input,
|
|
193
|
+
event.detail,
|
|
194
|
+
event.text,
|
|
195
|
+
event.output,
|
|
196
|
+
event.data?.arguments,
|
|
197
|
+
event.data?.input,
|
|
198
|
+
event.data?.patch,
|
|
199
|
+
event.data?.diff,
|
|
200
|
+
event.data?.aggregated_output,
|
|
201
|
+
event.data?.output,
|
|
202
|
+
];
|
|
203
|
+
for (const candidate of candidates) {
|
|
204
|
+
const text = stringifyMaybe(candidate);
|
|
205
|
+
if (looksLikeDiff(text)) return text;
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function stringifyMaybe(value) {
|
|
211
|
+
if (value == null) return "";
|
|
212
|
+
if (typeof value === "string") return value;
|
|
213
|
+
try {
|
|
214
|
+
return JSON.stringify(value, null, 2);
|
|
215
|
+
} catch {
|
|
216
|
+
return String(value);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function looksLikeDiff(value) {
|
|
221
|
+
const text = String(value || "");
|
|
222
|
+
if (!text.trim()) return false;
|
|
223
|
+
if (/\*\*\* Begin Patch[\s\S]*\*\*\* End Patch/.test(text)) return true;
|
|
224
|
+
if (/^diff --git\b/m.test(text)) return true;
|
|
225
|
+
if (/^@@\s/m.test(text)) return true;
|
|
226
|
+
if (/^\+\+\+\s.+\n---\s.+/m.test(text) || /^---\s.+\n\+\+\+\s.+/m.test(text)) return true;
|
|
227
|
+
const changedLines = text.split(/\r?\n/).filter((line) => /^[+-](?![+-]{2})/.test(line));
|
|
228
|
+
return changedLines.length >= 2 && /(patch|diff|apply_patch|edit|update file|add file|delete file)/i.test(text);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function providerCommandStatus(event) {
|
|
232
|
+
if (event.type === "tool-start" || event.type === "tool-update") return "running";
|
|
233
|
+
if (event.status === "error") return "error";
|
|
234
|
+
return event.status ? "done" : null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function summarizeOutput(value, maxLines) {
|
|
238
|
+
const text = String(value || "").replace(/\r/g, "");
|
|
239
|
+
if (!text.trim()) return { lines: [], omittedLineCount: 0 };
|
|
240
|
+
const lines = text.split("\n");
|
|
241
|
+
const trimmedLines = lines.at(-1) === "" ? lines.slice(0, -1) : lines;
|
|
242
|
+
return {
|
|
243
|
+
lines: trimmedLines.slice(0, maxLines),
|
|
244
|
+
omittedLineCount: Math.max(0, trimmedLines.length - maxLines),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function countLines(value) {
|
|
249
|
+
const text = String(value || "").replace(/\r/g, "");
|
|
250
|
+
if (!text.trim()) return 0;
|
|
251
|
+
const lines = text.split("\n");
|
|
252
|
+
return lines.at(-1) === "" ? lines.length - 1 : lines.length;
|
|
253
|
+
}
|
|
254
|
+
|
|
140
255
|
function shortenHome(value) {
|
|
141
256
|
const text = String(value || "");
|
|
142
257
|
const home = process.env.HOME;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.105",
|
|
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.105"
|
|
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.105",
|
|
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.105",
|
|
94
|
+
"@elench/testkit-bridge": "0.1.105",
|
|
95
|
+
"@elench/testkit-protocol": "0.1.105",
|
|
96
|
+
"@elench/ts-analysis": "0.1.105",
|
|
97
97
|
"@oclif/core": "^4.10.6",
|
|
98
98
|
"esbuild": "^0.25.11",
|
|
99
99
|
"execa": "^9.5.0",
|