pi-minimal 0.3.1 → 0.4.0
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/CHANGELOG.md +14 -0
- package/README.md +5 -2
- package/dist/config.js +19 -8
- package/dist/index.js +2 -1
- package/dist/tools.js +106 -2
- package/package.json +4 -1
- package/release-notes.md +12 -20
- package/src/config.ts +18 -8
- package/src/index.ts +2 -1
- package/src/tools.ts +97 -2
- package/test/config.test.ts +40 -19
- package/test/tools.test.ts +65 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,20 @@ Releases are cut automatically: when the `version` in `package.json` changes on
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.4.0] - 2026-10-27
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `editDiff` setting in `pi-minimal.json`: controls whether an `edit` row shows the
|
|
18
|
+
change itself. `"expanded"` (default) shows the unified diff in the `│`-ruled
|
|
19
|
+
block when the row is expanded with `ctrl+o`, where the bare "Successfully
|
|
20
|
+
replaced" text used to be. `"always"` shows it inline as soon as the edit lands,
|
|
21
|
+
capped at 40 lines with `… N more lines (ctrl+o)` for the rest. `"never"` keeps
|
|
22
|
+
the stats-only row. Lines use the theme's `toolDiffAdded` / `toolDiffRemoved` /
|
|
23
|
+
`toolDiffContext` colours and keep pi's line-number gutter; when one line
|
|
24
|
+
replaces one line, only the changed tokens are inverse-highlighted, as in pi's
|
|
25
|
+
own edit renderer. Failed edits and results without a diff are unchanged.
|
|
26
|
+
|
|
13
27
|
## [0.3.1] - 2026-10-25
|
|
14
28
|
|
|
15
29
|
### Added
|
package/README.md
CHANGED
|
@@ -49,10 +49,13 @@ The package loads JSON in this order:
|
|
|
49
49
|
|
|
50
50
|
Project values override global values. A missing file, omitted key, or invalid value does not override the lower-precedence value.
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Supported keys:
|
|
53
|
+
|
|
54
|
+
- `hideGauntletWidgets` (boolean, default `true`).
|
|
55
|
+
- `editDiff` (`"always"` | `"never"` | `"expanded"`, default `"expanded"`) – when an `edit` row shows its diff. `"expanded"` puts it behind `ctrl+o`; `"always"` shows it inline as the edit lands (first 40 lines, the rest behind `ctrl+o`); `"never"` keeps the `+3 −1` stats only.
|
|
53
56
|
|
|
54
57
|
```json
|
|
55
|
-
{ "hideGauntletWidgets": true }
|
|
58
|
+
{ "hideGauntletWidgets": true, "editDiff": "always" }
|
|
56
59
|
```
|
|
57
60
|
|
|
58
61
|
## Optional footer activity API
|
package/dist/config.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
export const EDIT_DIFF_MODES = ["always", "never", "expanded"];
|
|
4
5
|
const DEFAULTS = {
|
|
5
6
|
hideGauntletWidgets: true,
|
|
7
|
+
editDiff: "expanded",
|
|
6
8
|
};
|
|
7
9
|
export function loadPiMinimalConfig(options) {
|
|
8
10
|
const config = { ...DEFAULTS };
|
|
@@ -32,14 +34,23 @@ function applyLayer(config, path) {
|
|
|
32
34
|
console.error(`pi-minimal: expected an object in ${path}`);
|
|
33
35
|
return;
|
|
34
36
|
}
|
|
37
|
+
// Each key is validated on its own so one bad value does not discard the rest of the layer.
|
|
35
38
|
const record = parsed;
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
if (Object.hasOwn(record, "hideGauntletWidgets")) {
|
|
40
|
+
const value = record.hideGauntletWidgets;
|
|
41
|
+
if (typeof value === "boolean")
|
|
42
|
+
config.hideGauntletWidgets = value;
|
|
43
|
+
else
|
|
44
|
+
console.error(`pi-minimal: hideGauntletWidgets must be a boolean in ${path}`);
|
|
45
|
+
}
|
|
46
|
+
if (Object.hasOwn(record, "editDiff")) {
|
|
47
|
+
const value = record.editDiff;
|
|
48
|
+
if (isEditDiffMode(value))
|
|
49
|
+
config.editDiff = value;
|
|
50
|
+
else
|
|
51
|
+
console.error(`pi-minimal: editDiff must be one of ${EDIT_DIFF_MODES.map((mode) => `"${mode}"`).join(", ")} in ${path}`);
|
|
43
52
|
}
|
|
44
|
-
|
|
53
|
+
}
|
|
54
|
+
function isEditDiffMode(value) {
|
|
55
|
+
return typeof value === "string" && EDIT_DIFF_MODES.includes(value);
|
|
45
56
|
}
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { bindFooterContributionEvents } from "./footer-contribution-events.js";
|
|
|
7
7
|
import { installCwd } from "./cwd.js";
|
|
8
8
|
import { listSiblings, parseAgentList, parseTabList } from "./herdr.js";
|
|
9
9
|
import { hideGauntletWidgets } from "./tracker.js";
|
|
10
|
-
import { installToolRenderers, stopLiveWatch } from "./tools.js";
|
|
10
|
+
import { installToolRenderers, setEditDiffMode, stopLiveWatch } from "./tools.js";
|
|
11
11
|
import { setSessionCwd } from "./tool-rows.js";
|
|
12
12
|
import { applyReclaim, reclaimPolicy } from "./reclaim.js";
|
|
13
13
|
import { installWorkingStatus } from "./working-status.js";
|
|
@@ -170,6 +170,7 @@ export default function piMinimalExtension(pi) {
|
|
|
170
170
|
projectTrusted: ctx.isProjectTrusted(),
|
|
171
171
|
});
|
|
172
172
|
hideWidgets = config.hideGauntletWidgets;
|
|
173
|
+
setEditDiffMode(config.editDiff);
|
|
173
174
|
currentCwd = ctx.cwd;
|
|
174
175
|
setSessionCwd(ctx.cwd);
|
|
175
176
|
try {
|
package/dist/tools.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Container, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui
|
|
|
2
2
|
import { rowIsRunning } from "./activity.js";
|
|
3
3
|
import * as chrome from "./chrome.js";
|
|
4
4
|
import { callSummary, codemodeOutcome, nestedCalls, nestedItem, scriptOutput } from "./codemode-rows.js";
|
|
5
|
+
import { diffWords } from "diff";
|
|
5
6
|
import { createLiveWatch } from "./live-watch.js";
|
|
6
7
|
import { bashBody, formatDuration, formatOutcome, layoutRow, resultText, rowGlyph, toolItem, } from "./tool-rows.js";
|
|
7
8
|
/** Tolerates a stale chrome module after /reload, where only CHROME_MARGIN may exist. */
|
|
@@ -11,6 +12,13 @@ const live = createLiveWatch(400);
|
|
|
11
12
|
const rowIndent = () => margin() + 1;
|
|
12
13
|
/** Max lines of output shown when a row is expanded. */
|
|
13
14
|
const EXPANDED_MAX_LINES = 400;
|
|
15
|
+
/** Max diff lines shown inline under an edit row with `editDiff: "always"`; ctrl+o shows the rest. */
|
|
16
|
+
const INLINE_DIFF_MAX_LINES = 40;
|
|
17
|
+
let editDiffMode = "expanded";
|
|
18
|
+
/** Applies the `editDiff` setting; read at session start, so a change needs a new session or /reload. */
|
|
19
|
+
export function setEditDiffMode(mode) {
|
|
20
|
+
editDiffMode = mode;
|
|
21
|
+
}
|
|
14
22
|
export function stopLiveWatch() {
|
|
15
23
|
live.stop();
|
|
16
24
|
}
|
|
@@ -58,6 +66,97 @@ function outputBlock(kind, result, theme) {
|
|
|
58
66
|
invalidate() { },
|
|
59
67
|
};
|
|
60
68
|
}
|
|
69
|
+
function editDiffText(result) {
|
|
70
|
+
const details = result?.details;
|
|
71
|
+
return typeof details?.diff === "string" && details.diff.trim() ? details.diff : undefined;
|
|
72
|
+
}
|
|
73
|
+
/** pi's edit diff lines: "+123 content", "-123 content", " 123 context"; anything else (e.g. "...") is a separator. */
|
|
74
|
+
function parseDiffLine(line) {
|
|
75
|
+
const match = /^([+\- ])(\s*\d*) (.*)$/.exec(line);
|
|
76
|
+
if (!match)
|
|
77
|
+
return undefined;
|
|
78
|
+
return { prefix: match[1], lineNum: match[2], content: match[3].replace(/\t/g, " ") };
|
|
79
|
+
}
|
|
80
|
+
/** Word-level diff of a 1:1 replacement; changed tokens are inverted, leading indentation is left alone, as in pi. */
|
|
81
|
+
function highlightPair(removed, added, theme) {
|
|
82
|
+
let out = { removed: "", added: "" };
|
|
83
|
+
let firstRemoved = true;
|
|
84
|
+
let firstAdded = true;
|
|
85
|
+
for (const part of diffWords(removed, added)) {
|
|
86
|
+
if (part.removed || part.added) {
|
|
87
|
+
const key = part.removed ? "removed" : "added";
|
|
88
|
+
let value = part.value;
|
|
89
|
+
if (part.removed ? firstRemoved : firstAdded) {
|
|
90
|
+
const ws = /^(\s*)/.exec(value)?.[1] ?? "";
|
|
91
|
+
value = value.slice(ws.length);
|
|
92
|
+
out = { ...out, [key]: out[key] + ws };
|
|
93
|
+
if (part.removed)
|
|
94
|
+
firstRemoved = false;
|
|
95
|
+
else
|
|
96
|
+
firstAdded = false;
|
|
97
|
+
}
|
|
98
|
+
if (value)
|
|
99
|
+
out = { ...out, [key]: out[key] + theme.inverse(value) };
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
out = { removed: out.removed + part.value, added: out.added + part.value };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/** Colours pi's edit diff like pi does: theme diff colours per line, inverse on changed tokens of single-line replacements. */
|
|
108
|
+
function colorDiffLines(lines, theme) {
|
|
109
|
+
const out = [];
|
|
110
|
+
for (let i = 0; i < lines.length;) {
|
|
111
|
+
const parsed = parseDiffLine(lines[i]);
|
|
112
|
+
if (!parsed) {
|
|
113
|
+
out.push(theme.fg("toolDiffContext", lines[i]));
|
|
114
|
+
i++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (parsed.prefix === " ") {
|
|
118
|
+
out.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${parsed.content}`));
|
|
119
|
+
i++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const removed = [];
|
|
123
|
+
const added = [];
|
|
124
|
+
for (let p = parseDiffLine(lines[i] ?? ""); p?.prefix === "-"; p = parseDiffLine(lines[++i] ?? ""))
|
|
125
|
+
removed.push(p);
|
|
126
|
+
for (let p = parseDiffLine(lines[i] ?? ""); p?.prefix === "+"; p = parseDiffLine(lines[++i] ?? ""))
|
|
127
|
+
added.push(p);
|
|
128
|
+
if (removed.length === 1 && added.length === 1) {
|
|
129
|
+
const pair = highlightPair(removed[0].content, added[0].content, theme);
|
|
130
|
+
out.push(theme.fg("toolDiffRemoved", `-${removed[0].lineNum} ${pair.removed}`));
|
|
131
|
+
out.push(theme.fg("toolDiffAdded", `+${added[0].lineNum} ${pair.added}`));
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
for (const line of removed)
|
|
135
|
+
out.push(theme.fg("toolDiffRemoved", `-${line.lineNum} ${line.content}`));
|
|
136
|
+
for (const line of added)
|
|
137
|
+
out.push(theme.fg("toolDiffAdded", `+${line.lineNum} ${line.content}`));
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
/** The edit's diff in the `│`-ruled block. Inline views are capped; expanded views show up to the usual limit. */
|
|
142
|
+
function diffBlock(diff, theme, expanded) {
|
|
143
|
+
const all = diff.replace(/\n$/, "").split("\n");
|
|
144
|
+
const max = expanded ? EXPANDED_MAX_LINES : INLINE_DIFF_MAX_LINES;
|
|
145
|
+
const lines = colorDiffLines(all.length > max ? all.slice(0, max) : all, theme);
|
|
146
|
+
const hidden = all.length - lines.length;
|
|
147
|
+
const indent = " ".repeat(rowIndent() + 2);
|
|
148
|
+
const rule = theme.fg("borderMuted", "│");
|
|
149
|
+
return {
|
|
150
|
+
render(width) {
|
|
151
|
+
const inner = Math.max(0, width - indent.length - 2);
|
|
152
|
+
const out = lines.map((line) => `${indent}${rule} ${truncateToWidth(line, inner)}`);
|
|
153
|
+
if (hidden > 0)
|
|
154
|
+
out.push(`${indent}${rule} ${theme.fg("dim", `… ${hidden} more lines${expanded ? "" : " (ctrl+o)"}`)}`);
|
|
155
|
+
return out;
|
|
156
|
+
},
|
|
157
|
+
invalidate() { },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
61
160
|
function errorLineBlock(line, theme) {
|
|
62
161
|
if (!line)
|
|
63
162
|
return undefined;
|
|
@@ -149,10 +248,15 @@ export function compactRenderers(kind) {
|
|
|
149
248
|
live.unwatch(liveId(renderContext, toolItem(kind, renderContext.args)));
|
|
150
249
|
}
|
|
151
250
|
const container = new Container();
|
|
251
|
+
const expanded = Boolean(options.expanded);
|
|
152
252
|
const errorLine = errorLineBlock(renderContext.state.outcome?.errorLine, theme);
|
|
153
|
-
if (errorLine && !
|
|
253
|
+
if (errorLine && !expanded)
|
|
154
254
|
container.addChild(errorLine);
|
|
155
|
-
|
|
255
|
+
const diff = kind === "edit" && !renderContext.isError && !renderContext.isPartial ? editDiffText(typedResult) : undefined;
|
|
256
|
+
const showDiff = diff !== undefined && (editDiffMode === "always" || (editDiffMode === "expanded" && expanded));
|
|
257
|
+
if (showDiff)
|
|
258
|
+
container.addChild(diffBlock(diff, theme, expanded));
|
|
259
|
+
else if (expanded)
|
|
156
260
|
container.addChild(outputBlock(kind, typedResult, theme));
|
|
157
261
|
return container;
|
|
158
262
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-minimal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -44,5 +44,8 @@
|
|
|
44
44
|
"test": "tsc -p tsconfig.tests.json && node --test test-dist/test/*.test.js",
|
|
45
45
|
"lint": "eslint .",
|
|
46
46
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"diff": "^8.0.4"
|
|
47
50
|
}
|
|
48
51
|
}
|
package/release-notes.md
CHANGED
|
@@ -1,31 +1,23 @@
|
|
|
1
1
|
### Added
|
|
2
|
-
- Compact rows for tools that bring no renderer of their own, such as most MCP tools and
|
|
3
|
-
`subagents_enable`: `✓ mcp re_mcp · query_prometheus query=… step=1m 12 lines · 1.4s`.
|
|
4
|
-
The verb is `mcp`, `agent` (subagent tools) or `tool`; up to three scalar arguments show as
|
|
5
|
-
`key=value`; a short single-line result shows verbatim, otherwise a line count. Failures show
|
|
6
|
-
their first line underneath; `ctrl+o` shows the full result. Tools that draw themselves
|
|
7
|
-
(the `subagent` live view, pi-gauntlet's trackers) keep their own renderer.
|
|
8
2
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- The task tracker line is a segmented bar coloured by status (`Tasks ▰▰▰▰▱ 4/5`) instead of a
|
|
19
|
-
run of check marks; past 12 tasks it scales to 10 segments.
|
|
3
|
+
- `editDiff` setting in `pi-minimal.json`: controls whether an `edit` row shows the
|
|
4
|
+
change itself. `"expanded"` (default) shows the unified diff in the `│`-ruled
|
|
5
|
+
block when the row is expanded with `ctrl+o`, where the bare "Successfully
|
|
6
|
+
replaced" text used to be. `"always"` shows it inline as soon as the edit lands,
|
|
7
|
+
capped at 40 lines with `… N more lines (ctrl+o)` for the rest. `"never"` keeps
|
|
8
|
+
the stats-only row. Lines use the theme's `toolDiffAdded` / `toolDiffRemoved` /
|
|
9
|
+
`toolDiffContext` colours and keep pi's line-number gutter; when one line
|
|
10
|
+
replaces one line, only the changed tokens are inverse-highlighted, as in pi's
|
|
11
|
+
own edit renderer. Failed edits and results without a diff are unchanged.
|
|
20
12
|
|
|
21
13
|
## What's Changed
|
|
22
|
-
* feat:
|
|
14
|
+
* feat: editDiff setting — show edit diffs always, never, or behind ctrl+o (0.4.0) by @raviy06 in https://github.com/raviy06/pi-minimal/pull/18
|
|
23
15
|
|
|
24
16
|
|
|
25
|
-
**Full Changelog**: https://github.com/raviy06/pi-minimal/compare/v0.3.
|
|
17
|
+
**Full Changelog**: https://github.com/raviy06/pi-minimal/compare/v0.3.1...v0.4.0
|
|
26
18
|
|
|
27
19
|
---
|
|
28
20
|
**Install / upgrade**
|
|
29
21
|
```bash
|
|
30
|
-
pi install npm:pi-minimal@0.
|
|
22
|
+
pi install npm:pi-minimal@0.4.0 # or: pi update --extensions
|
|
31
23
|
```
|
package/src/config.ts
CHANGED
|
@@ -2,12 +2,18 @@ import { readFileSync } from "node:fs"
|
|
|
2
2
|
import { join } from "node:path"
|
|
3
3
|
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent"
|
|
4
4
|
|
|
5
|
+
/** When an `edit` row shows its diff: inline as soon as the edit lands, never, or only while expanded with ctrl+o. */
|
|
6
|
+
export type EditDiffMode = "always" | "never" | "expanded"
|
|
7
|
+
export const EDIT_DIFF_MODES: readonly EditDiffMode[] = ["always", "never", "expanded"]
|
|
8
|
+
|
|
5
9
|
export type PiMinimalConfig = {
|
|
6
10
|
hideGauntletWidgets: boolean
|
|
11
|
+
editDiff: EditDiffMode
|
|
7
12
|
}
|
|
8
13
|
|
|
9
14
|
const DEFAULTS: PiMinimalConfig = {
|
|
10
15
|
hideGauntletWidgets: true,
|
|
16
|
+
editDiff: "expanded",
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
export function loadPiMinimalConfig(options: {
|
|
@@ -44,16 +50,20 @@ function applyLayer(config: PiMinimalConfig, path: string): void {
|
|
|
44
50
|
return
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
// Each key is validated on its own so one bad value does not discard the rest of the layer.
|
|
47
54
|
const record = parsed as Record<string, unknown>
|
|
48
|
-
if (
|
|
49
|
-
|
|
55
|
+
if (Object.hasOwn(record, "hideGauntletWidgets")) {
|
|
56
|
+
const value = record.hideGauntletWidgets
|
|
57
|
+
if (typeof value === "boolean") config.hideGauntletWidgets = value
|
|
58
|
+
else console.error(`pi-minimal: hideGauntletWidgets must be a boolean in ${path}`)
|
|
50
59
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
console.error(`pi-minimal:
|
|
55
|
-
return
|
|
60
|
+
if (Object.hasOwn(record, "editDiff")) {
|
|
61
|
+
const value = record.editDiff
|
|
62
|
+
if (isEditDiffMode(value)) config.editDiff = value
|
|
63
|
+
else console.error(`pi-minimal: editDiff must be one of ${EDIT_DIFF_MODES.map((mode) => `"${mode}"`).join(", ")} in ${path}`)
|
|
56
64
|
}
|
|
65
|
+
}
|
|
57
66
|
|
|
58
|
-
|
|
67
|
+
function isEditDiffMode(value: unknown): value is EditDiffMode {
|
|
68
|
+
return typeof value === "string" && (EDIT_DIFF_MODES as readonly string[]).includes(value)
|
|
59
69
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { bindFooterContributionEvents } from "./footer-contribution-events.js"
|
|
|
7
7
|
import { installCwd } from "./cwd.js"
|
|
8
8
|
import { listSiblings, parseAgentList, parseTabList, type Sibling } from "./herdr.js"
|
|
9
9
|
import { hideGauntletWidgets } from "./tracker.js"
|
|
10
|
-
import { installToolRenderers, stopLiveWatch } from "./tools.js"
|
|
10
|
+
import { installToolRenderers, setEditDiffMode, stopLiveWatch } from "./tools.js"
|
|
11
11
|
import { setSessionCwd } from "./tool-rows.js"
|
|
12
12
|
import { applyReclaim, reclaimPolicy, type ReclaimReason } from "./reclaim.js"
|
|
13
13
|
import { installWorkingStatus } from "./working-status.js"
|
|
@@ -167,6 +167,7 @@ export default function piMinimalExtension(pi: ExtensionAPI): void {
|
|
|
167
167
|
projectTrusted: ctx.isProjectTrusted(),
|
|
168
168
|
})
|
|
169
169
|
hideWidgets = config.hideGauntletWidgets
|
|
170
|
+
setEditDiffMode(config.editDiff)
|
|
170
171
|
currentCwd = ctx.cwd
|
|
171
172
|
setSessionCwd(ctx.cwd)
|
|
172
173
|
try {
|
package/src/tools.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { Container, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui
|
|
|
3
3
|
import { rowIsRunning } from "./activity.js"
|
|
4
4
|
import * as chrome from "./chrome.js"
|
|
5
5
|
import { callSummary, codemodeOutcome, nestedCalls, nestedItem, scriptOutput, type NestedCall } from "./codemode-rows.js"
|
|
6
|
+
import { diffWords } from "diff"
|
|
7
|
+
import type { EditDiffMode } from "./config.js"
|
|
6
8
|
import { createLiveWatch } from "./live-watch.js"
|
|
7
9
|
import {
|
|
8
10
|
bashBody,
|
|
@@ -51,6 +53,15 @@ const live = createLiveWatch(400)
|
|
|
51
53
|
const rowIndent = () => margin() + 1
|
|
52
54
|
/** Max lines of output shown when a row is expanded. */
|
|
53
55
|
const EXPANDED_MAX_LINES = 400
|
|
56
|
+
/** Max diff lines shown inline under an edit row with `editDiff: "always"`; ctrl+o shows the rest. */
|
|
57
|
+
const INLINE_DIFF_MAX_LINES = 40
|
|
58
|
+
|
|
59
|
+
let editDiffMode: EditDiffMode = "expanded"
|
|
60
|
+
|
|
61
|
+
/** Applies the `editDiff` setting; read at session start, so a change needs a new session or /reload. */
|
|
62
|
+
export function setEditDiffMode(mode: EditDiffMode): void {
|
|
63
|
+
editDiffMode = mode
|
|
64
|
+
}
|
|
54
65
|
|
|
55
66
|
export function stopLiveWatch(): void {
|
|
56
67
|
live.stop()
|
|
@@ -98,6 +109,86 @@ function outputBlock(kind: ToolKind, result: ToolResultLike | undefined, theme:
|
|
|
98
109
|
}
|
|
99
110
|
}
|
|
100
111
|
|
|
112
|
+
function editDiffText(result: ToolResultLike | undefined): string | undefined {
|
|
113
|
+
const details = result?.details as { diff?: unknown } | undefined
|
|
114
|
+
return typeof details?.diff === "string" && details.diff.trim() ? details.diff : undefined
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type DiffLine = { prefix: "+" | "-" | " "; lineNum: string; content: string }
|
|
118
|
+
|
|
119
|
+
/** pi's edit diff lines: "+123 content", "-123 content", " 123 context"; anything else (e.g. "...") is a separator. */
|
|
120
|
+
function parseDiffLine(line: string): DiffLine | undefined {
|
|
121
|
+
const match = /^([+\- ])(\s*\d*) (.*)$/.exec(line)
|
|
122
|
+
if (!match) return undefined
|
|
123
|
+
return { prefix: match[1] as DiffLine["prefix"], lineNum: match[2]!, content: match[3]!.replace(/\t/g, " ") }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Word-level diff of a 1:1 replacement; changed tokens are inverted, leading indentation is left alone, as in pi. */
|
|
127
|
+
function highlightPair(removed: string, added: string, theme: Theme): { removed: string; added: string } {
|
|
128
|
+
let out = { removed: "", added: "" }
|
|
129
|
+
let firstRemoved = true
|
|
130
|
+
let firstAdded = true
|
|
131
|
+
for (const part of diffWords(removed, added)) {
|
|
132
|
+
if (part.removed || part.added) {
|
|
133
|
+
const key = part.removed ? "removed" : "added"
|
|
134
|
+
let value = part.value
|
|
135
|
+
if (part.removed ? firstRemoved : firstAdded) {
|
|
136
|
+
const ws = /^(\s*)/.exec(value)?.[1] ?? ""
|
|
137
|
+
value = value.slice(ws.length)
|
|
138
|
+
out = { ...out, [key]: out[key] + ws }
|
|
139
|
+
if (part.removed) firstRemoved = false
|
|
140
|
+
else firstAdded = false
|
|
141
|
+
}
|
|
142
|
+
if (value) out = { ...out, [key]: out[key] + theme.inverse(value) }
|
|
143
|
+
} else {
|
|
144
|
+
out = { removed: out.removed + part.value, added: out.added + part.value }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return out
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Colours pi's edit diff like pi does: theme diff colours per line, inverse on changed tokens of single-line replacements. */
|
|
151
|
+
function colorDiffLines(lines: string[], theme: Theme): string[] {
|
|
152
|
+
const out: string[] = []
|
|
153
|
+
for (let i = 0; i < lines.length; ) {
|
|
154
|
+
const parsed = parseDiffLine(lines[i]!)
|
|
155
|
+
if (!parsed) { out.push(theme.fg("toolDiffContext", lines[i]!)); i++; continue }
|
|
156
|
+
if (parsed.prefix === " ") { out.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${parsed.content}`)); i++; continue }
|
|
157
|
+
const removed: DiffLine[] = []
|
|
158
|
+
const added: DiffLine[] = []
|
|
159
|
+
for (let p = parseDiffLine(lines[i] ?? ""); p?.prefix === "-"; p = parseDiffLine(lines[++i] ?? "")) removed.push(p)
|
|
160
|
+
for (let p = parseDiffLine(lines[i] ?? ""); p?.prefix === "+"; p = parseDiffLine(lines[++i] ?? "")) added.push(p)
|
|
161
|
+
if (removed.length === 1 && added.length === 1) {
|
|
162
|
+
const pair = highlightPair(removed[0]!.content, added[0]!.content, theme)
|
|
163
|
+
out.push(theme.fg("toolDiffRemoved", `-${removed[0]!.lineNum} ${pair.removed}`))
|
|
164
|
+
out.push(theme.fg("toolDiffAdded", `+${added[0]!.lineNum} ${pair.added}`))
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
for (const line of removed) out.push(theme.fg("toolDiffRemoved", `-${line.lineNum} ${line.content}`))
|
|
168
|
+
for (const line of added) out.push(theme.fg("toolDiffAdded", `+${line.lineNum} ${line.content}`))
|
|
169
|
+
}
|
|
170
|
+
return out
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The edit's diff in the `│`-ruled block. Inline views are capped; expanded views show up to the usual limit. */
|
|
174
|
+
function diffBlock(diff: string, theme: Theme, expanded: boolean) {
|
|
175
|
+
const all = diff.replace(/\n$/, "").split("\n")
|
|
176
|
+
const max = expanded ? EXPANDED_MAX_LINES : INLINE_DIFF_MAX_LINES
|
|
177
|
+
const lines = colorDiffLines(all.length > max ? all.slice(0, max) : all, theme)
|
|
178
|
+
const hidden = all.length - lines.length
|
|
179
|
+
const indent = " ".repeat(rowIndent() + 2)
|
|
180
|
+
const rule = theme.fg("borderMuted", "│")
|
|
181
|
+
return {
|
|
182
|
+
render(width: number): string[] {
|
|
183
|
+
const inner = Math.max(0, width - indent.length - 2)
|
|
184
|
+
const out = lines.map((line) => `${indent}${rule} ${truncateToWidth(line, inner)}`)
|
|
185
|
+
if (hidden > 0) out.push(`${indent}${rule} ${theme.fg("dim", `… ${hidden} more lines${expanded ? "" : " (ctrl+o)"}`)}`)
|
|
186
|
+
return out
|
|
187
|
+
},
|
|
188
|
+
invalidate() {},
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
101
192
|
function errorLineBlock(line: string | undefined, theme: Theme) {
|
|
102
193
|
if (!line) return undefined
|
|
103
194
|
const indent = " ".repeat(rowIndent() + 2)
|
|
@@ -195,9 +286,13 @@ export function compactRenderers(kind: ToolKind): ToolRenderers {
|
|
|
195
286
|
live.unwatch(liveId(renderContext, toolItem(kind, renderContext.args)))
|
|
196
287
|
}
|
|
197
288
|
const container = new Container()
|
|
289
|
+
const expanded = Boolean((options as RenderOptions).expanded)
|
|
198
290
|
const errorLine = errorLineBlock(renderContext.state.outcome?.errorLine, theme)
|
|
199
|
-
if (errorLine && !
|
|
200
|
-
|
|
291
|
+
if (errorLine && !expanded) container.addChild(errorLine)
|
|
292
|
+
const diff = kind === "edit" && !renderContext.isError && !renderContext.isPartial ? editDiffText(typedResult) : undefined
|
|
293
|
+
const showDiff = diff !== undefined && (editDiffMode === "always" || (editDiffMode === "expanded" && expanded))
|
|
294
|
+
if (showDiff) container.addChild(diffBlock(diff, theme, expanded))
|
|
295
|
+
else if (expanded) container.addChild(outputBlock(kind, typedResult, theme))
|
|
201
296
|
return container
|
|
202
297
|
},
|
|
203
298
|
}
|
package/test/config.test.ts
CHANGED
|
@@ -30,85 +30,106 @@ test("defaults hideGauntletWidgets to true", (t) => {
|
|
|
30
30
|
const { agentDir, cwd } = setup(t)
|
|
31
31
|
const error = t.mock.method(console, "error")
|
|
32
32
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true })
|
|
33
|
-
assert.deepEqual(config, { hideGauntletWidgets: true })
|
|
33
|
+
assert.deepEqual(config, { hideGauntletWidgets: true, editDiff: "expanded" })
|
|
34
34
|
assert.equal(error.mock.callCount(), 0)
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
test("global false overrides default", (t) => {
|
|
38
38
|
const { agentDir, cwd } = setup(t)
|
|
39
|
-
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false }))
|
|
39
|
+
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false, editDiff: "expanded" }))
|
|
40
40
|
const error = t.mock.method(console, "error")
|
|
41
41
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: false })
|
|
42
|
-
assert.deepEqual(config, { hideGauntletWidgets: false })
|
|
42
|
+
assert.deepEqual(config, { hideGauntletWidgets: false, editDiff: "expanded" })
|
|
43
43
|
assert.equal(error.mock.callCount(), 0)
|
|
44
44
|
})
|
|
45
45
|
|
|
46
46
|
test("trusted project true overrides global false", (t) => {
|
|
47
47
|
const { agentDir, cwd } = setup(t)
|
|
48
|
-
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false }))
|
|
49
|
-
writeProject(cwd, JSON.stringify({ hideGauntletWidgets: true }))
|
|
48
|
+
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false, editDiff: "expanded" }))
|
|
49
|
+
writeProject(cwd, JSON.stringify({ hideGauntletWidgets: true, editDiff: "expanded" }))
|
|
50
50
|
const error = t.mock.method(console, "error")
|
|
51
51
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true })
|
|
52
|
-
assert.deepEqual(config, { hideGauntletWidgets: true })
|
|
52
|
+
assert.deepEqual(config, { hideGauntletWidgets: true, editDiff: "expanded" })
|
|
53
53
|
assert.equal(error.mock.callCount(), 0)
|
|
54
54
|
})
|
|
55
55
|
|
|
56
56
|
test("untrusted project false does not override global true", (t) => {
|
|
57
57
|
const { agentDir, cwd } = setup(t)
|
|
58
|
-
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: true }))
|
|
59
|
-
writeProject(cwd, JSON.stringify({ hideGauntletWidgets: false }))
|
|
58
|
+
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: true, editDiff: "expanded" }))
|
|
59
|
+
writeProject(cwd, JSON.stringify({ hideGauntletWidgets: false, editDiff: "expanded" }))
|
|
60
60
|
const error = t.mock.method(console, "error")
|
|
61
61
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: false })
|
|
62
|
-
assert.deepEqual(config, { hideGauntletWidgets: true })
|
|
62
|
+
assert.deepEqual(config, { hideGauntletWidgets: true, editDiff: "expanded" })
|
|
63
63
|
assert.equal(error.mock.callCount(), 0)
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
test("omitted keys do not override", (t) => {
|
|
67
67
|
const { agentDir, cwd } = setup(t)
|
|
68
|
-
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false }))
|
|
68
|
+
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false, editDiff: "expanded" }))
|
|
69
69
|
writeProject(cwd, JSON.stringify({}))
|
|
70
70
|
const error = t.mock.method(console, "error")
|
|
71
71
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true })
|
|
72
|
-
assert.deepEqual(config, { hideGauntletWidgets: false })
|
|
72
|
+
assert.deepEqual(config, { hideGauntletWidgets: false, editDiff: "expanded" })
|
|
73
73
|
assert.equal(error.mock.callCount(), 0)
|
|
74
74
|
})
|
|
75
75
|
|
|
76
76
|
test("unknown keys do not override", (t) => {
|
|
77
77
|
const { agentDir, cwd } = setup(t)
|
|
78
|
-
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false }))
|
|
78
|
+
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false, editDiff: "expanded" }))
|
|
79
79
|
writeProject(cwd, JSON.stringify({ extra: true }))
|
|
80
80
|
const error = t.mock.method(console, "error")
|
|
81
81
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true })
|
|
82
|
-
assert.deepEqual(config, { hideGauntletWidgets: false })
|
|
82
|
+
assert.deepEqual(config, { hideGauntletWidgets: false, editDiff: "expanded" })
|
|
83
83
|
assert.equal(error.mock.callCount(), 0)
|
|
84
84
|
})
|
|
85
85
|
|
|
86
86
|
test("invalid JSON calls console.error and leaves the lower-precedence value unchanged", (t) => {
|
|
87
87
|
const { agentDir, cwd } = setup(t)
|
|
88
|
-
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false }))
|
|
88
|
+
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false, editDiff: "expanded" }))
|
|
89
89
|
writeProject(cwd, "{")
|
|
90
90
|
const error = t.mock.method(console, "error")
|
|
91
91
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true })
|
|
92
|
-
assert.deepEqual(config, { hideGauntletWidgets: false })
|
|
92
|
+
assert.deepEqual(config, { hideGauntletWidgets: false, editDiff: "expanded" })
|
|
93
93
|
assert.ok(error.mock.callCount() >= 1)
|
|
94
94
|
})
|
|
95
95
|
|
|
96
96
|
test("non-object root calls console.error and leaves the lower-precedence value unchanged", (t) => {
|
|
97
97
|
const { agentDir, cwd } = setup(t)
|
|
98
|
-
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false }))
|
|
98
|
+
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false, editDiff: "expanded" }))
|
|
99
99
|
writeProject(cwd, JSON.stringify(["hideGauntletWidgets", false]))
|
|
100
100
|
const error = t.mock.method(console, "error")
|
|
101
101
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true })
|
|
102
|
-
assert.deepEqual(config, { hideGauntletWidgets: false })
|
|
102
|
+
assert.deepEqual(config, { hideGauntletWidgets: false, editDiff: "expanded" })
|
|
103
103
|
assert.ok(error.mock.callCount() >= 1)
|
|
104
104
|
})
|
|
105
105
|
|
|
106
106
|
test("non-boolean hideGauntletWidgets calls console.error and leaves the lower-precedence value unchanged", (t) => {
|
|
107
107
|
const { agentDir, cwd } = setup(t)
|
|
108
|
-
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false }))
|
|
108
|
+
writeGlobal(agentDir, JSON.stringify({ hideGauntletWidgets: false, editDiff: "expanded" }))
|
|
109
109
|
writeProject(cwd, JSON.stringify({ hideGauntletWidgets: "yes" }))
|
|
110
110
|
const error = t.mock.method(console, "error")
|
|
111
111
|
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true })
|
|
112
|
-
assert.deepEqual(config, { hideGauntletWidgets: false })
|
|
112
|
+
assert.deepEqual(config, { hideGauntletWidgets: false, editDiff: "expanded" })
|
|
113
113
|
assert.ok(error.mock.callCount() >= 1)
|
|
114
114
|
})
|
|
115
|
+
|
|
116
|
+
test("editDiff accepts always, never and expanded, layered like other keys", (t) => {
|
|
117
|
+
const { agentDir, cwd } = setup(t)
|
|
118
|
+
writeGlobal(agentDir, JSON.stringify({ editDiff: "always" }))
|
|
119
|
+
writeProject(cwd, JSON.stringify({ editDiff: "never" }))
|
|
120
|
+
const error = t.mock.method(console, "error")
|
|
121
|
+
assert.equal(loadPiMinimalConfig({ agentDir, cwd, projectTrusted: false }).editDiff, "always")
|
|
122
|
+
assert.equal(loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true }).editDiff, "never")
|
|
123
|
+
assert.equal(error.mock.callCount(), 0)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test("an invalid editDiff value is reported and does not override", (t) => {
|
|
127
|
+
const { agentDir, cwd } = setup(t)
|
|
128
|
+
writeGlobal(agentDir, JSON.stringify({ editDiff: "always", hideGauntletWidgets: false }))
|
|
129
|
+
writeProject(cwd, JSON.stringify({ editDiff: "sometimes" }))
|
|
130
|
+
const error = t.mock.method(console, "error")
|
|
131
|
+
const config = loadPiMinimalConfig({ agentDir, cwd, projectTrusted: true })
|
|
132
|
+
assert.deepEqual(config, { hideGauntletWidgets: false, editDiff: "always" })
|
|
133
|
+
assert.equal(error.mock.callCount(), 1)
|
|
134
|
+
assert.match(String(error.mock.calls[0]!.arguments[0]), /editDiff must be one of "always", "never", "expanded"/)
|
|
135
|
+
})
|
package/test/tools.test.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
genericVerb,
|
|
11
11
|
installToolRenderers,
|
|
12
12
|
isToolKind,
|
|
13
|
+
setEditDiffMode,
|
|
13
14
|
stopLiveWatch,
|
|
14
15
|
} from "../src/tools.js"
|
|
15
16
|
|
|
@@ -176,3 +177,67 @@ test("generic renderer draws one row, an error line on failure, and output on ex
|
|
|
176
177
|
const open = r.renderResult!({ content: [{ type: "text", text: "a\nb" }] } as never, { expanded: true, isPartial: false } as never, plainTheme, context as never)
|
|
177
178
|
assert.deepEqual(open.render(90).map((l) => l.trim()), ["│ a", "│ b"])
|
|
178
179
|
})
|
|
180
|
+
|
|
181
|
+
// pi's edit tool emits its own line-numbered form, not a unified diff: "+123 content", "-123 content", " 123 context", "..." between hunks.
|
|
182
|
+
const EDIT_DIFF = " 1 context\n-2 const a = old(x)\n+2 const a = fresh(x)\n 3 context"
|
|
183
|
+
const editResult = { content: [{ type: "text", text: "Successfully replaced 1 block(s)" }], details: { diff: EDIT_DIFF } }
|
|
184
|
+
function editRows(expanded: boolean, result: unknown = editResult, isError = false): string[] {
|
|
185
|
+
const renderers = compactRenderers("edit")
|
|
186
|
+
const context = { args: { path: "a.ts" }, toolCallId: "e1", invalidate() {}, state: {}, isPartial: false, isError, durationMs: 100 } as never
|
|
187
|
+
const theme = {
|
|
188
|
+
fg: (c: string, s: string) => (c.startsWith("toolDiff") ? `<${c.slice(8).toLowerCase()}>${s}` : s),
|
|
189
|
+
inverse: (s: string) => `[${s}]`,
|
|
190
|
+
} as never
|
|
191
|
+
const block = renderers.renderResult!(result as never, { expanded, isPartial: false } as never, theme, context)
|
|
192
|
+
return block.render(80).map((line) => line.trimEnd())
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
test("editDiff=expanded (default) shows the diff only behind ctrl+o, in the theme's diff colours", (t) => {
|
|
196
|
+
t.after(() => setEditDiffMode("expanded"))
|
|
197
|
+
setEditDiffMode("expanded")
|
|
198
|
+
assert.deepEqual(editRows(false), [])
|
|
199
|
+
const rows = editRows(true)
|
|
200
|
+
assert.equal(rows.length, 4)
|
|
201
|
+
assert.match(rows[0]!, /│ <context> 1 context$/)
|
|
202
|
+
assert.match(rows[1]!, /│ <removed>-2 const a = /)
|
|
203
|
+
assert.match(rows[2]!, /│ <added>\+2 const a = /)
|
|
204
|
+
assert.ok(rows.every((row) => !row.includes("Successfully replaced")), "the diff replaces the result text")
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test("a one-line replacement highlights only the changed tokens with inverse", (t) => {
|
|
208
|
+
t.after(() => setEditDiffMode("expanded"))
|
|
209
|
+
setEditDiffMode("expanded")
|
|
210
|
+
const [, removed, added] = editRows(true)
|
|
211
|
+
assert.match(removed!, /-2 const a = \[old\]\(x\)$/)
|
|
212
|
+
assert.match(added!, /\+2 const a = \[fresh\]\(x\)$/)
|
|
213
|
+
const multi = editRows(true, { content: [], details: { diff: "-1 a\n-2 b\n+1 c" } })
|
|
214
|
+
assert.ok(multi.every((row) => !row.includes("[")), "blocks that are not 1:1 are not token-highlighted")
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test("editDiff=always shows the diff inline and editDiff=never only on request falls back to the result text", (t) => {
|
|
218
|
+
t.after(() => setEditDiffMode("expanded"))
|
|
219
|
+
setEditDiffMode("always")
|
|
220
|
+
assert.equal(editRows(false).length, 4)
|
|
221
|
+
assert.equal(editRows(true).length, 4)
|
|
222
|
+
setEditDiffMode("never")
|
|
223
|
+
assert.deepEqual(editRows(false), [])
|
|
224
|
+
assert.match(editRows(true)[0]!, /│ Successfully replaced 1 block\(s\)$/)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test("editDiff=always truncates a long inline diff and leaves the full diff to ctrl+o", (t) => {
|
|
228
|
+
t.after(() => setEditDiffMode("expanded"))
|
|
229
|
+
setEditDiffMode("always")
|
|
230
|
+
const long = { content: [{ type: "text", text: "ok" }], details: { diff: Array.from({ length: 60 }, (_, i) => `+${i + 1} line ${i}`).join("\n") } }
|
|
231
|
+
const inline = editRows(false, long)
|
|
232
|
+
assert.equal(inline.length, 41)
|
|
233
|
+
assert.match(inline[40]!, /… 20 more lines \(ctrl\+o\)$/)
|
|
234
|
+
assert.equal(editRows(true, long).length, 60)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test("edit failures and results without a diff never render a diff block", (t) => {
|
|
238
|
+
t.after(() => setEditDiffMode("expanded"))
|
|
239
|
+
setEditDiffMode("always")
|
|
240
|
+
assert.deepEqual(editRows(false, { content: [{ type: "text", text: "edited" }] }), [])
|
|
241
|
+
const failed = editRows(false, { content: [{ type: "text", text: "oldText not found" }] }, true)
|
|
242
|
+
assert.ok(failed.every((row) => !row.includes("<removed>")))
|
|
243
|
+
})
|