pi-extensions 0.1.10 → 0.1.12
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/.ralph/todo-app-plan.state.json +14 -0
- package/README.md +1 -1
- package/arcade/CHANGELOG.md +3 -0
- package/arcade/README.md +2 -0
- package/arcade/assets/demo.mp4 +0 -0
- package/arcade/package.json +1 -1
- package/code-actions/CHANGELOG.md +3 -0
- package/code-actions/package.json +1 -1
- package/code-actions/src/index.ts +1 -0
- package/control/control.ts +1397 -0
- package/files-widget/CHANGELOG.md +8 -0
- package/files-widget/README.md +1 -1
- package/files-widget/file-tree.ts +41 -2
- package/files-widget/file-viewer.ts +178 -11
- package/files-widget/package.json +5 -1
- package/package.json +1 -1
- package/review/review.ts +807 -0
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this extension will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.1.10] - 2026-01-26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Treat git-reported directory entries as directories to avoid viewer errors
|
|
9
|
+
- Guard the viewer against opening directories directly
|
|
10
|
+
- Wrap delta diff output without breaking gutters and avoid truncation
|
|
11
|
+
- Add a safe fallback when `bat` fails to render with wrapping
|
|
12
|
+
|
|
5
13
|
## [0.1.9] - 2026-01-26
|
|
6
14
|
|
|
7
15
|
### Changed
|
package/files-widget/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# files-widget
|
|
2
2
|
|
|
3
|
-
In-terminal file browser and viewer for Pi. Navigate files, view diffs, select code, and send comments to the agent without leaving the terminal.
|
|
3
|
+
In-terminal file browser and diff viewer widget for Pi. Navigate files, view diffs, select code, and send comments to the agent without leaving the terminal and without interrupting your agent.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
1
2
|
import { join } from "node:path";
|
|
2
3
|
|
|
3
4
|
import { MAX_TREE_DEPTH } from "./constants";
|
|
@@ -5,6 +6,14 @@ import type { DiffStats, FileNode, FlatNode } from "./types";
|
|
|
5
6
|
|
|
6
7
|
const collator = new Intl.Collator(undefined, { sensitivity: "base" });
|
|
7
8
|
|
|
9
|
+
function isDirectoryPath(path: string): boolean {
|
|
10
|
+
try {
|
|
11
|
+
return statSync(path).isDirectory();
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
function compareNodes(a: FileNode, b: FileNode): number {
|
|
9
18
|
if (a.isDirectory !== b.isDirectory) {
|
|
10
19
|
return a.isDirectory ? -1 : 1;
|
|
@@ -153,11 +162,41 @@ export function buildFileTreeFromPaths(
|
|
|
153
162
|
|
|
154
163
|
const fileRelPath = parts.join("/");
|
|
155
164
|
if (seenFiles.has(fileRelPath)) continue;
|
|
156
|
-
seenFiles.add(fileRelPath);
|
|
157
165
|
|
|
158
166
|
const filePath = join(cwd, fileRelPath);
|
|
159
|
-
const fileGitStatus = gitStatus.get(fileRelPath);
|
|
167
|
+
const fileGitStatus = gitStatus.get(fileRelPath) ?? gitStatus.get(`${fileRelPath}/`);
|
|
160
168
|
const fileDiffStats = diffStats.get(fileRelPath);
|
|
169
|
+
const existingDir = directoryMap.get(fileRelPath);
|
|
170
|
+
|
|
171
|
+
if (existingDir) {
|
|
172
|
+
if (fileGitStatus) {
|
|
173
|
+
existingDir.gitStatus = fileGitStatus;
|
|
174
|
+
}
|
|
175
|
+
if (fileDiffStats) {
|
|
176
|
+
existingDir.diffStats = fileDiffStats;
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const isDirEntry = normalized.endsWith("/") || isDirectoryPath(filePath);
|
|
182
|
+
if (isDirEntry) {
|
|
183
|
+
const depth = parts.length;
|
|
184
|
+
const dirNode: FileNode = {
|
|
185
|
+
name: fileName,
|
|
186
|
+
path: filePath,
|
|
187
|
+
isDirectory: true,
|
|
188
|
+
children: [],
|
|
189
|
+
expanded: depth < 1,
|
|
190
|
+
hasChangedChildren: false,
|
|
191
|
+
gitStatus: fileGitStatus,
|
|
192
|
+
diffStats: fileDiffStats,
|
|
193
|
+
};
|
|
194
|
+
directoryMap.set(fileRelPath, dirNode);
|
|
195
|
+
current.children?.push(dirNode);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
seenFiles.add(fileRelPath);
|
|
161
200
|
|
|
162
201
|
current.children?.push({
|
|
163
202
|
name: fileName,
|
|
@@ -1,9 +1,157 @@
|
|
|
1
|
+
import { visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
1
2
|
import { execSync } from "node:child_process";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
+
import { readFileSync, statSync } from "node:fs";
|
|
3
4
|
|
|
4
5
|
import { isGitRepo } from "./git";
|
|
5
6
|
import { hasCommand, stripLeadingEmptyLines } from "./utils";
|
|
6
7
|
|
|
8
|
+
const DIFF_CONTENT_PREFIXES = new Set(["+", "-", " "]);
|
|
9
|
+
|
|
10
|
+
function isDiffContentLine(line: string): boolean {
|
|
11
|
+
if (!line) return false;
|
|
12
|
+
if (!DIFF_CONTENT_PREFIXES.has(line[0])) return false;
|
|
13
|
+
if (line.startsWith("+++ ") || line.startsWith("--- ")) return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function wrapLine(line: string, width: number): string[] {
|
|
18
|
+
if (width <= 0 || line.length <= width) {
|
|
19
|
+
return [line];
|
|
20
|
+
}
|
|
21
|
+
const wrapped: string[] = [];
|
|
22
|
+
for (let i = 0; i < line.length; i += width) {
|
|
23
|
+
wrapped.push(line.slice(i, i + width));
|
|
24
|
+
}
|
|
25
|
+
return wrapped;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function wrapDiffLines(lines: string[], width: number): string[] {
|
|
29
|
+
if (width <= 0) return lines;
|
|
30
|
+
const wrapped: string[] = [];
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
if (line.length <= width) {
|
|
33
|
+
wrapped.push(line);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (isDiffContentLine(line)) {
|
|
37
|
+
const prefix = line[0];
|
|
38
|
+
const content = line.slice(1);
|
|
39
|
+
const contentWidth = Math.max(width - 1, 1);
|
|
40
|
+
for (const chunk of wrapLine(content, contentWidth)) {
|
|
41
|
+
wrapped.push(prefix + chunk);
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
wrapped.push(...wrapLine(line, width));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return wrapped;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function extractAnsiCode(str: string, pos: number): { length: number } | null {
|
|
51
|
+
if (pos >= str.length || str[pos] !== "\x1b") return null;
|
|
52
|
+
const next = str[pos + 1];
|
|
53
|
+
if (next === "[") {
|
|
54
|
+
let j = pos + 2;
|
|
55
|
+
while (j < str.length && !/[mGKHJ]/.test(str[j])) j++;
|
|
56
|
+
if (j < str.length) return { length: j + 1 - pos };
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (next === "]") {
|
|
60
|
+
let j = pos + 2;
|
|
61
|
+
while (j < str.length) {
|
|
62
|
+
if (str[j] === "\x07") return { length: j + 1 - pos };
|
|
63
|
+
if (str[j] === "\x1b" && str[j + 1] === "\\") return { length: j + 2 - pos };
|
|
64
|
+
j++;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (next === "_") {
|
|
69
|
+
let j = pos + 2;
|
|
70
|
+
while (j < str.length) {
|
|
71
|
+
if (str[j] === "\x07") return { length: j + 1 - pos };
|
|
72
|
+
if (str[j] === "\x1b" && str[j + 1] === "\\") return { length: j + 2 - pos };
|
|
73
|
+
j++;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function stripAnsiCodes(line: string): string {
|
|
81
|
+
let result = "";
|
|
82
|
+
for (let i = 0; i < line.length;) {
|
|
83
|
+
const ansi = extractAnsiCode(line, i);
|
|
84
|
+
if (ansi) {
|
|
85
|
+
i += ansi.length;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
result += line[i];
|
|
89
|
+
i += 1;
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function splitByVisibleWidth(line: string, width: number): { prefix: string; rest: string } {
|
|
95
|
+
if (width <= 0) return { prefix: "", rest: line };
|
|
96
|
+
let visible = 0;
|
|
97
|
+
let i = 0;
|
|
98
|
+
while (i < line.length) {
|
|
99
|
+
const ansi = extractAnsiCode(line, i);
|
|
100
|
+
if (ansi) {
|
|
101
|
+
i += ansi.length;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const charWidth = visibleWidth(line[i]);
|
|
105
|
+
if (visible + charWidth > width) break;
|
|
106
|
+
visible += charWidth;
|
|
107
|
+
i += 1;
|
|
108
|
+
}
|
|
109
|
+
return { prefix: line.slice(0, i), rest: line.slice(i) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function maskDigits(line: string): string {
|
|
113
|
+
let result = "";
|
|
114
|
+
for (let i = 0; i < line.length;) {
|
|
115
|
+
const ansi = extractAnsiCode(line, i);
|
|
116
|
+
if (ansi) {
|
|
117
|
+
result += line.slice(i, i + ansi.length);
|
|
118
|
+
i += ansi.length;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const char = line[i];
|
|
122
|
+
result += char >= "0" && char <= "9" ? " " : char;
|
|
123
|
+
i += 1;
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function wrapDeltaLine(line: string, width: number): string[] {
|
|
129
|
+
const clean = stripAnsiCodes(line);
|
|
130
|
+
let separatorIndex = clean.indexOf("│");
|
|
131
|
+
if (separatorIndex === -1) separatorIndex = clean.indexOf("|");
|
|
132
|
+
if (separatorIndex === -1) return wrapTextWithAnsi(line, width);
|
|
133
|
+
|
|
134
|
+
let prefixWidth = visibleWidth(clean.slice(0, separatorIndex + 1));
|
|
135
|
+
if (clean[separatorIndex + 1] === " ") prefixWidth += 1;
|
|
136
|
+
if (prefixWidth >= width) return wrapTextWithAnsi(line, width);
|
|
137
|
+
|
|
138
|
+
const { prefix, rest } = splitByVisibleWidth(line, prefixWidth);
|
|
139
|
+
const contentWidth = Math.max(width - visibleWidth(prefix), 1);
|
|
140
|
+
const continuationPrefix = maskDigits(prefix);
|
|
141
|
+
const wrappedContent = wrapTextWithAnsi(rest, contentWidth);
|
|
142
|
+
|
|
143
|
+
return wrappedContent.map((chunk, index) => (index === 0 ? prefix : continuationPrefix) + chunk);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function wrapDeltaLines(lines: string[], width: number): string[] {
|
|
147
|
+
if (width <= 0) return lines;
|
|
148
|
+
const wrapped: string[] = [];
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
wrapped.push(...wrapDeltaLine(line, width));
|
|
151
|
+
}
|
|
152
|
+
return wrapped;
|
|
153
|
+
}
|
|
154
|
+
|
|
7
155
|
export function loadFileContent(
|
|
8
156
|
filePath: string,
|
|
9
157
|
cwd: string,
|
|
@@ -15,23 +163,31 @@ export function loadFileContent(
|
|
|
15
163
|
const termWidth = width || process.stdout.columns || 80;
|
|
16
164
|
|
|
17
165
|
try {
|
|
166
|
+
try {
|
|
167
|
+
if (statSync(filePath).isDirectory()) {
|
|
168
|
+
return ["Directory selected - expand it in the file tree instead of opening it."];
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
// Ignore stat errors and fall through to normal handling
|
|
172
|
+
}
|
|
173
|
+
|
|
18
174
|
if (diffMode && hasChanges && isGitRepo(cwd)) {
|
|
19
175
|
try {
|
|
20
176
|
// Try different diff strategies
|
|
21
177
|
let diffOutput = "";
|
|
22
178
|
|
|
23
179
|
// First try: unstaged changes
|
|
24
|
-
const unstaged = execSync(`git diff -- "${filePath}"`, { cwd, encoding: "utf-8", timeout: 10000, stdio: "pipe" });
|
|
180
|
+
const unstaged = execSync(`git diff --no-color -- "${filePath}"`, { cwd, encoding: "utf-8", timeout: 10000, stdio: "pipe" });
|
|
25
181
|
if (unstaged.trim()) {
|
|
26
182
|
diffOutput = unstaged;
|
|
27
183
|
} else {
|
|
28
184
|
// Second try: staged changes
|
|
29
|
-
const staged = execSync(`git diff --cached -- "${filePath}"`, { cwd, encoding: "utf-8", timeout: 10000, stdio: "pipe" });
|
|
185
|
+
const staged = execSync(`git diff --no-color --cached -- "${filePath}"`, { cwd, encoding: "utf-8", timeout: 10000, stdio: "pipe" });
|
|
30
186
|
if (staged.trim()) {
|
|
31
187
|
diffOutput = staged;
|
|
32
188
|
} else {
|
|
33
189
|
// Third try: diff against HEAD (for new files that are staged)
|
|
34
|
-
const headDiff = execSync(`git diff HEAD -- "${filePath}"`, { cwd, encoding: "utf-8", timeout: 10000, stdio: "pipe" });
|
|
190
|
+
const headDiff = execSync(`git diff --no-color HEAD -- "${filePath}"`, { cwd, encoding: "utf-8", timeout: 10000, stdio: "pipe" });
|
|
35
191
|
if (headDiff.trim()) {
|
|
36
192
|
diffOutput = headDiff;
|
|
37
193
|
}
|
|
@@ -46,7 +202,7 @@ export function loadFileContent(
|
|
|
46
202
|
// Pipe through delta with line numbers for better readability
|
|
47
203
|
try {
|
|
48
204
|
const deltaOutput = execSync(
|
|
49
|
-
`delta --no-gitconfig --width=${termWidth} --line-numbers`,
|
|
205
|
+
`delta --no-gitconfig --width=${termWidth} --line-numbers --wrap-max-lines=unlimited --max-line-length=0`,
|
|
50
206
|
{
|
|
51
207
|
cwd,
|
|
52
208
|
encoding: "utf-8",
|
|
@@ -55,13 +211,13 @@ export function loadFileContent(
|
|
|
55
211
|
stdio: ["pipe", "pipe", "pipe"],
|
|
56
212
|
}
|
|
57
213
|
);
|
|
58
|
-
return stripLeadingEmptyLines(deltaOutput.split("\n"));
|
|
214
|
+
return wrapDeltaLines(stripLeadingEmptyLines(deltaOutput.split("\n")), termWidth);
|
|
59
215
|
} catch {
|
|
60
216
|
// Fall back to raw diff
|
|
61
217
|
}
|
|
62
218
|
}
|
|
63
219
|
|
|
64
|
-
return diffOutput.split("\n");
|
|
220
|
+
return wrapDiffLines(stripLeadingEmptyLines(diffOutput.split("\n")), termWidth);
|
|
65
221
|
} catch (e: any) {
|
|
66
222
|
return [`Diff error: ${e.message}`];
|
|
67
223
|
}
|
|
@@ -79,10 +235,21 @@ export function loadFileContent(
|
|
|
79
235
|
}
|
|
80
236
|
|
|
81
237
|
if (hasCommand("bat")) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
238
|
+
try {
|
|
239
|
+
return execSync(
|
|
240
|
+
`bat --style=numbers --color=always --paging=never --wrap=auto --terminal-width=${termWidth} "${filePath}"`,
|
|
241
|
+
{ encoding: "utf-8", timeout: 10000 }
|
|
242
|
+
).split("\n");
|
|
243
|
+
} catch {
|
|
244
|
+
try {
|
|
245
|
+
return execSync(
|
|
246
|
+
`bat --style=numbers --color=always --paging=never --terminal-width=${termWidth} "${filePath}"`,
|
|
247
|
+
{ encoding: "utf-8", timeout: 10000 }
|
|
248
|
+
).split("\n");
|
|
249
|
+
} catch {
|
|
250
|
+
// Fall through to raw file read
|
|
251
|
+
}
|
|
252
|
+
}
|
|
86
253
|
}
|
|
87
254
|
|
|
88
255
|
const raw = readFileSync(filePath, "utf-8");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmustier/pi-files-widget",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "In-terminal file browser and viewer for Pi.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Thomas Mustier",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
},
|
|
15
15
|
"bugs": "https://github.com/tmustier/pi-extensions/issues",
|
|
16
16
|
"homepage": "https://github.com/tmustier/pi-extensions/tree/main/files-widget",
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@mariozechner/pi-coding-agent": "^0.50.0",
|
|
19
|
+
"@mariozechner/pi-tui": "^0.50.0"
|
|
20
|
+
},
|
|
17
21
|
"pi": {
|
|
18
22
|
"extensions": [
|
|
19
23
|
"index.ts"
|