@pi-archimedes/diff 2.7.3 → 2.9.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 CHANGED
@@ -43,13 +43,14 @@ 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, and 60 code columns per side — and it does **not** read settings, because standalone has no config reader. In the [suite](https://github.com/danielcherubini/pi-archimedes), its `archimedes.diff` namespace supplies the same three values (`diffTheme`, `diffSplitMinWidth`, `diffSplitMinCodeWidth`) through the suite's config reader:
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/danielcherubini/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
  |---------|------|---------|-------------|
50
50
  | `diffTheme` | string | `github-dark` | Shiki syntax-highlighting theme |
51
51
  | `diffSplitMinWidth` | number | `150` | Minimum terminal columns for split view (≥ 100) |
52
52
  | `diffSplitMinCodeWidth` | number | `60` | Minimum code columns per side in split view (≥ 30) |
53
+ | `diffSplitWrapCheck` | boolean | `true` | Fall back to unified view when lines wrap (>20% or ≥8 lines) |
53
54
 
54
55
  The palette is derived from the chosen Shiki theme; it does **not** automatically pick up Pi's active theme colours. If you want a different look, pick a closer theme rather than expecting a live match.
55
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-archimedes/diff",
3
- "version": "2.7.3",
3
+ "version": "2.9.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/danielcherubini/pi-archimedes.git"
@@ -28,8 +28,8 @@
28
28
  "@earendil-works/pi-tui": ">=0.1.0"
29
29
  },
30
30
  "devDependencies": {
31
- "@earendil-works/pi-coding-agent": "^0.85.1",
32
- "@earendil-works/pi-tui": "^0.85.1",
31
+ "@earendil-works/pi-coding-agent": "^0.87.0",
32
+ "@earendil-works/pi-tui": "^0.87.0",
33
33
  "@types/diff": "^7.0.2",
34
34
  "typescript": "^6.0.0"
35
35
  },
package/src/index.ts CHANGED
@@ -29,12 +29,14 @@ export interface DiffConfig {
29
29
  diffTheme: string;
30
30
  diffSplitMinWidth: number;
31
31
  diffSplitMinCodeWidth: number;
32
+ diffSplitWrapCheck?: boolean;
32
33
  }
33
34
 
34
35
  const DEFAULT_DIFF_CONFIG: DiffConfig = {
35
36
  diffTheme: "github-dark",
36
37
  diffSplitMinWidth: 150,
37
38
  diffSplitMinCodeWidth: 60,
39
+ diffSplitWrapCheck: true,
38
40
  };
39
41
 
40
42
  let _readConfig: () => DiffConfig = () => DEFAULT_DIFF_CONFIG;
@@ -44,8 +46,8 @@ function getConfig(): DiffConfig { return _readConfig(); }
44
46
  // Settings
45
47
  // ---------------------------------------------------------------------------
46
48
 
47
- export function getDiffSettingsItems(): SettingItem[] {
48
- const config = getConfig();
49
+ export function getDiffSettingsItems(overrideConfig?: DiffConfig): SettingItem[] {
50
+ const config = overrideConfig ?? getConfig();
49
51
  return [
50
52
  {
51
53
  id: "diffTheme",
@@ -65,6 +67,13 @@ export function getDiffSettingsItems(): SettingItem[] {
65
67
  description: "Minimum code column width for split diff view",
66
68
  currentValue: String(config.diffSplitMinCodeWidth),
67
69
  },
70
+ {
71
+ id: "diffSplitWrapCheck",
72
+ label: "Split Wrap Check",
73
+ description: "Fall back to unified diff if lines wrap",
74
+ currentValue: (config.diffSplitWrapCheck ?? true) ? "On" : "Off",
75
+ values: ["On", "Off"],
76
+ },
68
77
  ];
69
78
  }
70
79
 
@@ -0,0 +1,110 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { parseDiff } from "../core/diff.js";
3
+ import {
4
+ shouldUseSplit,
5
+ setConfigGetter,
6
+ getConfig,
7
+ adaptiveWrapRows,
8
+ wrapAnsi,
9
+ } from "./shared.js";
10
+
11
+ describe("adaptiveWrapRows", () => {
12
+ it("returns correct row limits at width boundaries", () => {
13
+ expect(adaptiveWrapRows(44)).toBe(2);
14
+ expect(adaptiveWrapRows(45)).toBe(4);
15
+ expect(adaptiveWrapRows(79)).toBe(4);
16
+ expect(adaptiveWrapRows(80)).toBe(5);
17
+ });
18
+ });
19
+
20
+ describe("wrapAnsi", () => {
21
+ it("wraps lines without exceeding max rows", () => {
22
+ const str = "a".repeat(100);
23
+ // w=44 -> adaptiveWrapRows=2
24
+ const rows = wrapAnsi(str, 44);
25
+ expect(rows).toHaveLength(2);
26
+ });
27
+
28
+ it("wraps up to the adaptive row limits and emits › when truncated on maxRows", () => {
29
+ // w=80 -> maxRows=5; input requires > 5 rows
30
+ const longStr = "x".repeat(80 * 6);
31
+ const rows = wrapAnsi(longStr, 80);
32
+ expect(rows).toHaveLength(5);
33
+ expect(rows[4]).toContain("›");
34
+
35
+ // w=44 -> maxRows=2; input requires > 2 rows
36
+ const narrowLongStr = "y".repeat(44 * 3);
37
+ const narrowRows = wrapAnsi(narrowLongStr, 44);
38
+ expect(narrowRows).toHaveLength(2);
39
+ expect(narrowRows[1]).toContain("›");
40
+ });
41
+
42
+ it("does not emit › when content fits within maxRows", () => {
43
+ const str = "z".repeat(80 * 3);
44
+ const rows = wrapAnsi(str, 80);
45
+ expect(rows).toHaveLength(3);
46
+ expect(rows[2]).not.toContain("›");
47
+ });
48
+ });
49
+
50
+ describe("render shared / shouldUseSplit", () => {
51
+ beforeEach(() => {
52
+ setConfigGetter(() => ({
53
+ diffSplitMinWidth: 150,
54
+ diffSplitMinCodeWidth: 60,
55
+ diffSplitWrapCheck: true,
56
+ }));
57
+ });
58
+
59
+ it("returns false for empty diff", () => {
60
+ const diff = parseDiff("", "");
61
+ expect(shouldUseSplit(diff, 200)).toBe(false);
62
+ });
63
+
64
+ it("returns false if terminal width < diffSplitMinWidth", () => {
65
+ const old = "const x = 1;\n";
66
+ const updated = "const x = 2;\n";
67
+ const diff = parseDiff(old, updated);
68
+ expect(shouldUseSplit(diff, 140)).toBe(false);
69
+ });
70
+
71
+ it("returns false if available code column width < diffSplitMinCodeWidth", () => {
72
+ setConfigGetter(() => ({
73
+ diffSplitMinWidth: 100,
74
+ diffSplitMinCodeWidth: 60,
75
+ diffSplitWrapCheck: true,
76
+ }));
77
+ const old = "const x = 1;\n";
78
+ const updated = "const x = 2;\n";
79
+ const diff = parseDiff(old, updated);
80
+ // With tw=110, half=54, nw=2, gw=7, cw=47 < 60 -> false
81
+ expect(shouldUseSplit(diff, 110)).toBe(false);
82
+ });
83
+
84
+ it("returns true for normal short lines when terminal width and code width fit", () => {
85
+ const old = "const x = 1;\n";
86
+ const updated = "const x = 2;\n";
87
+ const diff = parseDiff(old, updated);
88
+ expect(shouldUseSplit(diff, 200)).toBe(true);
89
+ });
90
+
91
+ it("falls back to false when lines wrap and diffSplitWrapCheck is true", () => {
92
+ const longLineOld = "const longLine = '" + "a".repeat(150) + "';\n";
93
+ const longLineNew = "const longLine = '" + "b".repeat(150) + "';\n";
94
+ const diff = parseDiff(longLineOld, longLineNew);
95
+ // 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);
97
+ });
98
+
99
+ it("returns true when lines wrap but diffSplitWrapCheck is false", () => {
100
+ setConfigGetter(() => ({
101
+ diffSplitMinWidth: 150,
102
+ diffSplitMinCodeWidth: 60,
103
+ diffSplitWrapCheck: false,
104
+ }));
105
+ const longLineOld = "const longLine = '" + "a".repeat(150) + "';\n";
106
+ const longLineNew = "const longLine = '" + "b".repeat(150) + "';\n";
107
+ const diff = parseDiff(longLineOld, longLineNew);
108
+ expect(shouldUseSplit(diff, 200)).toBe(true);
109
+ });
110
+ });
@@ -14,22 +14,32 @@ export const MAX_HL_CHARS = 80_000;
14
14
  export const WORD_DIFF_MIN_SIM = 0.15;
15
15
  const SPLIT_MAX_WRAP_RATIO = 0.2;
16
16
  const SPLIT_MAX_WRAP_LINES = 8;
17
- const MAX_WRAP_ROWS_WIDE = 3;
18
- const MAX_WRAP_ROWS_MED = 2;
19
- const MAX_WRAP_ROWS_NARROW = 1;
17
+ const MAX_WRAP_ROWS_WIDE = 5;
18
+ const MAX_WRAP_ROWS_MED = 4;
19
+ const MAX_WRAP_ROWS_NARROW = 2;
20
20
  export const DEFAULT_TERM_WIDTH = 200;
21
21
 
22
22
  // ---------------------------------------------------------------------------
23
23
  // Config
24
24
  // ---------------------------------------------------------------------------
25
25
 
26
- export function getConfig(): { diffSplitMinWidth: number; diffSplitMinCodeWidth: number } {
26
+ export interface DiffRenderConfig {
27
+ diffSplitMinWidth: number;
28
+ diffSplitMinCodeWidth: number;
29
+ diffSplitWrapCheck?: boolean;
30
+ }
31
+
32
+ export function getConfig(): DiffRenderConfig {
27
33
  return typeof _getConfig === "function" ? _getConfig() : DEFAULT_CONFIG;
28
34
  }
29
35
 
30
- const DEFAULT_CONFIG = { diffSplitMinWidth: 150, diffSplitMinCodeWidth: 60 };
31
- let _getConfig: (() => { diffSplitMinWidth: number; diffSplitMinCodeWidth: number }) | undefined;
32
- export function setConfigGetter(fn: () => { diffSplitMinWidth: number; diffSplitMinCodeWidth: number }): void {
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 {
33
43
  _getConfig = fn;
34
44
  }
35
45
 
@@ -38,8 +48,8 @@ export function setConfigGetter(fn: () => { diffSplitMinWidth: number; diffSplit
38
48
  // ---------------------------------------------------------------------------
39
49
 
40
50
  export function adaptiveWrapRows(w: number): number {
41
- if (w >= 180) return MAX_WRAP_ROWS_WIDE;
42
- if (w >= 120) return MAX_WRAP_ROWS_MED;
51
+ if (w >= 80) return MAX_WRAP_ROWS_WIDE;
52
+ if (w >= 45) return MAX_WRAP_ROWS_MED;
43
53
  return MAX_WRAP_ROWS_NARROW;
44
54
  }
45
55
 
@@ -112,6 +122,8 @@ export function shouldUseSplit(diff: ParsedDiff, tw: number, maxRows = MAX_PREVI
112
122
  const cw = Math.max(1, half - gw);
113
123
  if (cw < cfg.diffSplitMinCodeWidth) return false;
114
124
 
125
+ if (cfg.diffSplitWrapCheck === false) return true;
126
+
115
127
  const vis = diff.lines.slice(0, maxRows);
116
128
  let contentLines = 0, wrapCandidates = 0;
117
129
  for (const l of vis) {