@tmustier/pi-files-widget 0.1.20 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -1
- package/README.md +5 -1
- package/TODO.md +1 -0
- package/browser.ts +137 -56
- package/demo.svg +1 -1
- package/file-viewer.ts +1 -1
- package/git.ts +33 -6
- package/index.ts +43 -5
- package/input-utils.ts +1 -1
- package/package.json +7 -3
- package/utils.ts +2 -1
- package/viewer.ts +12 -5
package/file-viewer.ts
CHANGED
package/git.ts
CHANGED
|
@@ -11,6 +11,27 @@ export function isGitRepo(cwd: string): boolean {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Path of `cwd` relative to the repository top-level (e.g. "app/"), or "" when
|
|
16
|
+
* `cwd` is the top-level itself. `git status --porcelain` reports paths relative
|
|
17
|
+
* to the repository root, while `git ls-files` (and this widget's node keys) are
|
|
18
|
+
* relative to `cwd` — this prefix lets us translate between the two.
|
|
19
|
+
*/
|
|
20
|
+
function getGitPathPrefix(cwd: string): string {
|
|
21
|
+
try {
|
|
22
|
+
return execSync("git rev-parse --show-prefix", { cwd, encoding: "utf-8", timeout: 2000, stdio: "pipe" }).trim();
|
|
23
|
+
} catch {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Convert a repo-root-relative path to a cwd-relative one; null if outside cwd. */
|
|
29
|
+
function stripPathPrefix(filePath: string, prefix: string): string | null {
|
|
30
|
+
if (!prefix) return filePath;
|
|
31
|
+
if (filePath.startsWith(prefix)) return filePath.slice(prefix.length);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
export function getGitStatus(cwd: string, options: { includeIgnored?: boolean } = {}): Map<string, string> {
|
|
15
36
|
const status = new Map<string, string>();
|
|
16
37
|
try {
|
|
@@ -18,11 +39,13 @@ export function getGitStatus(cwd: string, options: { includeIgnored?: boolean }
|
|
|
18
39
|
if (options.includeIgnored !== false) {
|
|
19
40
|
flags.push("--ignored");
|
|
20
41
|
}
|
|
42
|
+
const prefix = getGitPathPrefix(cwd);
|
|
21
43
|
const output = execSync(`git status ${flags.join(" ")}`, { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
22
44
|
for (const line of output.split("\n")) {
|
|
23
45
|
if (line.length < 3) continue;
|
|
24
46
|
const statusCode = line.slice(0, 2).trim() || "?";
|
|
25
|
-
const filePath = line.slice(3);
|
|
47
|
+
const filePath = stripPathPrefix(line.slice(3), prefix);
|
|
48
|
+
if (filePath === null || !filePath) continue;
|
|
26
49
|
status.set(filePath, statusCode);
|
|
27
50
|
}
|
|
28
51
|
} catch {}
|
|
@@ -39,6 +62,7 @@ export function getGitFileList(cwd: string): string[] {
|
|
|
39
62
|
} catch {}
|
|
40
63
|
|
|
41
64
|
try {
|
|
65
|
+
const prefix = getGitPathPrefix(cwd);
|
|
42
66
|
const statusOutput = execSync("git status --porcelain -uall -z", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
43
67
|
const entries = statusOutput.split("\0");
|
|
44
68
|
for (let i = 0; i < entries.length; i++) {
|
|
@@ -52,8 +76,9 @@ export function getGitFileList(cwd: string): string[] {
|
|
|
52
76
|
} else if (filePath.includes(" -> ")) {
|
|
53
77
|
filePath = filePath.split(" -> ").pop() || filePath;
|
|
54
78
|
}
|
|
55
|
-
|
|
56
|
-
|
|
79
|
+
const relPath = filePath ? stripPathPrefix(filePath, prefix) : null;
|
|
80
|
+
if (relPath) {
|
|
81
|
+
files.add(relPath);
|
|
57
82
|
}
|
|
58
83
|
}
|
|
59
84
|
} catch {}
|
|
@@ -72,8 +97,10 @@ export function getGitBranch(cwd: string): string {
|
|
|
72
97
|
export function getGitDiffStats(cwd: string): Map<string, DiffStats> {
|
|
73
98
|
const stats = new Map<string, DiffStats>();
|
|
74
99
|
try {
|
|
75
|
-
// Get diff stats for modified files
|
|
76
|
-
|
|
100
|
+
// Get diff stats for modified files. --relative keeps paths relative to cwd
|
|
101
|
+
// (and scoped to it) so they match the widget's cwd-relative node keys even
|
|
102
|
+
// when cwd is a subdirectory of the repository.
|
|
103
|
+
const output = execSync("git diff --relative --numstat HEAD", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
77
104
|
for (const line of output.split("\n")) {
|
|
78
105
|
const parts = line.split("\t");
|
|
79
106
|
if (parts.length >= 3) {
|
|
@@ -84,7 +111,7 @@ export function getGitDiffStats(cwd: string): Map<string, DiffStats> {
|
|
|
84
111
|
}
|
|
85
112
|
}
|
|
86
113
|
// Also get stats for staged files
|
|
87
|
-
const stagedOutput = execSync("git diff --numstat --cached", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
114
|
+
const stagedOutput = execSync("git diff --relative --numstat --cached", { cwd, encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
88
115
|
for (const line of stagedOutput.split("\n")) {
|
|
89
116
|
const parts = line.split("\t");
|
|
90
117
|
if (parts.length >= 3) {
|
package/index.ts
CHANGED
|
@@ -5,14 +5,37 @@
|
|
|
5
5
|
* Use /readfiles to open the file browser, navigate with j/k, Enter to view.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { ExtensionAPI } from "@
|
|
9
|
-
import {
|
|
8
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { statSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
10
12
|
|
|
11
13
|
import { createFileBrowser } from "./browser";
|
|
12
14
|
import { POLL_INTERVAL_MS } from "./constants";
|
|
13
15
|
import { formatCommentMessage } from "./comment";
|
|
14
16
|
import { hasCommand } from "./utils";
|
|
15
17
|
|
|
18
|
+
function resolveInitialPath(arg: string | undefined, cwd: string): { path: string; error?: string } {
|
|
19
|
+
if (!arg) return { path: cwd };
|
|
20
|
+
let candidate = arg.trim();
|
|
21
|
+
if (!candidate) return { path: cwd };
|
|
22
|
+
const home = homedir();
|
|
23
|
+
if (candidate === "~") {
|
|
24
|
+
candidate = home;
|
|
25
|
+
} else if (candidate.startsWith("~/")) {
|
|
26
|
+
candidate = join(home, candidate.slice(2));
|
|
27
|
+
}
|
|
28
|
+
const absolute = isAbsolute(candidate) ? candidate : resolve(cwd, candidate);
|
|
29
|
+
try {
|
|
30
|
+
if (!statSync(absolute).isDirectory()) {
|
|
31
|
+
return { path: cwd, error: `${absolute} is not a directory` };
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
return { path: cwd, error: `${absolute} is not accessible` };
|
|
35
|
+
}
|
|
36
|
+
return { path: absolute };
|
|
37
|
+
}
|
|
38
|
+
|
|
16
39
|
export default function editorExtension(pi: ExtensionAPI): void {
|
|
17
40
|
const cwd = process.cwd();
|
|
18
41
|
const agentModifiedFiles = new Set<string>();
|
|
@@ -20,14 +43,21 @@ export default function editorExtension(pi: ExtensionAPI): void {
|
|
|
20
43
|
const getMissingDeps = () => requiredDeps.filter((dep) => !hasCommand(dep));
|
|
21
44
|
|
|
22
45
|
pi.registerCommand("readfiles", {
|
|
23
|
-
description: "Open file browser",
|
|
24
|
-
handler: async (
|
|
46
|
+
description: "Open file browser (optional: /readfiles <path> to start outside the current directory)",
|
|
47
|
+
handler: async (args, ctx) => {
|
|
25
48
|
const missing = getMissingDeps();
|
|
26
49
|
if (missing.length > 0) {
|
|
27
50
|
ctx.ui.notify(`files-widget requires ${missing.join(", ")}. Install: brew install bat git-delta glow`, "error");
|
|
28
51
|
return;
|
|
29
52
|
}
|
|
30
53
|
|
|
54
|
+
const resolved = resolveInitialPath(args, cwd);
|
|
55
|
+
if (resolved.error) {
|
|
56
|
+
ctx.ui.notify(resolved.error, "error");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const initialPath = resolved.path;
|
|
60
|
+
|
|
31
61
|
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
32
62
|
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
|
33
63
|
|
|
@@ -51,7 +81,15 @@ export default function editorExtension(pi: ExtensionAPI): void {
|
|
|
51
81
|
};
|
|
52
82
|
|
|
53
83
|
const requestRender = () => tui.requestRender();
|
|
54
|
-
const browser = createFileBrowser(
|
|
84
|
+
const browser = createFileBrowser(
|
|
85
|
+
initialPath,
|
|
86
|
+
agentModifiedFiles,
|
|
87
|
+
theme,
|
|
88
|
+
cleanup,
|
|
89
|
+
requestComment,
|
|
90
|
+
requestRender,
|
|
91
|
+
cwd
|
|
92
|
+
);
|
|
55
93
|
|
|
56
94
|
pollInterval = setInterval(() => {
|
|
57
95
|
requestRender();
|
package/input-utils.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmustier/pi-files-widget",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "In-terminal file browser and viewer for Pi.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Thomas Mustier",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"bugs": "https://github.com/tmustier/pi-extensions/issues",
|
|
16
16
|
"homepage": "https://github.com/tmustier/pi-extensions/tree/main/files-widget",
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@
|
|
19
|
-
"@
|
|
18
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
19
|
+
"@earendil-works/pi-tui": "*"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"postinstall": "node -e \"console.log('pi-files-widget: required deps: bat, delta, glow. Install with: brew install bat git-delta glow')\""
|
|
@@ -26,5 +26,9 @@
|
|
|
26
26
|
"index.ts"
|
|
27
27
|
],
|
|
28
28
|
"video": "https://raw.githubusercontent.com/tmustier/pi-extensions/main/files-widget/demo.mp4"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@earendil-works/pi-coding-agent": "^0.80.3",
|
|
32
|
+
"@earendil-works/pi-tui": "^0.80.3"
|
|
29
33
|
}
|
|
30
34
|
}
|
package/utils.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { execSync } from "node:child_process";
|
|
|
2
2
|
|
|
3
3
|
export function hasCommand(cmd: string): boolean {
|
|
4
4
|
try {
|
|
5
|
-
|
|
5
|
+
const checkCmd = process.platform === "win32" ? "where" : "which";
|
|
6
|
+
execSync(`${checkCmd} ${cmd}`, { stdio: "ignore" });
|
|
6
7
|
return true;
|
|
7
8
|
} catch {
|
|
8
9
|
return false;
|
package/viewer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Theme } from "@
|
|
2
|
-
import { Key, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Key, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3
3
|
import { readFileSync, statSync } from "node:fs";
|
|
4
4
|
import { relative } from "node:path";
|
|
5
5
|
|
|
@@ -59,11 +59,17 @@ export interface ViewerController {
|
|
|
59
59
|
handleInput(data: string): ViewerAction;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export interface ViewerConfig {
|
|
63
|
+
getRoot: () => string;
|
|
64
|
+
projectCwd: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
export function createViewer(
|
|
63
|
-
|
|
68
|
+
config: ViewerConfig,
|
|
64
69
|
theme: Theme,
|
|
65
70
|
requestComment: (payload: CommentPayload, comment: string) => void
|
|
66
71
|
): ViewerController {
|
|
72
|
+
const { getRoot, projectCwd } = config;
|
|
67
73
|
const searchInput = createTextInputBuffer();
|
|
68
74
|
const commentInput = createTextInputBuffer({ preserveNewlines: true });
|
|
69
75
|
|
|
@@ -177,7 +183,7 @@ export function createViewer(
|
|
|
177
183
|
if (!state.file) return;
|
|
178
184
|
refreshRawContent();
|
|
179
185
|
const hasChanges = !!state.file.gitStatus;
|
|
180
|
-
const result = loadFileContent(state.file.path,
|
|
186
|
+
const result = loadFileContent(state.file.path, getRoot(), state.diffMode, hasChanges, width, state.renderMarkdown);
|
|
181
187
|
state.content = result.lines;
|
|
182
188
|
state.renderMarkdown = result.renderedMarkdown;
|
|
183
189
|
state.lastRenderWidth = width;
|
|
@@ -242,7 +248,8 @@ export function createViewer(
|
|
|
242
248
|
|
|
243
249
|
const rawLines = state.rawContent.split("\n");
|
|
244
250
|
const selectedText = rawLines.slice(state.selectStart, state.selectEnd + 1).join("\n");
|
|
245
|
-
const
|
|
251
|
+
const rel = relative(projectCwd, state.file.path);
|
|
252
|
+
const relPath = !rel || rel.startsWith("..") ? state.file.path : rel;
|
|
246
253
|
const lineRange = state.selectStart === state.selectEnd
|
|
247
254
|
? `line ${state.selectStart + 1}`
|
|
248
255
|
: `lines ${state.selectStart + 1}-${state.selectEnd + 1}`;
|