@pi-unipi/utility 0.2.7 → 0.2.9
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 +70 -144
- package/package.json +1 -1
- package/src/diff/highlighter.ts +1 -2
- package/src/diff/renderer.ts +25 -52
- package/src/diff/wrapper.ts +60 -8
package/README.md
CHANGED
|
@@ -1,94 +1,68 @@
|
|
|
1
1
|
# @pi-unipi/utility
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Environment info, diagnostics, cleanup, name badge, and diff rendering. The grab-bag package for maintaining your development environment and making tool output readable.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The diff rendering is the standout feature — Shiki-powered syntax-highlighted diffs for `write` and `edit` tool output. Side-by-side view for edits, unified view for writes, with color presets and auto-fallback on narrow terminals.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Commands
|
|
8
8
|
|
|
9
9
|
| Command | Description |
|
|
10
10
|
|---------|-------------|
|
|
11
|
-
| `/unipi:continue` | Continue agent without polluting context |
|
|
12
|
-
| `/unipi:reload` | Explain how to reload extensions |
|
|
13
|
-
| `/unipi:status` | Show status of all unipi modules |
|
|
14
|
-
| `/unipi:cleanup` | Clean stale DBs, temp files, old sessions |
|
|
15
11
|
| `/unipi:env` | Show environment info (Node, Pi, OS, paths) |
|
|
16
12
|
| `/unipi:doctor` | Run diagnostics across all modules |
|
|
17
|
-
| `/unipi:
|
|
13
|
+
| `/unipi:status` | Show status of all unipi modules |
|
|
14
|
+
| `/unipi:cleanup` | Clean stale DBs, temp files, old sessions |
|
|
15
|
+
| `/unipi:reload` | Explain how to reload extensions |
|
|
16
|
+
| `/unipi:name-badge` | Toggle name badge overlay |
|
|
18
17
|
| `/unipi:badge-gen` | Generate session name via LLM and enable badge |
|
|
19
|
-
| `/unipi:util-settings` |
|
|
20
|
-
| `/unipi:badge-settings` | Settings overlay (deprecated alias for `/unipi:util-settings`) |
|
|
18
|
+
| `/unipi:util-settings` | Unified settings for badge and diff rendering |
|
|
21
19
|
|
|
22
|
-
###
|
|
20
|
+
### Examples
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
```
|
|
23
|
+
/unipi:env # Show environment
|
|
24
|
+
/unipi:doctor # Run diagnostics
|
|
25
|
+
/unipi:cleanup # Clean stale files
|
|
26
|
+
/unipi:cleanup --dry-run # Preview what would be cleaned
|
|
27
|
+
/unipi:name-badge # Toggle the session name badge
|
|
28
|
+
/unipi:badge-gen # Generate a session name via LLM
|
|
29
|
+
```
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
## Special Triggers
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|--------|------|-------------|
|
|
35
|
-
| **ProcessLifecycle** | `lifecycle/process` | Parent PID polling, orphan detection, signal handlers, cleanup registry |
|
|
36
|
-
| **cleanupStale** | `lifecycle/cleanup` | Stale DB/temp/session/cache cleanup with dry-run support |
|
|
37
|
-
| **TTLCache** | `cache/ttl-cache` | Memory or SQLite-backed TTL cache with auto-expiration |
|
|
38
|
-
| **AnalyticsCollector** | `analytics/collector` | Privacy-respecting event collection with daily rollup |
|
|
39
|
-
| **runDiagnostics** | `diagnostics/engine` | Cross-module health checks with plugin architecture |
|
|
40
|
-
| **detectCapabilities** | `display/capabilities` | Terminal feature detection (color, Nerd Font, unicode) |
|
|
41
|
-
| **Width Utilities** | `display/width` | ANSI-aware clamp, wrap, collapse, pad, center |
|
|
42
|
-
| **SettingsInspector** | `tui/settings-inspector` | Reusable settings overlay data model |
|
|
43
|
-
|
|
44
|
-
## Installation
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
pi install npm:@pi-unipi/utility
|
|
48
|
-
```
|
|
33
|
+
Utility registers with the info-screen dashboard, showing module status and diagnostic results. The footer subscribes to utility events for its extension status segment.
|
|
49
34
|
|
|
50
|
-
|
|
35
|
+
The diff rendering feature wraps Pi's built-in `write` and `edit` tools. When enabled, these tools show syntax-highlighted diffs instead of plain output. This is a transparent replacement — the agent doesn't need to know about it.
|
|
51
36
|
|
|
52
|
-
|
|
53
|
-
pi install npm:@pi-unipi/unipi
|
|
54
|
-
```
|
|
37
|
+
## Agent Tools
|
|
55
38
|
|
|
56
|
-
|
|
39
|
+
| Tool | Description |
|
|
40
|
+
|------|-------------|
|
|
41
|
+
| `ctx_batch` | Atomic batch execution with rollback support |
|
|
42
|
+
| `ctx_env` | Environment inspection for debugging |
|
|
43
|
+
| `write` | Write file with syntax-highlighted diff (when diff enabled) |
|
|
44
|
+
| `edit` | Edit file with split/unified diff view (when diff enabled) |
|
|
57
45
|
|
|
58
|
-
###
|
|
46
|
+
### Batch Execution
|
|
59
47
|
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
/unipi:cleanup # Clean stale files
|
|
63
|
-
/unipi:cleanup --dry-run # Preview what would be cleaned
|
|
64
|
-
/unipi:env # Show environment
|
|
65
|
-
/unipi:doctor # Run diagnostics
|
|
66
|
-
```
|
|
48
|
+
```typescript
|
|
49
|
+
import { BatchBuilder } from "@pi-unipi/utility/tools/batch";
|
|
67
50
|
|
|
68
|
-
|
|
51
|
+
const report = await new BatchBuilder()
|
|
52
|
+
.addCommand("search", { query: "refactor" })
|
|
53
|
+
.addTool("memory_search", { query: "patterns" })
|
|
54
|
+
.withOptions({ failFast: true, commandTimeoutMs: 30000 })
|
|
55
|
+
.execute(myExecutor);
|
|
69
56
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
57
|
+
if (!report.success) {
|
|
58
|
+
console.log("Failed:", report.results.find(r => !r.success)?.error);
|
|
59
|
+
}
|
|
73
60
|
```
|
|
74
61
|
|
|
75
|
-
|
|
76
|
-
It auto-restores visibility on session restart.
|
|
62
|
+
## Configurables
|
|
77
63
|
|
|
78
64
|
### Diff Rendering
|
|
79
65
|
|
|
80
|
-
Shiki-powered, syntax-highlighted diffs for `write` and `edit` tool output. When enabled, the default tools are replaced with enhanced versions that show side-by-side or stacked diffs with syntax highlighting.
|
|
81
|
-
|
|
82
|
-
**Features:**
|
|
83
|
-
- Split view (side-by-side) for `edit` tool, auto-falls back to unified on narrow terminals
|
|
84
|
-
- Unified view (stacked single-column) for `write` tool overwrites
|
|
85
|
-
- 4 color presets: default, midnight, subtle, neon
|
|
86
|
-
- LRU cache (192 entries) for Shiki highlights
|
|
87
|
-
- Large diff fallback (skip highlighting above 80k chars)
|
|
88
|
-
- Environment variable color overrides (`DIFF_ADD_BG`, `DIFF_REM_BG`, etc.)
|
|
89
|
-
|
|
90
|
-
**Configuration:**
|
|
91
|
-
|
|
92
66
|
```
|
|
93
67
|
/unipi:util-settings # Open unified settings TUI
|
|
94
68
|
```
|
|
@@ -106,26 +80,38 @@ Or edit `.unipi/config/util-settings.json` directly:
|
|
|
106
80
|
}
|
|
107
81
|
```
|
|
108
82
|
|
|
109
|
-
|
|
110
|
-
|
|
83
|
+
| Setting | Default | Options |
|
|
84
|
+
|---------|---------|---------|
|
|
85
|
+
| `enabled` | true | true/false |
|
|
86
|
+
| `theme` | "default" | default, midnight, subtle, neon |
|
|
87
|
+
| `shikiTheme` | "github-dark" | github-dark, dracula, one-dark-pro, catppuccin-mocha, nord, tokyo-night |
|
|
88
|
+
| `splitMinWidth` | 150 | Minimum terminal width for split view |
|
|
111
89
|
|
|
112
|
-
|
|
90
|
+
Environment variable overrides: `DIFF_ADD_BG`, `DIFF_REM_BG`, etc.
|
|
113
91
|
|
|
114
|
-
|
|
115
|
-
|
|
92
|
+
Features:
|
|
93
|
+
- Split view (side-by-side) for `edit`, auto-falls back to unified on narrow terminals
|
|
94
|
+
- Unified view (stacked) for `write` overwrites
|
|
95
|
+
- LRU cache (192 entries) for Shiki highlights
|
|
96
|
+
- Large diff fallback (skip highlighting above 80k chars)
|
|
116
97
|
|
|
117
|
-
|
|
118
|
-
.addCommand("search", { query: "refactor" })
|
|
119
|
-
.addTool("memory_search", { query: "patterns" })
|
|
120
|
-
.withOptions({ failFast: true, commandTimeoutMs: 30000 })
|
|
121
|
-
.execute(myExecutor);
|
|
98
|
+
### Name Badge
|
|
122
99
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
100
|
+
The badge is a persistent HUD overlay in the top-right corner showing the current session name. It auto-restores visibility on session restart.
|
|
101
|
+
|
|
102
|
+
## Programmatic API
|
|
103
|
+
|
|
104
|
+
| Module | Path | Description |
|
|
105
|
+
|--------|------|-------------|
|
|
106
|
+
| ProcessLifecycle | `lifecycle/process` | Parent PID polling, orphan detection, signal handlers |
|
|
107
|
+
| cleanupStale | `lifecycle/cleanup` | Stale DB/temp/session/cache cleanup with dry-run |
|
|
108
|
+
| TTLCache | `cache/ttl-cache` | Memory or SQLite-backed TTL cache |
|
|
109
|
+
| AnalyticsCollector | `analytics/collector` | Privacy-respecting event collection with daily rollup |
|
|
110
|
+
| runDiagnostics | `diagnostics/engine` | Cross-module health checks with plugin architecture |
|
|
111
|
+
| detectCapabilities | `display/capabilities` | Terminal feature detection (color, Nerd Font, unicode) |
|
|
112
|
+
| Width Utilities | `display/width` | ANSI-aware clamp, wrap, collapse, pad, center |
|
|
127
113
|
|
|
128
|
-
### TTL Cache
|
|
114
|
+
### TTL Cache
|
|
129
115
|
|
|
130
116
|
```typescript
|
|
131
117
|
import { TTLCache } from "@pi-unipi/utility/cache/ttl-cache";
|
|
@@ -135,16 +121,7 @@ await cache.set("key", { data: "value" });
|
|
|
135
121
|
const value = await cache.get("key");
|
|
136
122
|
```
|
|
137
123
|
|
|
138
|
-
###
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
import { runDiagnostics, formatDiagnosticsReport } from "@pi-unipi/utility/diagnostics/engine";
|
|
142
|
-
|
|
143
|
-
const report = await runDiagnostics();
|
|
144
|
-
console.log(formatDiagnosticsReport(report));
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### Terminal Capabilities (Code)
|
|
124
|
+
### Terminal Capabilities
|
|
148
125
|
|
|
149
126
|
```typescript
|
|
150
127
|
import { detectCapabilities, getIcon } from "@pi-unipi/utility/display/capabilities";
|
|
@@ -154,64 +131,13 @@ console.log("Nerd Font:", caps.nerdFont);
|
|
|
154
131
|
console.log(getIcon("", "[OK]")); // Uses Nerd Font if available
|
|
155
132
|
```
|
|
156
133
|
|
|
157
|
-
## Architecture
|
|
158
|
-
|
|
159
|
-
```
|
|
160
|
-
packages/utility/src/
|
|
161
|
-
├── index.ts # Extension entry point
|
|
162
|
-
├── commands.ts # Command registration
|
|
163
|
-
├── types.ts # Shared types
|
|
164
|
-
├── info-screen.ts # Info-screen integration
|
|
165
|
-
├── lifecycle/
|
|
166
|
-
│ ├── process.ts # Process lifecycle manager
|
|
167
|
-
│ └── cleanup.ts # Stale cleanup utility
|
|
168
|
-
├── cache/
|
|
169
|
-
│ └── ttl-cache.ts # TTL cache (memory + SQLite)
|
|
170
|
-
├── analytics/
|
|
171
|
-
│ └── collector.ts # Event collection + rollup
|
|
172
|
-
├── diagnostics/
|
|
173
|
-
│ └── engine.ts # Health check engine
|
|
174
|
-
├── display/
|
|
175
|
-
│ ├── capabilities.ts # Terminal detection
|
|
176
|
-
│ └── width.ts # Width utilities
|
|
177
|
-
├── diff/
|
|
178
|
-
│ ├── settings.ts # Unified settings (badge + diff) read/write + migration
|
|
179
|
-
│ ├── theme.ts # Diff color presets, resolution chain, hex ↔ ANSI
|
|
180
|
-
│ ├── parser.ts # Diff parsing (structuredPatch, word diff analysis)
|
|
181
|
-
│ ├── highlighter.ts # Shiki singleton, LRU cache, language detection
|
|
182
|
-
│ ├── renderer.ts # Split/unified renderers, ANSI utilities
|
|
183
|
-
│ └── wrapper.ts # write/edit tool wrapping with diff output
|
|
184
|
-
├── tui/
|
|
185
|
-
│ ├── settings-inspector.ts # Settings overlay model
|
|
186
|
-
│ ├── name-badge.ts # Name badge overlay component
|
|
187
|
-
│ ├── name-badge-state.ts # Name badge state manager
|
|
188
|
-
│ ├── badge-settings.ts # Badge settings (thin wrapper over diff/settings)
|
|
189
|
-
│ └── util-settings-tui.ts # Unified settings TUI (badge + diff)
|
|
190
|
-
└── tools/
|
|
191
|
-
├── batch.ts # Batch execution
|
|
192
|
-
└── env.ts # Environment info
|
|
193
|
-
```
|
|
194
|
-
|
|
195
134
|
## Privacy
|
|
196
135
|
|
|
197
|
-
The analytics collector is
|
|
198
|
-
- No file contents
|
|
136
|
+
The analytics collector is privacy-respecting:
|
|
137
|
+
- No file contents recorded
|
|
199
138
|
- No sensitive data (API keys, tokens, passwords) — redacted automatically
|
|
200
139
|
- Strings truncated to 500 characters
|
|
201
|
-
- All data stays local
|
|
202
|
-
|
|
203
|
-
## Dependencies
|
|
204
|
-
|
|
205
|
-
- `@pi-unipi/core` — Shared constants, events, utilities
|
|
206
|
-
- `@mariozechner/pi-coding-agent` — Pi extension API
|
|
207
|
-
- `@sinclair/typebox` — Schema validation (peer dependency)
|
|
208
|
-
- `diff` — Unified diff generation (for diff rendering)
|
|
209
|
-
- `@shikijs/cli` — Shiki syntax highlighting (for diff rendering)
|
|
210
|
-
- `sqlite3` — Optional, for persistent cache/analytics
|
|
211
|
-
|
|
212
|
-
### Dev Dependencies
|
|
213
|
-
|
|
214
|
-
- `@types/diff` — TypeScript types for the diff library
|
|
140
|
+
- All data stays local
|
|
215
141
|
|
|
216
142
|
## License
|
|
217
143
|
|
package/package.json
CHANGED
package/src/diff/highlighter.ts
CHANGED
|
@@ -273,9 +273,8 @@ export async function getShikiHighlighter(): Promise<any> {
|
|
|
273
273
|
],
|
|
274
274
|
});
|
|
275
275
|
return shikiHighlighter;
|
|
276
|
-
} catch
|
|
276
|
+
} catch {
|
|
277
277
|
// If Shiki fails to load, return null — we'll use plain text
|
|
278
|
-
console.warn("[pi-diff] Shiki highlighter failed to load:", err);
|
|
279
278
|
shikiInitPromise = null;
|
|
280
279
|
return null;
|
|
281
280
|
}
|
package/src/diff/renderer.ts
CHANGED
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ANSI diff rendering: split (side-by-side) and unified (stacked) views.
|
|
5
5
|
* Includes ANSI utilities, background injection, and adaptive wrapping.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: All output lines must be truncated to terminal width
|
|
8
|
+
* to prevent TUI crashes. Use fit() or truncateToTermWidth()
|
|
9
|
+
* before pushing lines.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import type { ParsedDiff, DiffLine } from "./parser.js";
|
|
9
13
|
import type { DiffColors } from "./theme.js";
|
|
10
14
|
import { hexToBgAnsi, hexToFgAnsi } from "./theme.js";
|
|
11
15
|
import { hlBlock, detectLanguage } from "./highlighter.js";
|
|
16
|
+
import { truncateToWidth as piTruncateToWidth, visibleWidth as piVisibleWidth } from "@mariozechner/pi-tui";
|
|
12
17
|
|
|
13
18
|
// ─── Constants ──────────────────────────────────────────────────────────────────
|
|
14
19
|
|
|
@@ -36,64 +41,21 @@ export function strip(s: string): string {
|
|
|
36
41
|
|
|
37
42
|
/**
|
|
38
43
|
* Get the visible width of a string (excluding ANSI escapes).
|
|
39
|
-
*
|
|
44
|
+
* Delegates to pi-tui's ANSI-aware implementation for correctness.
|
|
40
45
|
*/
|
|
41
46
|
export function visibleWidth(s: string): number {
|
|
42
|
-
|
|
43
|
-
let width = 0;
|
|
44
|
-
for (const char of stripped) {
|
|
45
|
-
const code = char.codePointAt(0)!;
|
|
46
|
-
// CJK Unified Ideographs, CJK Compatibility, etc.
|
|
47
|
-
if (
|
|
48
|
-
(code >= 0x4e00 && code <= 0x9fff) ||
|
|
49
|
-
(code >= 0x3000 && code <= 0x30ff) ||
|
|
50
|
-
(code >= 0xff00 && code <= 0xffef) ||
|
|
51
|
-
(code >= 0xf900 && code <= 0xfaff) ||
|
|
52
|
-
(code >= 0x2e80 && code <= 0x2eff) ||
|
|
53
|
-
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
54
|
-
(code >= 0x20000 && code <= 0x2a6df)
|
|
55
|
-
) {
|
|
56
|
-
width += 2;
|
|
57
|
-
} else if (code > 0xffff) {
|
|
58
|
-
// Surrogate pairs / emoji — typically width 2
|
|
59
|
-
width += 2;
|
|
60
|
-
} else {
|
|
61
|
-
width += 1;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return width;
|
|
47
|
+
return piVisibleWidth(s);
|
|
65
48
|
}
|
|
66
49
|
|
|
67
50
|
/**
|
|
68
51
|
* Fit a string to a target width, padding or truncating as needed.
|
|
69
|
-
*
|
|
52
|
+
* Uses pi-tui's ANSI-aware truncation for correctness.
|
|
70
53
|
*/
|
|
71
54
|
export function fit(s: string, targetWidth: number): string {
|
|
72
55
|
const vw = visibleWidth(s);
|
|
73
56
|
if (vw === targetWidth) return s;
|
|
74
57
|
if (vw > targetWidth) {
|
|
75
|
-
|
|
76
|
-
let width = 0;
|
|
77
|
-
let i = 0;
|
|
78
|
-
const stripped = strip(s);
|
|
79
|
-
for (; i < stripped.length && width < targetWidth; i++) {
|
|
80
|
-
const code = stripped.codePointAt(i)!;
|
|
81
|
-
width += (code >= 0x4e00 && code <= 0x9fff) || code > 0xffff ? 2 : 1;
|
|
82
|
-
}
|
|
83
|
-
// Find the corresponding position in the original (with ANSI) string
|
|
84
|
-
let strippedIdx = 0;
|
|
85
|
-
let origIdx = 0;
|
|
86
|
-
while (origIdx < s.length && strippedIdx < i) {
|
|
87
|
-
if (s[origIdx] === "\x1b") {
|
|
88
|
-
// Skip ANSI sequence
|
|
89
|
-
while (origIdx < s.length && s[origIdx] !== "m") origIdx++;
|
|
90
|
-
origIdx++;
|
|
91
|
-
} else {
|
|
92
|
-
strippedIdx++;
|
|
93
|
-
origIdx++;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return s.substring(0, origIdx) + "\x1b[0m";
|
|
58
|
+
return piTruncateToWidth(s, targetWidth, "");
|
|
97
59
|
}
|
|
98
60
|
// Pad
|
|
99
61
|
return s + " ".repeat(targetWidth - vw);
|
|
@@ -198,6 +160,17 @@ export function termW(): number {
|
|
|
198
160
|
}
|
|
199
161
|
}
|
|
200
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Truncate a line to terminal width using pi-tui's ANSI-aware truncation.
|
|
165
|
+
* Prevents TUI crash when rendered lines exceed terminal width.
|
|
166
|
+
* Accounts for rendering overhead from Box nesting (~6 chars).
|
|
167
|
+
*/
|
|
168
|
+
export function truncateToTermWidth(line: string): string {
|
|
169
|
+
const maxW = Math.max(20, termW() - 6);
|
|
170
|
+
if (piVisibleWidth(line) <= maxW) return line;
|
|
171
|
+
return piTruncateToWidth(line, maxW, "…");
|
|
172
|
+
}
|
|
173
|
+
|
|
201
174
|
// ─── Background Injection ───────────────────────────────────────────────────────
|
|
202
175
|
|
|
203
176
|
/**
|
|
@@ -313,16 +286,16 @@ export function renderUnified(
|
|
|
313
286
|
|
|
314
287
|
switch (line.type) {
|
|
315
288
|
case "hunk":
|
|
316
|
-
lines.push(`${hunkFg}${line.content}${reset}`);
|
|
289
|
+
lines.push(truncateToTermWidth(`${hunkFg}${line.content}${reset}`));
|
|
317
290
|
break;
|
|
318
291
|
case "add":
|
|
319
|
-
lines.push(`${addBg}${addFg}+${reset}${addBg} ${lnum(null, 4)} ${lnum(line.newLine, 4)} │ ${line.content}${reset}`);
|
|
292
|
+
lines.push(truncateToTermWidth(`${addBg}${addFg}+${reset}${addBg} ${lnum(null, 4)} ${lnum(line.newLine, 4)} │ ${line.content}${reset}`));
|
|
320
293
|
break;
|
|
321
294
|
case "remove":
|
|
322
|
-
lines.push(`${remBg}${remFg}-${reset}${remBg} ${lnum(line.oldLine, 4)} ${lnum(null, 4)} │ ${line.content}${reset}`);
|
|
295
|
+
lines.push(truncateToTermWidth(`${remBg}${remFg}-${reset}${remBg} ${lnum(line.oldLine, 4)} ${lnum(null, 4)} │ ${line.content}${reset}`));
|
|
323
296
|
break;
|
|
324
297
|
case "context":
|
|
325
|
-
lines.push(` ${headerFg}${lnum(line.oldLine, 4)} ${lnum(line.newLine, 4)}${reset} │ ${line.content}`);
|
|
298
|
+
lines.push(truncateToTermWidth(` ${headerFg}${lnum(line.oldLine, 4)} ${lnum(line.newLine, 4)}${reset} │ ${line.content}`));
|
|
326
299
|
break;
|
|
327
300
|
}
|
|
328
301
|
}
|
|
@@ -411,7 +384,7 @@ export function renderSplit(
|
|
|
411
384
|
for (let i = 0; i < leftLines.length; i++) {
|
|
412
385
|
const left = leftLines[i];
|
|
413
386
|
const right = rightLines[i];
|
|
414
|
-
lines.push(`${left} │ ${right}`);
|
|
387
|
+
lines.push(truncateToTermWidth(`${left} │ ${right}`));
|
|
415
388
|
}
|
|
416
389
|
|
|
417
390
|
if (truncated) {
|
package/src/diff/wrapper.ts
CHANGED
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import type { ExtensionAPI, ToolDefinition, AgentToolResult } from "@mariozechner/pi-coding-agent";
|
|
15
|
+
import { visibleWidth as piVisibleWidth, truncateToWidth as piTruncateToWidth } from "@mariozechner/pi-tui";
|
|
15
16
|
import { readDiffSettings } from "./settings.js";
|
|
16
17
|
import { parseDiff } from "./parser.js";
|
|
17
18
|
import { resolveDiffColors, applyDiffPalette } from "./theme.js";
|
|
18
|
-
import { renderSplit, renderUnified, termW, SPLIT_MIN_WIDTH } from "./renderer.js";
|
|
19
|
+
import { renderSplit, renderUnified, termW, SPLIT_MIN_WIDTH, truncateToTermWidth } from "./renderer.js";
|
|
19
20
|
import { detectLanguageFromPath, hlBlock, MAX_HL_CHARS } from "./highlighter.js";
|
|
20
21
|
|
|
21
22
|
// ─── Types ──────────────────────────────────────────────────────────────────────
|
|
@@ -157,7 +158,15 @@ export function registerEnhancedWriteTool(pi: ExtensionAPI, cwd: string): void {
|
|
|
157
158
|
renderResult(result: any, _options: any, theme: any): any {
|
|
158
159
|
const details = result?.details as DiffToolDetails | undefined;
|
|
159
160
|
if (!details || !details.diff || !details.diff.lines || details.diff.lines.length === 0) {
|
|
160
|
-
|
|
161
|
+
// Error or empty-diff case: render the message from result.content so the
|
|
162
|
+
// user sees "Could not find text to replace..." etc. Never return null here
|
|
163
|
+
// because Container.render() will crash on null child.
|
|
164
|
+
const msg = result?.content?.[0]?.text ?? "";
|
|
165
|
+
return {
|
|
166
|
+
setText: () => {},
|
|
167
|
+
text: msg,
|
|
168
|
+
render: (width: number) => (width > 0 ? [msg.slice(0, width)] : [msg]),
|
|
169
|
+
} as any;
|
|
161
170
|
}
|
|
162
171
|
|
|
163
172
|
try {
|
|
@@ -169,14 +178,34 @@ export function registerEnhancedWriteTool(pi: ExtensionAPI, cwd: string): void {
|
|
|
169
178
|
? renderSplit(details.diff, details.language, max, dc)
|
|
170
179
|
: renderUnified(details.diff, details.language, max, dc);
|
|
171
180
|
|
|
172
|
-
//
|
|
181
|
+
// Split into lines and cache for width-aware rendering.
|
|
182
|
+
// Each line is already truncated to terminal width by
|
|
183
|
+
// truncateToTermWidth() in the renderer, but we also
|
|
184
|
+
// respect the width parameter from Box.render().
|
|
185
|
+
const cachedLines = rendered.split("\n");
|
|
186
|
+
|
|
173
187
|
return {
|
|
174
188
|
setText: () => {},
|
|
175
189
|
text: rendered,
|
|
176
|
-
render: () =>
|
|
190
|
+
render: (width: number) => {
|
|
191
|
+
// If width is provided, re-truncate lines that
|
|
192
|
+
// still exceed it (e.g., inside nested Boxes)
|
|
193
|
+
const maxW = width > 0 ? width : tw;
|
|
194
|
+
return cachedLines.map((line: string) => {
|
|
195
|
+
if (piVisibleWidth(line) > maxW) {
|
|
196
|
+
return piTruncateToWidth(line, maxW, "…");
|
|
197
|
+
}
|
|
198
|
+
return line;
|
|
199
|
+
});
|
|
200
|
+
},
|
|
177
201
|
} as any;
|
|
178
202
|
} catch {
|
|
179
|
-
|
|
203
|
+
const fallback = "(diff rendering failed)";
|
|
204
|
+
return {
|
|
205
|
+
setText: () => {},
|
|
206
|
+
text: fallback,
|
|
207
|
+
render: (width: number) => (width > 0 ? [fallback.slice(0, width)] : [fallback]),
|
|
208
|
+
} as any;
|
|
180
209
|
}
|
|
181
210
|
},
|
|
182
211
|
});
|
|
@@ -262,7 +291,15 @@ export function registerEnhancedEditTool(pi: ExtensionAPI, cwd: string): void {
|
|
|
262
291
|
renderResult(result: any, _options: any, theme: any): any {
|
|
263
292
|
const details = result?.details as DiffToolDetails | undefined;
|
|
264
293
|
if (!details || !details.diff || !details.diff.lines || details.diff.lines.length === 0) {
|
|
265
|
-
|
|
294
|
+
// Error or empty-diff case: render the message from result.content so the
|
|
295
|
+
// user sees "Could not find text to replace..." etc. Never return null here
|
|
296
|
+
// because Container.render() will crash on null child.
|
|
297
|
+
const msg = result?.content?.[0]?.text ?? "";
|
|
298
|
+
return {
|
|
299
|
+
setText: () => {},
|
|
300
|
+
text: msg,
|
|
301
|
+
render: (width: number) => (width > 0 ? [msg.slice(0, width)] : [msg]),
|
|
302
|
+
} as any;
|
|
266
303
|
}
|
|
267
304
|
|
|
268
305
|
try {
|
|
@@ -274,13 +311,28 @@ export function registerEnhancedEditTool(pi: ExtensionAPI, cwd: string): void {
|
|
|
274
311
|
? renderSplit(details.diff, details.language, max, dc)
|
|
275
312
|
: renderUnified(details.diff, details.language, max, dc);
|
|
276
313
|
|
|
314
|
+
const cachedLines = rendered.split("\n");
|
|
315
|
+
|
|
277
316
|
return {
|
|
278
317
|
setText: () => {},
|
|
279
318
|
text: rendered,
|
|
280
|
-
render: () =>
|
|
319
|
+
render: (width: number) => {
|
|
320
|
+
const maxW = width > 0 ? width : tw;
|
|
321
|
+
return cachedLines.map((line: string) => {
|
|
322
|
+
if (piVisibleWidth(line) > maxW) {
|
|
323
|
+
return piTruncateToWidth(line, maxW, "…");
|
|
324
|
+
}
|
|
325
|
+
return line;
|
|
326
|
+
});
|
|
327
|
+
},
|
|
281
328
|
} as any;
|
|
282
329
|
} catch {
|
|
283
|
-
|
|
330
|
+
const fallback = "(diff rendering failed)";
|
|
331
|
+
return {
|
|
332
|
+
setText: () => {},
|
|
333
|
+
text: fallback,
|
|
334
|
+
render: (width: number) => (width > 0 ? [fallback.slice(0, width)] : [fallback]),
|
|
335
|
+
} as any;
|
|
284
336
|
}
|
|
285
337
|
},
|
|
286
338
|
});
|