@pi-archimedes/diff 2.12.0 → 2.14.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/README.md +4 -4
- package/package.json +3 -3
- package/src/ansi/codes.test.ts +0 -43
- package/src/ansi/codes.ts +0 -14
- package/src/ansi/index.ts +1 -1
- package/src/config-threading.test.ts +175 -0
- package/src/diff-component.ts +8 -2
- package/src/index.ts +12 -27
- package/src/module-state.test.ts +91 -0
- package/src/render/index.ts +1 -2
- package/src/render/shared.test.ts +17 -26
- package/src/render/shared.ts +2 -16
- package/src/render/split.ts +6 -3
- package/src/render/unified.ts +5 -2
- package/src/shiki.test.ts +10 -22
- package/src/shiki.ts +4 -14
- package/src/tools/edit.test.ts +11 -1
- package/src/tools/edit.ts +10 -6
- package/src/tools/register.test.ts +22 -9
- package/src/tools/write.test.ts +10 -0
- package/src/tools/write.ts +10 -5
package/README.md
CHANGED
|
@@ -24,10 +24,10 @@ New to Pi? Pi itself is a one-time global install and needs Node.js ≥ 22.19.0:
|
|
|
24
24
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
After installing Pi, choose one installation command above, then `cd` into your project and run `pi`. Inside the session, `/login` signs you in and `/model` picks a model — the [setup section](https://github.com/
|
|
27
|
+
After installing Pi, choose one installation command above, then `cd` into your project and run `pi`. Inside the session, `/login` signs you in and `/model` picks a model — the [setup section](https://github.com/archimedes-codes/pi-archimedes#setup) walks through the first run. A running session picks the diff renderer up with `/reload`.
|
|
28
28
|
|
|
29
29
|
<div align="center">
|
|
30
|
-
<img src="https://raw.githubusercontent.com/
|
|
30
|
+
<img src="https://raw.githubusercontent.com/archimedes-codes/pi-archimedes/main/docs/images/diff-edit.png" width="700" alt="Shiki syntax-highlighted split diff">
|
|
31
31
|
</div>
|
|
32
32
|
|
|
33
33
|
## What you get
|
|
@@ -43,7 +43,7 @@ The renderer displays `edit` and `write` tool changes in the tool UI, including
|
|
|
43
43
|
|
|
44
44
|
## Appearance
|
|
45
45
|
|
|
46
|
-
Standalone, the renderer runs on fixed defaults — the `github-dark` theme, a 150-column minimum for split view, 60 code columns per side, and line-wrap safety checks enabled — and it does **not** read settings, because standalone has no config reader. In the [suite](https://github.com/
|
|
46
|
+
Standalone, the renderer runs on fixed defaults — the `github-dark` theme, a 150-column minimum for split view, 60 code columns per side, and line-wrap safety checks enabled — and it does **not** read settings, because standalone has no config reader. In the [suite](https://github.com/archimedes-codes/pi-archimedes), its `archimedes.diff` namespace supplies the settings through the suite's config reader:
|
|
47
47
|
|
|
48
48
|
| Setting | Type | Default | Description |
|
|
49
49
|
|---------|------|---------|-------------|
|
|
@@ -56,4 +56,4 @@ The palette is derived from the chosen Shiki theme; it does **not** automaticall
|
|
|
56
56
|
|
|
57
57
|
On/off is managed by the suite: toggle via `/plugins` (`archimedes.diff.enabled`, default on).
|
|
58
58
|
|
|
59
|
-
← [Back to pi-archimedes](https://github.com/
|
|
59
|
+
← [Back to pi-archimedes](https://github.com/archimedes-codes/pi-archimedes)
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-archimedes/diff",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
|
-
"url": "https://github.com/
|
|
6
|
+
"url": "https://github.com/archimedes-codes/pi-archimedes.git"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"keywords": [
|
|
@@ -37,6 +37,6 @@
|
|
|
37
37
|
"extensions": [
|
|
38
38
|
"./src/index.ts"
|
|
39
39
|
],
|
|
40
|
-
"image": "https://raw.githubusercontent.com/
|
|
40
|
+
"image": "https://raw.githubusercontent.com/archimedes-codes/pi-archimedes/main/docs/images/diff-edit.png"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/ansi/codes.test.ts
CHANGED
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
DEFAULT_DIFF_BG,
|
|
20
20
|
themeCacheKey,
|
|
21
21
|
resolveDiffColors,
|
|
22
|
-
resetDiffColors,
|
|
23
22
|
} from "./codes.js";
|
|
24
23
|
|
|
25
24
|
// ── Exported constants ──────────────────────────────────────────────────────
|
|
@@ -185,19 +184,16 @@ describe("themeCacheKey", () => {
|
|
|
185
184
|
|
|
186
185
|
describe("resolveDiffColors", () => {
|
|
187
186
|
it("returns DEFAULT_DIFF_COLORS when no theme", () => {
|
|
188
|
-
resetDiffColors();
|
|
189
187
|
const result = resolveDiffColors(undefined);
|
|
190
188
|
expect(result).toEqual(DEFAULT_DIFF_COLORS);
|
|
191
189
|
});
|
|
192
190
|
|
|
193
191
|
it("returns DEFAULT_DIFF_COLORS when theme lacks getFgAnsi", () => {
|
|
194
|
-
resetDiffColors();
|
|
195
192
|
const result = resolveDiffColors({ fg: () => "" });
|
|
196
193
|
expect(result).toEqual(DEFAULT_DIFF_COLORS);
|
|
197
194
|
});
|
|
198
195
|
|
|
199
196
|
it("returns theme-derived colors when theme has getFgAnsi", () => {
|
|
200
|
-
resetDiffColors();
|
|
201
197
|
const theme = {
|
|
202
198
|
getFgAnsi: (token: string) => {
|
|
203
199
|
if (token === "toolDiffAdded") return "\x1b[38;2;50;200;50m";
|
|
@@ -218,7 +214,6 @@ describe("resolveDiffColors", () => {
|
|
|
218
214
|
// it (pre-0.99.0) substituted a colour where the theme had said "the
|
|
219
215
|
// terminal's own". Both readings agree on the fix: a terminal default is
|
|
220
216
|
// an answer, not a missing value.
|
|
221
|
-
resetDiffColors();
|
|
222
217
|
for (const token of ["", "\x1b[39m", "\x1b[0m"]) {
|
|
223
218
|
const result = resolveDiffColors({ getFgAnsi: () => token });
|
|
224
219
|
expect(result).toEqual({ fgAdd: token, fgDel: token, fgCtx: token });
|
|
@@ -226,7 +221,6 @@ describe("resolveDiffColors", () => {
|
|
|
226
221
|
});
|
|
227
222
|
|
|
228
223
|
it("substitutes the hardcoded fallback per token, only where the theme has no answer", () => {
|
|
229
|
-
resetDiffColors();
|
|
230
224
|
const theme = {
|
|
231
225
|
getFgAnsi: (token: string) => {
|
|
232
226
|
if (token === "toolDiffAdded") return "\x1b[38;2;50;200;50m";
|
|
@@ -242,44 +236,7 @@ describe("resolveDiffColors", () => {
|
|
|
242
236
|
});
|
|
243
237
|
|
|
244
238
|
it("keeps the hardcoded set for the no-theme-object path", () => {
|
|
245
|
-
resetDiffColors();
|
|
246
239
|
expect(resolveDiffColors(undefined)).toEqual(DEFAULT_DIFF_COLORS);
|
|
247
240
|
expect(resolveDiffColors({})).toEqual(DEFAULT_DIFF_COLORS);
|
|
248
241
|
});
|
|
249
242
|
});
|
|
250
|
-
|
|
251
|
-
// ── resetDiffColors ─────────────────────────────────────────────────────────
|
|
252
|
-
|
|
253
|
-
describe("resetDiffColors", () => {
|
|
254
|
-
it("clears cache so subsequent call re-derives", () => {
|
|
255
|
-
const theme = {
|
|
256
|
-
getFgAnsi: (token: string) => {
|
|
257
|
-
if (token === "toolDiffAdded") return "\x1b[38;2;50;200;50m";
|
|
258
|
-
if (token === "toolDiffRemoved") return "\x1b[38;2;200;50;50m";
|
|
259
|
-
if (token === "toolDiffContext") return "\x1b[38;2;100;100;100m";
|
|
260
|
-
return "";
|
|
261
|
-
},
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
// First call derives colors
|
|
265
|
-
const result1 = resolveDiffColors(theme);
|
|
266
|
-
expect(result1.fgAdd).toBe("\x1b[38;2;50;200;50m");
|
|
267
|
-
|
|
268
|
-
// Reset clears cache
|
|
269
|
-
resetDiffColors();
|
|
270
|
-
|
|
271
|
-
// Now change the theme
|
|
272
|
-
const newTheme = {
|
|
273
|
-
getFgAnsi: (token: string) => {
|
|
274
|
-
if (token === "toolDiffAdded") return "\x1b[38;2;10;100;10m";
|
|
275
|
-
if (token === "toolDiffRemoved") return "\x1b[38;2;100;10;10m";
|
|
276
|
-
if (token === "toolDiffContext") return "\x1b[38;2;50;50;50m";
|
|
277
|
-
return "";
|
|
278
|
-
},
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
// After reset, new theme should be picked up
|
|
282
|
-
const result2 = resolveDiffColors(newTheme);
|
|
283
|
-
expect(result2.fgAdd).toBe("\x1b[38;2;10;100;10m");
|
|
284
|
-
});
|
|
285
|
-
});
|
package/src/ansi/codes.ts
CHANGED
|
@@ -56,9 +56,6 @@ export const DEFAULT_DIFF_BG: DiffBg = {
|
|
|
56
56
|
divider: `${FG_RULE}│\x1b[0m`,
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
// Theme cache key state — lives here since it's used by resolveDiffColors.
|
|
60
|
-
let _lastThemeKey: string | undefined;
|
|
61
|
-
|
|
62
59
|
// ---------------------------------------------------------------------------
|
|
63
60
|
// Regex patterns
|
|
64
61
|
// ---------------------------------------------------------------------------
|
|
@@ -84,11 +81,6 @@ export const DEFAULT_DIFF_COLORS: DiffColors = { fgAdd: FG_ADD, fgDel: FG_DEL, f
|
|
|
84
81
|
// Theme-aware color resolution
|
|
85
82
|
// ---------------------------------------------------------------------------
|
|
86
83
|
|
|
87
|
-
/** Reset auto-derived colors — call when theme changes. */
|
|
88
|
-
export function resetDiffColors(): void {
|
|
89
|
-
_lastThemeKey = undefined;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
84
|
export function themeCacheKey(theme?: any): string {
|
|
93
85
|
if (!theme?.fg) return "no-theme";
|
|
94
86
|
const fgKeys = [
|
|
@@ -130,12 +122,6 @@ function themeFgAnsi(theme: any, token: string): string | undefined {
|
|
|
130
122
|
}
|
|
131
123
|
|
|
132
124
|
export function resolveDiffColors(theme?: any): DiffColors {
|
|
133
|
-
const themeKey = themeCacheKey(theme);
|
|
134
|
-
|
|
135
|
-
// Re-derive theme key cache (bg colors are now passed explicitly via DiffBg)
|
|
136
|
-
if (themeKey !== _lastThemeKey) {
|
|
137
|
-
_lastThemeKey = themeKey;
|
|
138
|
-
}
|
|
139
125
|
if (!theme?.getFgAnsi) return DEFAULT_DIFF_COLORS;
|
|
140
126
|
// Per token, so one unknown token cannot drag the whole row back to the
|
|
141
127
|
// hardcoded set.
|
package/src/ansi/index.ts
CHANGED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config threading (issue #163, the #156 shape-parity card).
|
|
3
|
+
*
|
|
4
|
+
* `registerDiffTools(pi, getTheme, readConfig)` used to park `readConfig` in a
|
|
5
|
+
* module-scope binding and re-point two submodule getters at it. Two sessions in
|
|
6
|
+
* one process therefore shared ONE config slot: after the second registration,
|
|
7
|
+
* the first session's rows rendered with the second session's `diffTheme` and
|
|
8
|
+
* split thresholds — measured flipping a 200-column render from split to unified.
|
|
9
|
+
*
|
|
10
|
+
* The pin drives INSTALLED RENDERERS, never a leaf function. Passing a config
|
|
11
|
+
* straight to `shouldUseSplit` cannot observe the shared holder, so a test
|
|
12
|
+
* written against the leaf reads green on both the broken and the fixed tree and
|
|
13
|
+
* pins nothing.
|
|
14
|
+
*
|
|
15
|
+
* Red on the base tree: session A's reader stops firing the moment session B
|
|
16
|
+
* registers (`expected N to be greater than N`). Green once the reader is
|
|
17
|
+
* captured per registration.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, it, expect } from "vitest";
|
|
21
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
25
|
+
import { registerDiffTools } from "./index.js";
|
|
26
|
+
import type { DiffRenderConfig } from "./render/index.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A `pi` handle that collects every resolver, the way pi collects a LIST of them
|
|
30
|
+
* per extension (same shape as `src/tools/register.test.ts`).
|
|
31
|
+
*/
|
|
32
|
+
function fakePi() {
|
|
33
|
+
const renderers: ((name: string, next: () => any) => any)[] = [];
|
|
34
|
+
const pi = {
|
|
35
|
+
registerTool: () => {},
|
|
36
|
+
registerToolRenderer: (r: (name: string, next: () => any) => any) => {
|
|
37
|
+
renderers.push(r);
|
|
38
|
+
},
|
|
39
|
+
on: () => () => {},
|
|
40
|
+
} as unknown as ExtensionAPI;
|
|
41
|
+
return { pi, renderers };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the way pi does: fold every registered resolver over the previous
|
|
46
|
+
* answer. Both installers push a resolver, so capturing only the last one would
|
|
47
|
+
* overwrite the `edit` answer — the fold is what keeps it.
|
|
48
|
+
*/
|
|
49
|
+
function resolveTool(
|
|
50
|
+
resolvers: readonly ((name: string, next: () => any) => any)[],
|
|
51
|
+
toolName: string,
|
|
52
|
+
) {
|
|
53
|
+
let out: any = { renderShell: "default" };
|
|
54
|
+
for (const r of resolvers) out = r(toolName, () => out) ?? out;
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The theme stub from `src/tools/edit.test.ts` — a token-addressable fake, so
|
|
59
|
+
// nothing here decides a colour by reading an escape sequence.
|
|
60
|
+
const theme = {
|
|
61
|
+
fg: (token: string, text?: string) =>
|
|
62
|
+
text === undefined ? `[${token}]` : `[${token}:${text}]`,
|
|
63
|
+
bold: (text: string) => `**${text}**`,
|
|
64
|
+
bg: (_token: string, text: string) => text,
|
|
65
|
+
italic: (text: string) => text,
|
|
66
|
+
} as any;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A FRESH context per call, exactly as pi hands one to a renderer. A reused ctx
|
|
70
|
+
* would let the `ctx.state._pk` memo short-circuit the row before it ever built
|
|
71
|
+
* a component, and the read we are counting would not happen.
|
|
72
|
+
*/
|
|
73
|
+
function freshCtx() {
|
|
74
|
+
return {
|
|
75
|
+
state: {},
|
|
76
|
+
outputPad: 1,
|
|
77
|
+
argsComplete: true,
|
|
78
|
+
lastComponent: undefined,
|
|
79
|
+
invalidate: () => {},
|
|
80
|
+
isError: false,
|
|
81
|
+
expanded: false,
|
|
82
|
+
} as any;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Render a row the way pi does: build it, then draw it at a real width. */
|
|
86
|
+
function drawRow(renderers: any, args: any, width: number): string[] {
|
|
87
|
+
const component = renderers.renderCall(args, theme, freshCtx());
|
|
88
|
+
return component.render(width);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Let the component's async render promise run to completion. */
|
|
92
|
+
async function settle(): Promise<void> {
|
|
93
|
+
for (let i = 0; i < 5; i++) await new Promise((resolve) => setTimeout(resolve, 0));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function workspace(dir: string, name: string, contents: string): string {
|
|
97
|
+
const file = path.join(dir, name);
|
|
98
|
+
writeFileSync(file, contents);
|
|
99
|
+
return file;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
describe("config threading across two sessions in one process", () => {
|
|
103
|
+
it("keeps session A reading its own config after session B registers (#163)", async () => {
|
|
104
|
+
const dir = mkdtempSync(path.join(tmpdir(), "diff-threading-"));
|
|
105
|
+
try {
|
|
106
|
+
let readsA = 0;
|
|
107
|
+
let readsB = 0;
|
|
108
|
+
|
|
109
|
+
// A wants a wide-enough terminal for the split view and a light theme.
|
|
110
|
+
const configA: DiffRenderConfig = {
|
|
111
|
+
diffTheme: "github-light",
|
|
112
|
+
diffSplitMinWidth: 100,
|
|
113
|
+
diffSplitMinCodeWidth: 30,
|
|
114
|
+
diffSplitWrapCheck: false,
|
|
115
|
+
};
|
|
116
|
+
// B wants a terminal wider than any real one: every render falls back to
|
|
117
|
+
// unified. If A inherits B's reader, A's split view disappears.
|
|
118
|
+
const configB: DiffRenderConfig = {
|
|
119
|
+
diffTheme: "dracula",
|
|
120
|
+
diffSplitMinWidth: 4000,
|
|
121
|
+
diffSplitMinCodeWidth: 30,
|
|
122
|
+
diffSplitWrapCheck: false,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const readerA = () => {
|
|
126
|
+
readsA++;
|
|
127
|
+
return configA;
|
|
128
|
+
};
|
|
129
|
+
const readerB = () => {
|
|
130
|
+
readsB++;
|
|
131
|
+
return configB;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const sessionA = fakePi();
|
|
135
|
+
registerDiffTools(sessionA.pi, () => theme, readerA);
|
|
136
|
+
|
|
137
|
+
const editA = resolveTool(sessionA.renderers, "edit");
|
|
138
|
+
expect(typeof editA?.renderCall).toBe("function");
|
|
139
|
+
|
|
140
|
+
const first = workspace(dir, "first.ts", "const a = 1;\n");
|
|
141
|
+
drawRow(
|
|
142
|
+
editA,
|
|
143
|
+
{ path: first, oldText: "const a = 1;\n", newText: "const b = 2;\n" },
|
|
144
|
+
200,
|
|
145
|
+
);
|
|
146
|
+
await settle();
|
|
147
|
+
|
|
148
|
+
const readsAfterFirst = readsA;
|
|
149
|
+
expect(readsAfterFirst).toBeGreaterThan(0);
|
|
150
|
+
|
|
151
|
+
// A second session, on a DIFFERENT `pi` handle, with a different reader.
|
|
152
|
+
const sessionB = fakePi();
|
|
153
|
+
registerDiffTools(sessionB.pi, () => theme, readerB);
|
|
154
|
+
expect(resolveTool(sessionB.renderers, "edit")?.renderCall).toBeTypeOf("function");
|
|
155
|
+
|
|
156
|
+
// A brand new `edit` row, still rendered by A's resolvers.
|
|
157
|
+
const second = workspace(dir, "second.ts", "const c = 3;\n");
|
|
158
|
+
drawRow(
|
|
159
|
+
editA,
|
|
160
|
+
{ path: second, oldText: "const c = 3;\n", newText: "const d = 4;\n" },
|
|
161
|
+
200,
|
|
162
|
+
);
|
|
163
|
+
await settle();
|
|
164
|
+
|
|
165
|
+
// A's rows must still ask A. On the shared-holder tree this reads
|
|
166
|
+
// `expected N to be greater than N`: B's registration re-pointed the
|
|
167
|
+
// module binding and A's reader never fires again.
|
|
168
|
+
expect(readsA).toBeGreaterThan(readsAfterFirst);
|
|
169
|
+
// A's rows must not have been rendered by B's reader either.
|
|
170
|
+
expect(readsB).toBe(0);
|
|
171
|
+
} finally {
|
|
172
|
+
rmSync(dir, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
package/src/diff-component.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
6
6
|
import * as Ansi from "./ansi/index.js";
|
|
7
7
|
import type { DiffBg, DiffColors } from "./ansi/index.js";
|
|
8
8
|
import { DEFAULT_DIFF_COLORS, DEFAULT_DIFF_BG, deriveBgFromTheme } from "./ansi/index.js";
|
|
9
|
-
import { renderSplitLines, MAX_RENDER_LINES } from "./render/index.js";
|
|
9
|
+
import { renderSplitLines, MAX_RENDER_LINES, type DiffRenderConfig } from "./render/index.js";
|
|
10
10
|
import type { ParsedDiff } from "./core/diff.js";
|
|
11
11
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
@@ -31,9 +31,10 @@ import type { ParsedDiff } from "./core/diff.js";
|
|
|
31
31
|
*
|
|
32
32
|
* ```ts
|
|
33
33
|
* renderResult(result, options, theme, context) {
|
|
34
|
+
* const cfg = readConfig();
|
|
34
35
|
* const d = result.details;
|
|
35
36
|
* if (d?._type === "diff") {
|
|
36
|
-
* const comp = context.lastComponent ?? new DiffComponent(d.diff, d.language, theme);
|
|
37
|
+
* const comp = context.lastComponent ?? new DiffComponent(d.diff, d.language, theme, cfg);
|
|
37
38
|
* return comp;
|
|
38
39
|
* }
|
|
39
40
|
* }
|
|
@@ -43,6 +44,8 @@ export class DiffComponent implements Component {
|
|
|
43
44
|
private diff: ParsedDiff;
|
|
44
45
|
private language: BundledLanguage | undefined;
|
|
45
46
|
private theme: Theme;
|
|
47
|
+
/** The config the row was registered with — never read from module state. */
|
|
48
|
+
private config: DiffRenderConfig;
|
|
46
49
|
private maxLines: number;
|
|
47
50
|
|
|
48
51
|
/** Box wrapper with base background — makes the diff self-contained. */
|
|
@@ -64,11 +67,13 @@ export class DiffComponent implements Component {
|
|
|
64
67
|
diff: ParsedDiff,
|
|
65
68
|
language: BundledLanguage | undefined,
|
|
66
69
|
theme: Theme,
|
|
70
|
+
config: DiffRenderConfig,
|
|
67
71
|
maxLines: number = MAX_RENDER_LINES,
|
|
68
72
|
) {
|
|
69
73
|
this.diff = diff;
|
|
70
74
|
this.language = language;
|
|
71
75
|
this.theme = theme;
|
|
76
|
+
this.config = config;
|
|
72
77
|
this.maxLines = maxLines;
|
|
73
78
|
|
|
74
79
|
// Box provides base background so the diff is self-contained.
|
|
@@ -122,6 +127,7 @@ export class DiffComponent implements Component {
|
|
|
122
127
|
this.diff,
|
|
123
128
|
this.language,
|
|
124
129
|
width,
|
|
130
|
+
this.config,
|
|
125
131
|
this.maxLines,
|
|
126
132
|
dc,
|
|
127
133
|
dbg,
|
package/src/index.ts
CHANGED
|
@@ -7,11 +7,9 @@
|
|
|
7
7
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type { Component, SettingItem } from "@earendil-works/pi-tui";
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import { setConfigGetter as setRenderConfig } from "./render/index.js";
|
|
10
|
+
import type { DiffRenderConfig } from "./render/index.js";
|
|
12
11
|
import { DiffComponent } from "./diff-component.js";
|
|
13
12
|
import type { DiffBg, DiffColors } from "./ansi/index.js";
|
|
14
|
-
import { resetDiffColors } from "./ansi/index.js";
|
|
15
13
|
import { installWriteTool, installEditTool } from "./tools/index.js";
|
|
16
14
|
import { diag } from "./log.js";
|
|
17
15
|
import { reportPiSupport } from "./pi-support.js";
|
|
@@ -27,12 +25,10 @@ export type { DiffBg, DiffColors } from "./ansi/index.js";
|
|
|
27
25
|
// Config
|
|
28
26
|
// ---------------------------------------------------------------------------
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
diffSplitWrapCheck?: boolean;
|
|
35
|
-
}
|
|
28
|
+
// The shape lives in the render layer (`render/shared.ts`), which is what
|
|
29
|
+
// actually consumes it — aliased here so the public name survives and the shape
|
|
30
|
+
// has exactly one declaration.
|
|
31
|
+
export type DiffConfig = DiffRenderConfig;
|
|
36
32
|
|
|
37
33
|
const DEFAULT_DIFF_CONFIG: DiffConfig = {
|
|
38
34
|
diffTheme: "github-dark",
|
|
@@ -41,15 +37,11 @@ const DEFAULT_DIFF_CONFIG: DiffConfig = {
|
|
|
41
37
|
diffSplitWrapCheck: true,
|
|
42
38
|
};
|
|
43
39
|
|
|
44
|
-
let _readConfig: () => DiffConfig = () => DEFAULT_DIFF_CONFIG;
|
|
45
|
-
function getConfig(): DiffConfig { return _readConfig(); }
|
|
46
|
-
|
|
47
40
|
// ---------------------------------------------------------------------------
|
|
48
41
|
// Settings
|
|
49
42
|
// ---------------------------------------------------------------------------
|
|
50
43
|
|
|
51
|
-
export function getDiffSettingsItems(
|
|
52
|
-
const config = overrideConfig ?? getConfig();
|
|
44
|
+
export function getDiffSettingsItems(config: DiffConfig): SettingItem[] {
|
|
53
45
|
return [
|
|
54
46
|
{
|
|
55
47
|
id: "diffTheme",
|
|
@@ -117,8 +109,10 @@ try {
|
|
|
117
109
|
* builds a fresh extension and hands registration a new handle. A module-wide
|
|
118
110
|
* flag would then install for the first session only, and every session after
|
|
119
111
|
* it would get pi's native `edit`/`write` rows and none of the result details
|
|
120
|
-
* the rows read. The
|
|
121
|
-
*
|
|
112
|
+
* the rows read. The config reader is not module state: it is handed to the
|
|
113
|
+
* installers and captured by the rows they register, so a second call on an
|
|
114
|
+
* already-installed handle is a no-op and cannot re-point the first session's
|
|
115
|
+
* rows at another session's config (#163).
|
|
122
116
|
*/
|
|
123
117
|
const _installedExtensions = new WeakSet<object>();
|
|
124
118
|
|
|
@@ -135,15 +129,6 @@ export function registerDiffTools(
|
|
|
135
129
|
// runs again on every session_start.
|
|
136
130
|
reportPiSupport("diff", "1.2.0");
|
|
137
131
|
|
|
138
|
-
_readConfig = readConfig;
|
|
139
|
-
|
|
140
|
-
// Reset theme-derived colors so they are re-derived from the current theme
|
|
141
|
-
resetDiffColors();
|
|
142
|
-
|
|
143
|
-
// Wire config getters into submodules
|
|
144
|
-
setShikiConfig(() => getConfig());
|
|
145
|
-
setRenderConfig(() => getConfig());
|
|
146
|
-
|
|
147
132
|
// Already installed on this extension: re-registering would stack a duplicate
|
|
148
133
|
// resolver and handler set. A different handle is a different extension, so
|
|
149
134
|
// it installs even when this module instance is a reused one.
|
|
@@ -168,8 +153,8 @@ export function registerDiffTools(
|
|
|
168
153
|
// The rows are chosen by pi's renderer resolution, and the result details
|
|
169
154
|
// each row reads arrive through `tool_result` (ADR 0029). Neither registers a
|
|
170
155
|
// tool, so pi's own `edit` and `write` stay in place.
|
|
171
|
-
installEditTool(pi, cwd, home);
|
|
172
|
-
installWriteTool(pi, cwd, home, _TextComponent);
|
|
156
|
+
installEditTool(pi, cwd, home, readConfig);
|
|
157
|
+
installWriteTool(pi, cwd, home, _TextComponent, readConfig);
|
|
173
158
|
_installedExtensions.add(pi);
|
|
174
159
|
}
|
|
175
160
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-state shape pin for `packages/diff` (issue #163).
|
|
3
|
+
*
|
|
4
|
+
* `packages/core/src/session-state-discipline.test.ts` is the cross-package
|
|
5
|
+
* guard, but it is INTRA-FILE: it follows a session handler's body and the
|
|
6
|
+
* functions that body calls in the same file, so it was satisfied here by
|
|
7
|
+
* assigning `_readConfig` one line away from the handler that wrote it. This
|
|
8
|
+
* file asserts the SOURCE SHAPE instead, so it cannot be satisfied by moving an
|
|
9
|
+
* assignment to another file or one line further down.
|
|
10
|
+
*
|
|
11
|
+
* What is banned: a module-scope `let`/`var` holding a config getter, a config
|
|
12
|
+
* value, or a theme key, and any exported `setConfigGetter` — the setter being
|
|
13
|
+
* the tell that the binding is re-pointed per session rather than owned by the
|
|
14
|
+
* process.
|
|
15
|
+
*
|
|
16
|
+
* What stays allowed, deliberately: `_TextComponent` (`index.ts` — loaded once
|
|
17
|
+
* at module evaluation, never re-pointed) and `_shikiReady` (`shiki.ts` — an
|
|
18
|
+
* honest "the Shiki warm-up has run" flag). Both are pinned below so this file
|
|
19
|
+
* cannot quietly become a blanket "no module state" rule.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { describe, it, expect } from "vitest";
|
|
23
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
24
|
+
import { join, relative } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
|
|
27
|
+
const SRC_ROOT = fileURLToPath(new URL("./", import.meta.url));
|
|
28
|
+
|
|
29
|
+
/** Every non-test `.ts` file under `packages/diff/src`. */
|
|
30
|
+
function sourceFiles(dir: string = SRC_ROOT): string[] {
|
|
31
|
+
const out: string[] = [];
|
|
32
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
33
|
+
const full = join(dir, entry.name);
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
out.push(...sourceFiles(full));
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!entry.isFile() || !entry.name.endsWith(".ts") || entry.name.endsWith(".test.ts")) continue;
|
|
39
|
+
out.push(full);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A module-scope binding holding config, a config reader, or a theme key. */
|
|
45
|
+
const CONFIG_HOLDER = /^\s*(?:export\s+)?(?:let|var)\s+_?(?:getConfig|_readConfig|_lastThemeKey)\b/;
|
|
46
|
+
|
|
47
|
+
/** The exported setter that re-points such a binding per session. */
|
|
48
|
+
const CONFIG_SETTER = /^\s*export\s+(?:function|const)\s+setConfigGetter\b/;
|
|
49
|
+
|
|
50
|
+
function violations(files: readonly string[]): string[] {
|
|
51
|
+
const found: string[] = [];
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
const rel = relative(SRC_ROOT, file).replace(/\\/g, "/");
|
|
54
|
+
const lines = readFileSync(file, "utf8").split("\n");
|
|
55
|
+
lines.forEach((line, i) => {
|
|
56
|
+
if (CONFIG_HOLDER.test(line)) {
|
|
57
|
+
found.push(`${rel}:${i + 1} [config holder at module scope] ${line.trim()}`);
|
|
58
|
+
}
|
|
59
|
+
if (CONFIG_SETTER.test(line)) {
|
|
60
|
+
found.push(`${rel}:${i + 1} [per-session config setter exported] ${line.trim()}`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("diff module state (#163)", () => {
|
|
68
|
+
it("scans a non-empty file set (so the guard cannot go vacuous)", () => {
|
|
69
|
+
const files = sourceFiles();
|
|
70
|
+
expect(files.length).toBeGreaterThan(5);
|
|
71
|
+
for (const file of files) expect(statSync(file).isFile()).toBe(true);
|
|
72
|
+
expect(files.map((f) => relative(SRC_ROOT, f).replace(/\\/g, "/"))).toContain("shiki.ts");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("declares no module-scope config holder and exports no setConfigGetter", () => {
|
|
76
|
+
expect(violations(sourceFiles())).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("still allows the two honest process-wide bindings", () => {
|
|
80
|
+
const index = readFileSync(join(SRC_ROOT, "index.ts"), "utf8");
|
|
81
|
+
const shiki = readFileSync(join(SRC_ROOT, "shiki.ts"), "utf8");
|
|
82
|
+
expect(index).toMatch(/^let _TextComponent:/m);
|
|
83
|
+
expect(shiki).toMatch(/^let _shikiReady = false;$/m);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("keeps shiki.ts free of any import from index.js or render/ (no cycle)", () => {
|
|
87
|
+
const shiki = readFileSync(join(SRC_ROOT, "shiki.ts"), "utf8");
|
|
88
|
+
expect(shiki).not.toMatch(/from\s+["']\.\/index\.js["']/);
|
|
89
|
+
expect(shiki).not.toMatch(/from\s+["']\.\/render\//);
|
|
90
|
+
});
|
|
91
|
+
});
|
package/src/render/index.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { describe, it, expect
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { parseDiff } from "../core/diff.js";
|
|
3
3
|
import {
|
|
4
4
|
shouldUseSplit,
|
|
5
|
-
setConfigGetter,
|
|
6
|
-
getConfig,
|
|
7
5
|
adaptiveWrapRows,
|
|
8
6
|
wrapAnsi,
|
|
9
7
|
} from "./shared.js";
|
|
@@ -48,44 +46,41 @@ describe("wrapAnsi", () => {
|
|
|
48
46
|
});
|
|
49
47
|
|
|
50
48
|
describe("render shared / shouldUseSplit", () => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
// The config used to arrive through a module-scope getter; it is now a
|
|
50
|
+
// parameter, so each case passes the object it previously installed.
|
|
51
|
+
const cfg = {
|
|
52
|
+
diffTheme: "github-dark",
|
|
53
|
+
diffSplitMinWidth: 150,
|
|
54
|
+
diffSplitMinCodeWidth: 60,
|
|
55
|
+
diffSplitWrapCheck: true,
|
|
56
|
+
};
|
|
58
57
|
|
|
59
58
|
it("returns false for empty diff", () => {
|
|
60
59
|
const diff = parseDiff("", "");
|
|
61
|
-
expect(shouldUseSplit(diff, 200)).toBe(false);
|
|
60
|
+
expect(shouldUseSplit(diff, 200, cfg)).toBe(false);
|
|
62
61
|
});
|
|
63
62
|
|
|
64
63
|
it("returns false if terminal width < diffSplitMinWidth", () => {
|
|
65
64
|
const old = "const x = 1;\n";
|
|
66
65
|
const updated = "const x = 2;\n";
|
|
67
66
|
const diff = parseDiff(old, updated);
|
|
68
|
-
expect(shouldUseSplit(diff, 140)).toBe(false);
|
|
67
|
+
expect(shouldUseSplit(diff, 140, cfg)).toBe(false);
|
|
69
68
|
});
|
|
70
69
|
|
|
71
70
|
it("returns false if available code column width < diffSplitMinCodeWidth", () => {
|
|
72
|
-
|
|
73
|
-
diffSplitMinWidth: 100,
|
|
74
|
-
diffSplitMinCodeWidth: 60,
|
|
75
|
-
diffSplitWrapCheck: true,
|
|
76
|
-
}));
|
|
71
|
+
const narrow = { ...cfg, diffSplitMinWidth: 100, diffSplitMinCodeWidth: 60 };
|
|
77
72
|
const old = "const x = 1;\n";
|
|
78
73
|
const updated = "const x = 2;\n";
|
|
79
74
|
const diff = parseDiff(old, updated);
|
|
80
75
|
// With tw=110, half=54, nw=2, gw=7, cw=47 < 60 -> false
|
|
81
|
-
expect(shouldUseSplit(diff, 110)).toBe(false);
|
|
76
|
+
expect(shouldUseSplit(diff, 110, narrow)).toBe(false);
|
|
82
77
|
});
|
|
83
78
|
|
|
84
79
|
it("returns true for normal short lines when terminal width and code width fit", () => {
|
|
85
80
|
const old = "const x = 1;\n";
|
|
86
81
|
const updated = "const x = 2;\n";
|
|
87
82
|
const diff = parseDiff(old, updated);
|
|
88
|
-
expect(shouldUseSplit(diff, 200)).toBe(true);
|
|
83
|
+
expect(shouldUseSplit(diff, 200, cfg)).toBe(true);
|
|
89
84
|
});
|
|
90
85
|
|
|
91
86
|
it("falls back to false when lines wrap and diffSplitWrapCheck is true", () => {
|
|
@@ -93,18 +88,14 @@ describe("render shared / shouldUseSplit", () => {
|
|
|
93
88
|
const longLineNew = "const longLine = '" + "b".repeat(150) + "';\n";
|
|
94
89
|
const diff = parseDiff(longLineOld, longLineNew);
|
|
95
90
|
// In a 200 col terminal, cw ~ 92, line length > 150, all lines wrap (ratio = 1.0 >= 0.2)
|
|
96
|
-
expect(shouldUseSplit(diff, 200)).toBe(false);
|
|
91
|
+
expect(shouldUseSplit(diff, 200, cfg)).toBe(false);
|
|
97
92
|
});
|
|
98
93
|
|
|
99
94
|
it("returns true when lines wrap but diffSplitWrapCheck is false", () => {
|
|
100
|
-
|
|
101
|
-
diffSplitMinWidth: 150,
|
|
102
|
-
diffSplitMinCodeWidth: 60,
|
|
103
|
-
diffSplitWrapCheck: false,
|
|
104
|
-
}));
|
|
95
|
+
const noWrapCheck = { ...cfg, diffSplitWrapCheck: false };
|
|
105
96
|
const longLineOld = "const longLine = '" + "a".repeat(150) + "';\n";
|
|
106
97
|
const longLineNew = "const longLine = '" + "b".repeat(150) + "';\n";
|
|
107
98
|
const diff = parseDiff(longLineOld, longLineNew);
|
|
108
|
-
expect(shouldUseSplit(diff, 200)).toBe(true);
|
|
99
|
+
expect(shouldUseSplit(diff, 200, noWrapCheck)).toBe(true);
|
|
109
100
|
});
|
|
110
101
|
});
|
package/src/render/shared.ts
CHANGED
|
@@ -24,25 +24,12 @@ export const DEFAULT_TERM_WIDTH = 200;
|
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
|
|
26
26
|
export interface DiffRenderConfig {
|
|
27
|
+
diffTheme: string;
|
|
27
28
|
diffSplitMinWidth: number;
|
|
28
29
|
diffSplitMinCodeWidth: number;
|
|
29
30
|
diffSplitWrapCheck?: boolean;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
export function getConfig(): DiffRenderConfig {
|
|
33
|
-
return typeof _getConfig === "function" ? _getConfig() : DEFAULT_CONFIG;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const DEFAULT_CONFIG: DiffRenderConfig = {
|
|
37
|
-
diffSplitMinWidth: 150,
|
|
38
|
-
diffSplitMinCodeWidth: 60,
|
|
39
|
-
diffSplitWrapCheck: true,
|
|
40
|
-
};
|
|
41
|
-
let _getConfig: (() => DiffRenderConfig) | undefined;
|
|
42
|
-
export function setConfigGetter(fn: () => DiffRenderConfig): void {
|
|
43
|
-
_getConfig = fn;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
33
|
// ---------------------------------------------------------------------------
|
|
47
34
|
// Helpers
|
|
48
35
|
// ---------------------------------------------------------------------------
|
|
@@ -109,9 +96,8 @@ export function wrapAnsi(s: string, w: number, maxRows = adaptiveWrapRows(w), fi
|
|
|
109
96
|
return rows;
|
|
110
97
|
}
|
|
111
98
|
|
|
112
|
-
export function shouldUseSplit(diff: ParsedDiff, tw: number, maxRows = MAX_PREVIEW_LINES): boolean {
|
|
99
|
+
export function shouldUseSplit(diff: ParsedDiff, tw: number, cfg: DiffRenderConfig, maxRows = MAX_PREVIEW_LINES): boolean {
|
|
113
100
|
if (!diff.lines.length) return false;
|
|
114
|
-
const cfg = getConfig();
|
|
115
101
|
if (tw < cfg.diffSplitMinWidth) return false;
|
|
116
102
|
|
|
117
103
|
const nw = Math.max(2, String(Math.max(...diff.lines.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length);
|
package/src/render/split.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
MAX_RENDER_LINES,
|
|
14
14
|
WORD_DIFF_MIN_SIM,
|
|
15
15
|
DEFAULT_TERM_WIDTH,
|
|
16
|
+
type DiffRenderConfig,
|
|
16
17
|
adaptiveWrapRows,
|
|
17
18
|
wrapAnsi,
|
|
18
19
|
shouldUseSplit,
|
|
@@ -30,11 +31,12 @@ import { renderUnifiedLines } from "./unified.js";
|
|
|
30
31
|
export async function renderSplit(
|
|
31
32
|
diff: ParsedDiff,
|
|
32
33
|
language: BundledLanguage | undefined,
|
|
34
|
+
cfg: DiffRenderConfig,
|
|
33
35
|
max = MAX_PREVIEW_LINES,
|
|
34
36
|
dc: DiffColors = DEFAULT_DIFF_COLORS,
|
|
35
37
|
): Promise<string> {
|
|
36
38
|
const width = process.stdout.columns ?? DEFAULT_TERM_WIDTH;
|
|
37
|
-
const lines = await renderSplitLines(diff, language, width, max, dc, DEFAULT_DIFF_BG);
|
|
39
|
+
const lines = await renderSplitLines(diff, language, width, cfg, max, dc, DEFAULT_DIFF_BG);
|
|
38
40
|
return lines.join("\n");
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -42,6 +44,7 @@ export async function renderSplitLines(
|
|
|
42
44
|
diff: ParsedDiff,
|
|
43
45
|
language: BundledLanguage | undefined,
|
|
44
46
|
width: number,
|
|
47
|
+
cfg: DiffRenderConfig,
|
|
45
48
|
max = MAX_PREVIEW_LINES,
|
|
46
49
|
dc: DiffColors = DEFAULT_DIFF_COLORS,
|
|
47
50
|
dbg: DiffBg = DEFAULT_DIFF_BG,
|
|
@@ -49,7 +52,7 @@ export async function renderSplitLines(
|
|
|
49
52
|
const rst = dbg.rst;
|
|
50
53
|
const bgBase = dbg.bgBase;
|
|
51
54
|
|
|
52
|
-
if (!shouldUseSplit(diff, width, max)) return renderUnifiedLines(diff, language, width, max, dc, dbg);
|
|
55
|
+
if (!shouldUseSplit(diff, width, cfg, max)) return renderUnifiedLines(diff, language, width, cfg, max, dc, dbg);
|
|
53
56
|
if (!diff.lines.length) return [];
|
|
54
57
|
|
|
55
58
|
type Row = { left: DiffLine | null; right: DiffLine | null };
|
|
@@ -80,7 +83,7 @@ export async function renderSplitLines(
|
|
|
80
83
|
if (r.right && r.right.type !== "sep") rightSrc.push(r.right.content);
|
|
81
84
|
}
|
|
82
85
|
const [leftHL, rightHL] = canHL
|
|
83
|
-
? await Promise.all([hlBlock(leftSrc.join("\n"), language), hlBlock(rightSrc.join("\n"), language)])
|
|
86
|
+
? await Promise.all([hlBlock(leftSrc.join("\n"), language, cfg.diffTheme), hlBlock(rightSrc.join("\n"), language, cfg.diffTheme)])
|
|
84
87
|
: [leftSrc, rightSrc];
|
|
85
88
|
|
|
86
89
|
let lI = 0, rI = 0;
|
package/src/render/unified.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
MAX_RENDER_LINES,
|
|
13
13
|
WORD_DIFF_MIN_SIM,
|
|
14
14
|
DEFAULT_TERM_WIDTH,
|
|
15
|
+
type DiffRenderConfig,
|
|
15
16
|
adaptiveWrapRows,
|
|
16
17
|
wrapAnsi,
|
|
17
18
|
} from "./shared.js";
|
|
@@ -27,11 +28,12 @@ import {
|
|
|
27
28
|
export async function renderUnified(
|
|
28
29
|
diff: ParsedDiff,
|
|
29
30
|
language: BundledLanguage | undefined,
|
|
31
|
+
cfg: DiffRenderConfig,
|
|
30
32
|
max = MAX_RENDER_LINES,
|
|
31
33
|
dc: DiffColors = DEFAULT_DIFF_COLORS,
|
|
32
34
|
): Promise<string> {
|
|
33
35
|
const width = process.stdout.columns ?? DEFAULT_TERM_WIDTH;
|
|
34
|
-
const lines = await renderUnifiedLines(diff, language, width, max, dc, DEFAULT_DIFF_BG);
|
|
36
|
+
const lines = await renderUnifiedLines(diff, language, width, cfg, max, dc, DEFAULT_DIFF_BG);
|
|
35
37
|
return lines.join("\n");
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -39,6 +41,7 @@ export async function renderUnifiedLines(
|
|
|
39
41
|
diff: ParsedDiff,
|
|
40
42
|
language: BundledLanguage | undefined,
|
|
41
43
|
width: number,
|
|
44
|
+
cfg: DiffRenderConfig,
|
|
42
45
|
max = MAX_RENDER_LINES,
|
|
43
46
|
dc: DiffColors = DEFAULT_DIFF_COLORS,
|
|
44
47
|
dbg: DiffBg = DEFAULT_DIFF_BG,
|
|
@@ -61,7 +64,7 @@ export async function renderUnifiedLines(
|
|
|
61
64
|
if (l.type === "ctx" || l.type === "add") newSrc.push(l.content);
|
|
62
65
|
}
|
|
63
66
|
const [oldHL, newHL] = canHL
|
|
64
|
-
? await Promise.all([hlBlock(oldSrc.join("\n"), language), hlBlock(newSrc.join("\n"), language)])
|
|
67
|
+
? await Promise.all([hlBlock(oldSrc.join("\n"), language, cfg.diffTheme), hlBlock(newSrc.join("\n"), language, cfg.diffTheme)])
|
|
65
68
|
: [oldSrc, newSrc];
|
|
66
69
|
|
|
67
70
|
let oI = 0, nI = 0, idx = 0;
|
package/src/shiki.test.ts
CHANGED
|
@@ -15,7 +15,7 @@ vi.mock("./ansi/index.js", () => ({
|
|
|
15
15
|
|
|
16
16
|
// ── Imports ──────────────────────────────────────────────────────────────────
|
|
17
17
|
|
|
18
|
-
import { lang
|
|
18
|
+
import { lang } from "./shiki.js";
|
|
19
19
|
|
|
20
20
|
// ── lang ─────────────────────────────────────────────────────────────────────
|
|
21
21
|
|
|
@@ -231,33 +231,21 @@ describe("lang", () => {
|
|
|
231
231
|
});
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
-
// ──
|
|
234
|
+
// ── hlBlock ──────────────────────────────────────────────────────────────────
|
|
235
235
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
// The theme used to arrive through a module-scope config getter that these tests
|
|
237
|
+
// re-pointed; `hlBlock` now takes it directly (#163), so the two cases that
|
|
238
|
+
// exercised real highlighting survive as explicit-theme calls.
|
|
239
|
+
describe("hlBlock", () => {
|
|
240
|
+
it("returns an array of lines for a highlighted block", async () => {
|
|
239
241
|
const { hlBlock } = await import("./shiki.js");
|
|
240
|
-
|
|
241
|
-
setConfigGetter(() => ({ diffTheme: "github-light" }));
|
|
242
|
-
|
|
243
|
-
// The config getter should now return the new theme
|
|
244
|
-
// We can't directly inspect _getConfig, but we can verify hlBlock
|
|
245
|
-
// doesn't throw and uses the new config
|
|
246
|
-
const result = await hlBlock("const x = 1;", "javascript");
|
|
242
|
+
const result = await hlBlock("const x = 1;", "javascript", "github-dark");
|
|
247
243
|
expect(Array.isArray(result)).toBe(true);
|
|
248
244
|
});
|
|
249
245
|
|
|
250
|
-
it("
|
|
251
|
-
setConfigGetter(() => ({ diffTheme: "nord" }));
|
|
252
|
-
|
|
246
|
+
it("highlights a second language under its own theme", async () => {
|
|
253
247
|
const { hlBlock } = await import("./shiki.js");
|
|
254
|
-
const result = await hlBlock("print('hello')", "python");
|
|
248
|
+
const result = await hlBlock("print('hello')", "python", "github-dark");
|
|
255
249
|
expect(Array.isArray(result)).toBe(true);
|
|
256
250
|
});
|
|
257
|
-
|
|
258
|
-
it("restores default behavior after setting", () => {
|
|
259
|
-
setConfigGetter(() => ({ diffTheme: "github-dark" }));
|
|
260
|
-
// Should not throw
|
|
261
|
-
expect(() => setConfigGetter(() => ({ diffTheme: "one-dark" }))).not.toThrow();
|
|
262
|
-
});
|
|
263
251
|
});
|
package/src/shiki.ts
CHANGED
|
@@ -6,15 +6,6 @@ import type { BundledLanguage, BundledTheme } from "shiki";
|
|
|
6
6
|
|
|
7
7
|
import { normalizeShikiContrast } from "./ansi/index.js";
|
|
8
8
|
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Config access
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
let _getConfig: () => { diffTheme: string } = () => ({ diffTheme: "github-dark" });
|
|
14
|
-
export function setConfigGetter(fn: () => { diffTheme: string }): void {
|
|
15
|
-
_getConfig = fn;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
9
|
// ---------------------------------------------------------------------------
|
|
19
10
|
// Language detection
|
|
20
11
|
// ---------------------------------------------------------------------------
|
|
@@ -43,10 +34,10 @@ const MAX_HL_CHARS = 80_000;
|
|
|
43
34
|
const CACHE_LIMIT = 192;
|
|
44
35
|
|
|
45
36
|
let _shikiReady = false;
|
|
46
|
-
async function ensureShiki(): Promise<void> {
|
|
37
|
+
async function ensureShiki(theme: string): Promise<void> {
|
|
47
38
|
if (_shikiReady) return;
|
|
48
39
|
_shikiReady = true;
|
|
49
|
-
codeToANSI("", "typescript",
|
|
40
|
+
codeToANSI("", "typescript", theme as BundledTheme).catch(() => {});
|
|
50
41
|
}
|
|
51
42
|
|
|
52
43
|
const _cache = new Map<string, string[]>();
|
|
@@ -63,13 +54,12 @@ function _touch(k: string, v: string[]): string[] {
|
|
|
63
54
|
return v;
|
|
64
55
|
}
|
|
65
56
|
|
|
66
|
-
export async function hlBlock(code: string, language: BundledLanguage | undefined): Promise<string[]> {
|
|
57
|
+
export async function hlBlock(code: string, language: BundledLanguage | undefined, theme: string): Promise<string[]> {
|
|
67
58
|
if (!code) return [""];
|
|
68
59
|
if (!language || code.length > MAX_HL_CHARS) return code.split("\n");
|
|
69
60
|
|
|
70
|
-
await ensureShiki();
|
|
61
|
+
await ensureShiki(theme);
|
|
71
62
|
|
|
72
|
-
const theme = _getConfig().diffTheme;
|
|
73
63
|
const k = `${theme}\0${language}\0${code}`;
|
|
74
64
|
const hit = _cache.get(k);
|
|
75
65
|
if (hit) return _touch(k, hit); // Promote to most-recently-used
|
package/src/tools/edit.test.ts
CHANGED
|
@@ -14,6 +14,15 @@ import path from "node:path";
|
|
|
14
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { createEditTool } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { createEditRenderers, installEditTool } from "./edit.js";
|
|
17
|
+
import type { DiffRenderConfig } from "../render/index.js";
|
|
18
|
+
|
|
19
|
+
/** The config the rows read — now threaded per registration rather than held per module (#163). */
|
|
20
|
+
const DIFF_TEST_CONFIG: DiffRenderConfig = {
|
|
21
|
+
diffTheme: "github-dark",
|
|
22
|
+
diffSplitMinWidth: 150,
|
|
23
|
+
diffSplitMinCodeWidth: 60,
|
|
24
|
+
diffSplitWrapCheck: true,
|
|
25
|
+
};
|
|
17
26
|
|
|
18
27
|
// `diff` is standalone and must not import `@pi-archimedes/core` (see
|
|
19
28
|
// `src/pi-support.ts`), so the tests strip escape sequences themselves.
|
|
@@ -39,6 +48,7 @@ function captureTool(cwd: string, home: string) {
|
|
|
39
48
|
} as unknown as ExtensionAPI,
|
|
40
49
|
cwd,
|
|
41
50
|
home,
|
|
51
|
+
() => DIFF_TEST_CONFIG,
|
|
42
52
|
);
|
|
43
53
|
return resolved;
|
|
44
54
|
}
|
|
@@ -185,7 +195,7 @@ describe("edit row padding", () => {
|
|
|
185
195
|
// The row draws its own shell (`renderShell: "self"`), so the inset is ours to
|
|
186
196
|
// choose — but pi still tells the renderer how much tool output is padded
|
|
187
197
|
// here, and that number is what the row must respect.
|
|
188
|
-
const renderers = createEditRenderers("/project", "/home");
|
|
198
|
+
const renderers = createEditRenderers("/project", "/home", () => DIFF_TEST_CONFIG);
|
|
189
199
|
const callArgs = { path: "/project/src/a.ts", edits: [{ oldText: "x", newText: "y" }] };
|
|
190
200
|
|
|
191
201
|
function padXFor(outputPad: number): number {
|
package/src/tools/edit.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { parseDiff } from "../core/diff.js";
|
|
|
7
7
|
import * as Ansi from "../ansi/index.js";
|
|
8
8
|
import { themeCacheKey } from "../ansi/index.js";
|
|
9
9
|
import { lang } from "../shiki.js";
|
|
10
|
-
import { MAX_PREVIEW_LINES } from "../render/index.js";
|
|
10
|
+
import { MAX_PREVIEW_LINES, type DiffRenderConfig } from "../render/index.js";
|
|
11
11
|
import { DiffComponent } from "../diff-component.js";
|
|
12
12
|
|
|
13
13
|
interface EditOperation { oldText: string; newText: string }
|
|
@@ -75,13 +75,17 @@ export function editResultDetails(event: {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/** The Archimedes `edit` renderers, closed over the paths they shorten against. */
|
|
78
|
-
export function createEditRenderers(cwd: string, home: string): ToolRenderers {
|
|
78
|
+
export function createEditRenderers(cwd: string, home: string, readConfig: () => DiffRenderConfig): ToolRenderers {
|
|
79
79
|
const sp = (p: string) => Ansi.shortPath(cwd, home, p);
|
|
80
80
|
|
|
81
81
|
return {
|
|
82
82
|
renderShell: "self",
|
|
83
83
|
|
|
84
84
|
renderCall(args: any, theme: any, ctx: any) {
|
|
85
|
+
// Resolved once per invocation, not per row: the reader belongs to the
|
|
86
|
+
// session that registered this row, so it is captured here rather than
|
|
87
|
+
// read back out of module state (#163).
|
|
88
|
+
const cfg = readConfig();
|
|
85
89
|
const fp = args?.path ?? args?.file_path ?? "";
|
|
86
90
|
const operations = getEditOperations(args);
|
|
87
91
|
|
|
@@ -126,7 +130,7 @@ export function createEditRenderers(cwd: string, home: string): ToolRenderers {
|
|
|
126
130
|
const diff = diffs[0];
|
|
127
131
|
if (diff) {
|
|
128
132
|
shell.addChild(new Spacer(1));
|
|
129
|
-
shell.addChild(new DiffComponent(diff, lg, theme, MAX_PREVIEW_LINES));
|
|
133
|
+
shell.addChild(new DiffComponent(diff, lg, theme, cfg, MAX_PREVIEW_LINES));
|
|
130
134
|
}
|
|
131
135
|
} else {
|
|
132
136
|
const maxShown = Math.min(operations.length, 3);
|
|
@@ -139,7 +143,7 @@ export function createEditRenderers(cwd: string, home: string): ToolRenderers {
|
|
|
139
143
|
theme.fg("muted", `Edit ${i + 1}/${operations.length}`),
|
|
140
144
|
0, 0,
|
|
141
145
|
));
|
|
142
|
-
shell.addChild(new DiffComponent(diff, lg, theme, previewLines));
|
|
146
|
+
shell.addChild(new DiffComponent(diff, lg, theme, cfg, previewLines));
|
|
143
147
|
}
|
|
144
148
|
const remainder = operations.length - maxShown;
|
|
145
149
|
if (remainder > 0) {
|
|
@@ -213,8 +217,8 @@ export function createEditRenderers(cwd: string, home: string): ToolRenderers {
|
|
|
213
217
|
* handlers per extension, so calling it from `session_start` would stack a
|
|
214
218
|
* duplicate on every `/reload`.
|
|
215
219
|
*/
|
|
216
|
-
export function installEditTool(pi: ExtensionAPI, cwd: string, home: string): void {
|
|
217
|
-
const renderers = createEditRenderers(cwd, home);
|
|
220
|
+
export function installEditTool(pi: ExtensionAPI, cwd: string, home: string, readConfig: () => DiffRenderConfig): void {
|
|
221
|
+
const renderers = createEditRenderers(cwd, home, readConfig);
|
|
218
222
|
pi.registerToolRenderer((toolName, next) =>
|
|
219
223
|
toolName === "edit" ? { ...next(), ...renderers } : next(),
|
|
220
224
|
);
|
|
@@ -18,6 +18,19 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
18
18
|
import { Text } from "@earendil-works/pi-tui";
|
|
19
19
|
import { installEditTool } from "./edit.js";
|
|
20
20
|
import { installWriteTool } from "./write.js";
|
|
21
|
+
import type { DiffRenderConfig } from "../render/index.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The config reader both installers now take as their last argument (#163): it
|
|
25
|
+
* is captured by the rows each registration builds, never parked in module
|
|
26
|
+
* state where a second session could re-point it.
|
|
27
|
+
*/
|
|
28
|
+
const cfg = (): DiffRenderConfig => ({
|
|
29
|
+
diffTheme: "github-dark",
|
|
30
|
+
diffSplitMinWidth: 150,
|
|
31
|
+
diffSplitMinCodeWidth: 60,
|
|
32
|
+
diffSplitWrapCheck: true,
|
|
33
|
+
});
|
|
21
34
|
|
|
22
35
|
/** A `pi` handle that records what was registered. */
|
|
23
36
|
function fakePi() {
|
|
@@ -69,7 +82,7 @@ const emit = (
|
|
|
69
82
|
describe("edit registration", () => {
|
|
70
83
|
it("registers no tool and resolves only `edit`", () => {
|
|
71
84
|
const { pi, renderers, registered } = fakePi();
|
|
72
|
-
installEditTool(pi, "/work", "/home");
|
|
85
|
+
installEditTool(pi, "/work", "/home", cfg);
|
|
73
86
|
|
|
74
87
|
expect(registered).toHaveLength(0);
|
|
75
88
|
expect(renderers).toHaveLength(1);
|
|
@@ -79,7 +92,7 @@ describe("edit registration", () => {
|
|
|
79
92
|
|
|
80
93
|
it("keeps the built-in's own renderShell while replacing the renderers", () => {
|
|
81
94
|
const { pi, renderers } = fakePi();
|
|
82
|
-
installEditTool(pi, "/work", "/home");
|
|
95
|
+
installEditTool(pi, "/work", "/home", cfg);
|
|
83
96
|
|
|
84
97
|
const { result } = resolve(renderers[0], "edit");
|
|
85
98
|
expect(typeof result.renderCall).toBe("function");
|
|
@@ -92,7 +105,7 @@ describe("edit registration", () => {
|
|
|
92
105
|
const { dir, file, done } = makeWorkspace("one\ntwo\nthree\n");
|
|
93
106
|
try {
|
|
94
107
|
const { pi, handlers } = fakePi();
|
|
95
|
-
installEditTool(pi, dir, dir);
|
|
108
|
+
installEditTool(pi, dir, dir, cfg);
|
|
96
109
|
writeFileSync(file, "one\n2\nthree\n");
|
|
97
110
|
|
|
98
111
|
const out = emit(handlers, "tool_result", {
|
|
@@ -117,7 +130,7 @@ describe("edit registration", () => {
|
|
|
117
130
|
const { dir, file, done } = makeWorkspace();
|
|
118
131
|
try {
|
|
119
132
|
const { pi, handlers } = fakePi();
|
|
120
|
-
installEditTool(pi, dir, dir);
|
|
133
|
+
installEditTool(pi, dir, dir, cfg);
|
|
121
134
|
|
|
122
135
|
const out = emit(handlers, "tool_result", {
|
|
123
136
|
type: "tool_result",
|
|
@@ -146,7 +159,7 @@ describe("edit registration", () => {
|
|
|
146
159
|
const { dir, file, done } = makeWorkspace();
|
|
147
160
|
try {
|
|
148
161
|
const { pi, handlers } = fakePi();
|
|
149
|
-
installEditTool(pi, dir, dir);
|
|
162
|
+
installEditTool(pi, dir, dir, cfg);
|
|
150
163
|
|
|
151
164
|
const out = emit(handlers, "tool_result", {
|
|
152
165
|
type: "tool_result",
|
|
@@ -168,7 +181,7 @@ describe("edit registration", () => {
|
|
|
168
181
|
describe("write registration", () => {
|
|
169
182
|
it("registers no tool and resolves only `write`", () => {
|
|
170
183
|
const { pi, renderers, registered } = fakePi();
|
|
171
|
-
installWriteTool(pi, "/work", "/home", Text as any);
|
|
184
|
+
installWriteTool(pi, "/work", "/home", Text as any, cfg);
|
|
172
185
|
|
|
173
186
|
expect(registered).toHaveLength(0);
|
|
174
187
|
expect(resolve(renderers[0], "write").result?.renderShell).toBe("default");
|
|
@@ -179,7 +192,7 @@ describe("write registration", () => {
|
|
|
179
192
|
const { dir, file, done } = makeWorkspace("one\ntwo\n");
|
|
180
193
|
try {
|
|
181
194
|
const { pi, handlers } = fakePi();
|
|
182
|
-
installWriteTool(pi, dir, dir, Text as any);
|
|
195
|
+
installWriteTool(pi, dir, dir, Text as any, cfg);
|
|
183
196
|
|
|
184
197
|
emit(handlers, "tool_call", {
|
|
185
198
|
type: "tool_call",
|
|
@@ -210,7 +223,7 @@ describe("write registration", () => {
|
|
|
210
223
|
try {
|
|
211
224
|
const fresh = path.join(dir, "fresh.txt");
|
|
212
225
|
const { pi, handlers } = fakePi();
|
|
213
|
-
installWriteTool(pi, dir, dir, Text as any);
|
|
226
|
+
installWriteTool(pi, dir, dir, Text as any, cfg);
|
|
214
227
|
|
|
215
228
|
emit(handlers, "tool_call", {
|
|
216
229
|
type: "tool_call",
|
|
@@ -239,7 +252,7 @@ describe("write registration", () => {
|
|
|
239
252
|
const { dir, file, done } = makeWorkspace("same\n");
|
|
240
253
|
try {
|
|
241
254
|
const { pi, handlers } = fakePi();
|
|
242
|
-
installWriteTool(pi, dir, dir, Text as any);
|
|
255
|
+
installWriteTool(pi, dir, dir, Text as any, cfg);
|
|
243
256
|
|
|
244
257
|
emit(handlers, "tool_call", {
|
|
245
258
|
type: "tool_call",
|
package/src/tools/write.test.ts
CHANGED
|
@@ -15,6 +15,15 @@ import path from "node:path";
|
|
|
15
15
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { Text } from "@earendil-works/pi-tui";
|
|
17
17
|
import { installWriteTool } from "./write.js";
|
|
18
|
+
import type { DiffRenderConfig } from "../render/index.js";
|
|
19
|
+
|
|
20
|
+
/** The config the rows read — now threaded per registration rather than held per module (#163). */
|
|
21
|
+
const DIFF_TEST_CONFIG: DiffRenderConfig = {
|
|
22
|
+
diffTheme: "github-dark",
|
|
23
|
+
diffSplitMinWidth: 150,
|
|
24
|
+
diffSplitMinCodeWidth: 60,
|
|
25
|
+
diffSplitWrapCheck: true,
|
|
26
|
+
};
|
|
18
27
|
|
|
19
28
|
// `diff` is standalone and must not import `@pi-archimedes/core` (see
|
|
20
29
|
// `src/pi-support.ts`), so the tests strip escape sequences themselves.
|
|
@@ -41,6 +50,7 @@ function captureTool(cwd: string, home: string) {
|
|
|
41
50
|
cwd,
|
|
42
51
|
home,
|
|
43
52
|
Text as any,
|
|
53
|
+
() => DIFF_TEST_CONFIG,
|
|
44
54
|
);
|
|
45
55
|
return resolved;
|
|
46
56
|
}
|
package/src/tools/write.ts
CHANGED
|
@@ -8,7 +8,7 @@ import * as Ansi from "../ansi/index.js";
|
|
|
8
8
|
import { themeCacheKey } from "../ansi/index.js";
|
|
9
9
|
import { lang, hlBlock } from "../shiki.js";
|
|
10
10
|
import { DiffComponent } from "../diff-component.js";
|
|
11
|
-
import { MAX_RENDER_LINES } from "../render/index.js";
|
|
11
|
+
import { MAX_RENDER_LINES, type DiffRenderConfig } from "../render/index.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* The content a `write` call displaces, read while the call is still about to
|
|
@@ -67,11 +67,14 @@ export function createWriteRenderers(
|
|
|
67
67
|
cwd: string,
|
|
68
68
|
home: string,
|
|
69
69
|
TextComponent: new (text?: string, paddingX?: number, paddingY?: number) => Component,
|
|
70
|
+
readConfig: () => DiffRenderConfig,
|
|
70
71
|
): ToolRenderers {
|
|
71
72
|
const sp = (p: string) => Ansi.shortPath(cwd, home, p);
|
|
72
73
|
|
|
73
74
|
return {
|
|
74
75
|
renderCall(args: any, theme: any, ctx: any) {
|
|
76
|
+
// Once per invocation — see the same note in `edit.ts` (#163).
|
|
77
|
+
const cfg = readConfig();
|
|
75
78
|
const fp = args?.path ?? args?.file_path ?? "";
|
|
76
79
|
const isNew = !fp || !existsSync(fp);
|
|
77
80
|
const label = isNew ? "create" : "write";
|
|
@@ -90,7 +93,7 @@ export function createWriteRenderers(
|
|
|
90
93
|
ctx.state._previewKey = previewKey;
|
|
91
94
|
ctx.state._previewText = hdr;
|
|
92
95
|
const lg = lang(fp);
|
|
93
|
-
hlBlock(args.content, lg)
|
|
96
|
+
hlBlock(args.content, lg, cfg.diffTheme)
|
|
94
97
|
.then((lines: string[]) => {
|
|
95
98
|
if (ctx.state._previewKey !== previewKey) return;
|
|
96
99
|
const maxShow = ctx.expanded ? lines.length : 16;
|
|
@@ -112,6 +115,7 @@ export function createWriteRenderers(
|
|
|
112
115
|
},
|
|
113
116
|
|
|
114
117
|
renderResult(result: any, _opt: any, theme: any, ctx: any) {
|
|
118
|
+
const cfg = readConfig();
|
|
115
119
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
116
120
|
if (ctx.isError) {
|
|
117
121
|
const e = result.content
|
|
@@ -125,7 +129,7 @@ export function createWriteRenderers(
|
|
|
125
129
|
if (d?._type === "diff") {
|
|
126
130
|
const comp = ctx.lastComponent instanceof DiffComponent
|
|
127
131
|
? ctx.lastComponent
|
|
128
|
-
: new DiffComponent(d.diff, d.language, theme, MAX_RENDER_LINES);
|
|
132
|
+
: new DiffComponent(d.diff, d.language, theme, cfg, MAX_RENDER_LINES);
|
|
129
133
|
return comp;
|
|
130
134
|
}
|
|
131
135
|
if (d?._type === "noChange") {
|
|
@@ -140,7 +144,7 @@ export function createWriteRenderers(
|
|
|
140
144
|
ctx.state._nft = ` ${theme.fg("success", `✓ new file (${lineCount} lines)`)}`;
|
|
141
145
|
const lg = lang(fp);
|
|
142
146
|
if (rawContent) {
|
|
143
|
-
hlBlock(rawContent, lg)
|
|
147
|
+
hlBlock(rawContent, lg, cfg.diffTheme)
|
|
144
148
|
.then((hlLines: string[]) => {
|
|
145
149
|
if (ctx.state._nfk !== pk) return;
|
|
146
150
|
const maxShow = ctx.expanded ? hlLines.length : 12;
|
|
@@ -178,8 +182,9 @@ export function installWriteTool(
|
|
|
178
182
|
cwd: string,
|
|
179
183
|
home: string,
|
|
180
184
|
TextComponent: new (text?: string, paddingX?: number, paddingY?: number) => Component,
|
|
185
|
+
readConfig: () => DiffRenderConfig,
|
|
181
186
|
): void {
|
|
182
|
-
const renderers = createWriteRenderers(cwd, home, TextComponent);
|
|
187
|
+
const renderers = createWriteRenderers(cwd, home, TextComponent, readConfig);
|
|
183
188
|
pi.registerToolRenderer((toolName, next) =>
|
|
184
189
|
toolName === "write" ? { ...next(), ...renderers } : next(),
|
|
185
190
|
);
|