@tmustier/pi-files-widget 0.2.0 → 0.2.2

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 CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to this extension will be documented in this file.
4
4
 
5
+ ## [0.2.2] - 2026-10-28
6
+
7
+ ### Fixed
8
+ - Suggest `brew install` only on macOS when `bat`, `delta`, or `glow` is missing, and list only the missing packages. Other systems are pointed to the install instructions in the README, which now cover Debian and Ubuntu, where `bat` installs as `batcat`. Thanks to @curious2improve for the report ([#115](https://github.com/tmustier/pi-extensions/issues/115)).
9
+
10
+ ## [0.2.1] - 2026-09-22
11
+
12
+ ### Changed
13
+ - Reduce Pi startup work by checking required commands directly on `PATH` instead of spawning `which`/`where`, and load the file-browser implementation only when `/readfiles` is invoked.
14
+ - Update session lifecycle, key handling, notifications, and file viewer results for Pi 0.87.
15
+
5
16
  ## [0.2.0] - 2026-07-04
6
17
 
7
18
  ### Added
package/README.md CHANGED
@@ -22,10 +22,14 @@ Required deps (needed for /readfiles):
22
22
  # macOS (Homebrew)
23
23
  brew install bat git-delta glow
24
24
 
25
- # Ubuntu/Debian
25
+ # Debian 13+ / Ubuntu 25.04+
26
26
  sudo apt-get install -y bat git-delta glow
27
+ # Debian and Ubuntu install bat as batcat; files-widget looks for bat
28
+ mkdir -p ~/.local/bin && ln -s /usr/bin/batcat ~/.local/bin/bat
27
29
  ```
28
30
 
31
+ On other systems, install [bat](https://github.com/sharkdp/bat#installation), [delta](https://dandavison.github.io/delta/installation.html), and [glow](https://github.com/charmbracelet/glow#installation) with your package manager. The `bat`, `delta`, and `glow` commands must be on your `PATH`.
32
+
29
33
  ```bash
30
34
  pi install git:github.com/tmustier/pi-extensions
31
35
  ```
package/file-viewer.ts CHANGED
@@ -171,7 +171,10 @@ export function loadFileContent(
171
171
  try {
172
172
  try {
173
173
  if (statSync(filePath).isDirectory()) {
174
- return ["Directory selected - expand it in the file tree instead of opening it."];
174
+ return {
175
+ lines: ["Directory selected - expand it in the file tree instead of opening it."],
176
+ renderedMarkdown: false,
177
+ };
175
178
  }
176
179
  } catch {
177
180
  // Ignore stat errors and fall through to normal handling
package/index.ts CHANGED
@@ -10,10 +10,9 @@ import { statSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
11
  import { isAbsolute, join, resolve } from "node:path";
12
12
 
13
- import { createFileBrowser } from "./browser";
14
13
  import { POLL_INTERVAL_MS } from "./constants";
15
14
  import { formatCommentMessage } from "./comment";
16
- import { hasCommand } from "./utils";
15
+ import { hasCommand, missingCommandsMessage, REQUIRED_COMMANDS } from "./utils";
17
16
 
18
17
  function resolveInitialPath(arg: string | undefined, cwd: string): { path: string; error?: string } {
19
18
  if (!arg) return { path: cwd };
@@ -39,15 +38,14 @@ function resolveInitialPath(arg: string | undefined, cwd: string): { path: strin
39
38
  export default function editorExtension(pi: ExtensionAPI): void {
40
39
  const cwd = process.cwd();
41
40
  const agentModifiedFiles = new Set<string>();
42
- const requiredDeps = ["bat", "delta", "glow"] as const;
43
- const getMissingDeps = () => requiredDeps.filter((dep) => !hasCommand(dep));
41
+ const getMissingDeps = () => REQUIRED_COMMANDS.filter((dep) => !hasCommand(dep));
44
42
 
45
43
  pi.registerCommand("readfiles", {
46
44
  description: "Open file browser (optional: /readfiles <path> to start outside the current directory)",
47
45
  handler: async (args, ctx) => {
48
46
  const missing = getMissingDeps();
49
47
  if (missing.length > 0) {
50
- ctx.ui.notify(`files-widget requires ${missing.join(", ")}. Install: brew install bat git-delta glow`, "error");
48
+ ctx.ui.notify(missingCommandsMessage(missing), "error");
51
49
  return;
52
50
  }
53
51
 
@@ -57,6 +55,7 @@ export default function editorExtension(pi: ExtensionAPI): void {
57
55
  return;
58
56
  }
59
57
  const initialPath = resolved.path;
58
+ const { createFileBrowser } = await import("./browser");
60
59
 
61
60
  await ctx.ui.custom<void>((tui, theme, _kb, done) => {
62
61
  let pollInterval: ReturnType<typeof setInterval> | null = null;
@@ -73,7 +72,7 @@ export default function editorExtension(pi: ExtensionAPI): void {
73
72
  const message = formatCommentMessage(payload, comment);
74
73
  if (ctx.isIdle()) {
75
74
  pi.sendUserMessage(message);
76
- ctx.ui.notify(`Comment sent to agent for ${payload.relPath} (${payload.lineRange})`, "success");
75
+ ctx.ui.notify(`Comment sent to agent for ${payload.relPath} (${payload.lineRange})`, "info");
77
76
  } else {
78
77
  pi.sendUserMessage(message, { deliverAs: "followUp" });
79
78
  ctx.ui.notify(`Comment queued for agent for ${payload.relPath} (${payload.lineRange})`, "info");
@@ -119,13 +118,10 @@ export default function editorExtension(pi: ExtensionAPI): void {
119
118
  pi.on("session_start", async (_event, ctx) => {
120
119
  const missing = getMissingDeps();
121
120
  if (missing.length > 0) {
122
- ctx.ui.notify(`files-widget requires ${missing.join(", ")}. Install: brew install bat git-delta glow`, "error");
121
+ ctx.ui.notify(missingCommandsMessage(missing), "error");
123
122
  }
124
123
 
125
124
  agentModifiedFiles.clear();
126
125
  });
127
126
 
128
- pi.on("session_switch", async () => {
129
- agentModifiedFiles.clear();
130
- });
131
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmustier/pi-files-widget",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "In-terminal file browser and viewer for Pi.",
5
5
  "license": "MIT",
6
6
  "author": "Thomas Mustier",
@@ -19,7 +19,7 @@
19
19
  "@earendil-works/pi-tui": "*"
20
20
  },
21
21
  "scripts": {
22
- "postinstall": "node -e \"console.log('pi-files-widget: required deps: bat, delta, glow. Install with: brew install bat git-delta glow')\""
22
+ "postinstall": "node -e \"console.log('pi-files-widget: required deps: bat, delta, glow. ' + (process.platform === 'darwin' ? 'Install with: brew install bat git-delta glow' : 'Install instructions: https://github.com/tmustier/pi-extensions/tree/main/files-widget#install'))\""
23
23
  },
24
24
  "pi": {
25
25
  "extensions": [
@@ -28,7 +28,7 @@
28
28
  "video": "https://raw.githubusercontent.com/tmustier/pi-extensions/main/files-widget/demo.mp4"
29
29
  },
30
30
  "devDependencies": {
31
- "@earendil-works/pi-coding-agent": "^0.80.3",
32
- "@earendil-works/pi-tui": "^0.80.3"
31
+ "@earendil-works/pi-coding-agent": "0.87.0",
32
+ "@earendil-works/pi-tui": "0.87.0"
33
33
  }
34
34
  }
package/utils.ts CHANGED
@@ -1,13 +1,54 @@
1
- import { execSync } from "node:child_process";
2
-
3
- export function hasCommand(cmd: string): boolean {
4
- try {
5
- const checkCmd = process.platform === "win32" ? "where" : "which";
6
- execSync(`${checkCmd} ${cmd}`, { stdio: "ignore" });
7
- return true;
8
- } catch {
9
- return false;
1
+ import { accessSync, constants, statSync } from "node:fs";
2
+ import { extname, join } from "node:path";
3
+
4
+ export interface CommandLookupOptions {
5
+ platform?: NodeJS.Platform;
6
+ path?: string;
7
+ pathExt?: string;
8
+ cwd?: string;
9
+ }
10
+
11
+ export function hasCommand(cmd: string, options: CommandLookupOptions = {}): boolean {
12
+ const platform = options.platform ?? process.platform;
13
+ const pathValue = options.path ?? process.env.PATH ?? "";
14
+ const pathEntries = pathValue.split(platform === "win32" ? ";" : ":");
15
+ const searchDirectories = platform === "win32"
16
+ ? [options.cwd ?? process.cwd(), ...pathEntries]
17
+ : pathEntries;
18
+ const extensions = platform === "win32" && !extname(cmd)
19
+ ? (options.pathExt ?? process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)
20
+ : [""];
21
+ const accessMode = platform === "win32" ? constants.F_OK : constants.X_OK;
22
+
23
+ for (const rawDirectory of searchDirectories) {
24
+ const directory = platform === "win32" && rawDirectory.startsWith('"') && rawDirectory.endsWith('"')
25
+ ? rawDirectory.slice(1, -1)
26
+ : rawDirectory;
27
+ for (const extension of extensions) {
28
+ try {
29
+ const candidate = join(directory || ".", `${cmd}${extension}`);
30
+ accessSync(candidate, accessMode);
31
+ if (statSync(candidate).isFile()) return true;
32
+ } catch {
33
+ // Continue searching PATH.
34
+ }
35
+ }
10
36
  }
37
+
38
+ return false;
39
+ }
40
+
41
+ export const REQUIRED_COMMANDS = ["bat", "delta", "glow"] as const;
42
+ export type RequiredCommand = (typeof REQUIRED_COMMANDS)[number];
43
+
44
+ const HOMEBREW_PACKAGES: Record<RequiredCommand, string> = { bat: "bat", delta: "git-delta", glow: "glow" };
45
+ const INSTALL_DOCS_URL = "https://github.com/tmustier/pi-extensions/tree/main/files-widget#install";
46
+
47
+ export function missingCommandsMessage(missing: readonly RequiredCommand[], platform: NodeJS.Platform = process.platform): string {
48
+ const install = platform === "darwin"
49
+ ? `Install: brew install ${missing.map((command) => HOMEBREW_PACKAGES[command]).join(" ")}`
50
+ : `Install with your package manager: ${INSTALL_DOCS_URL}`;
51
+ return `files-widget requires ${missing.join(", ")}. ${install}`;
11
52
  }
12
53
 
13
54
  export function isUntrackedStatus(status?: string): boolean {
package/viewer.ts CHANGED
@@ -501,7 +501,7 @@ export function createViewer(
501
501
  jumpToNextMatch(1);
502
502
  return { type: "none" };
503
503
  }
504
- if (matchesKey(data, "N") && state.mode !== "select" && state.searchMatches.length > 0) {
504
+ if (matchesKey(data, "shift+n") && state.mode !== "select" && state.searchMatches.length > 0) {
505
505
  jumpToNextMatch(-1);
506
506
  return { type: "none" };
507
507
  }
@@ -553,7 +553,7 @@ export function createViewer(
553
553
  state.scroll = 0;
554
554
  return { type: "none" };
555
555
  }
556
- if (matchesKey(data, "m") && state.mode !== "select" && state.mode !== "comment") {
556
+ if (matchesKey(data, "m") && state.mode !== "select") {
557
557
  toggleMarkdownMode();
558
558
  return { type: "none" };
559
559
  }