@huseynsnmz/pi-board 0.14.0 → 0.15.3

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.
@@ -1,36 +1,30 @@
1
- /** Left arrow twice on an empty prompt opens the board. */
1
+ /** Left arrow twice on an empty prompt puts this session on the board and opens it. */
2
2
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { isKeyRelease, Key, matchesKey } from "@earendil-works/pi-tui";
4
4
  import { createDoublePress } from "../core/double-press.mjs";
5
-
6
- const WIDGET = "agent-board-double-left";
7
-
8
- type Editorish = { getExpandedText?: () => string };
5
+ import { watchFocus } from "./focus.ts";
9
6
 
10
7
  export function watchDoubleLeft(ctx: ExtensionContext): () => void {
11
- // Only a widget is handed the TUI, and only the TUI knows what has focus: without this check,
12
- // the Enter below could answer a dialog that's on screen.
13
- let focused: () => unknown = () => null;
14
- ctx.ui.setWidget(WIDGET, (tui) => {
15
- // Newer than some versions of Pi this may load in. Without it, double left does nothing.
16
- focused = () => (tui as unknown as { getFocusedComponent?: () => unknown }).getFocusedComponent?.();
17
- return { render: () => [], invalidate() {} };
18
- });
8
+ const focus = watchFocus(ctx, "agent-board-double-left");
19
9
  const presses = createDoublePress();
20
10
  const stop = ctx.ui.onTerminalInput((data) => {
21
11
  if (isKeyRelease(data)) return undefined;
22
- const editor = focused() as Editorish | null | undefined;
23
- if (!matchesKey(data, Key.left) || editor?.getExpandedText?.() !== "") {
12
+ // Checked first: the Enter below could otherwise answer a dialog that's on screen.
13
+ if (!matchesKey(data, Key.left) || focus.promptText() !== "") {
24
14
  presses.reset();
25
15
  return undefined;
26
16
  }
27
17
  if (!presses.press()) return undefined;
28
18
  // A command runs with more than a key handler is given, so this goes the way a typed one does.
29
- ctx.ui.setEditorText("/agent-board");
19
+ ctx.ui.setEditorText("/bg");
30
20
  return { data: "\r" };
31
21
  });
32
22
  return () => {
33
- stop();
34
- ctx.ui.setWidget(WIDGET, undefined);
23
+ try {
24
+ stop();
25
+ } catch {
26
+ /* see focus.stop */
27
+ }
28
+ focus.stop();
35
29
  };
36
30
  }
@@ -0,0 +1,25 @@
1
+ /** Ctrl+G: write the task in $VISUAL or $EDITOR. */
2
+ import { spawn } from "node:child_process";
3
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ /** The edited text, or undefined if no editor is set or it didn't finish cleanly. */
8
+ export async function editExternally(text: string, env = process.env): Promise<string | undefined> {
9
+ const command = env.VISUAL || env.EDITOR;
10
+ if (!command) return undefined;
11
+ const dir = mkdtempSync(join(tmpdir(), "pi-board-editor-"));
12
+ const file = join(dir, "task.md");
13
+ try {
14
+ writeFileSync(file, text);
15
+ const [program, ...args] = command.split(" ");
16
+ const code = await new Promise<number | null>((resolve) => {
17
+ const child = spawn(program!, [...args, file], { stdio: "inherit" });
18
+ child.on("error", () => resolve(null));
19
+ child.on("close", (exit) => resolve(exit));
20
+ });
21
+ return code === 0 ? readFileSync(file, "utf8").replace(/\n$/, "") : undefined;
22
+ } finally {
23
+ rmSync(dir, { recursive: true, force: true });
24
+ }
25
+ }
@@ -0,0 +1,32 @@
1
+ /** What has the keyboard, which only the TUI knows, and only a widget is handed the TUI. */
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+
4
+ type Editorish = { getExpandedText?: () => string };
5
+
6
+ export interface Focus {
7
+ /** The prompt's text if it has the keyboard, or undefined if something else does. */
8
+ promptText(): string | undefined;
9
+ /** Whether anything is known about what has the keyboard, on this version of Pi. */
10
+ known(): boolean;
11
+ stop(): void;
12
+ }
13
+
14
+ export function watchFocus(ctx: ExtensionContext, widget: string): Focus {
15
+ let focused: (() => unknown) | null = null;
16
+ ctx.ui.setWidget(widget, (tui) => {
17
+ const reader = tui as unknown as { getFocusedComponent?: () => unknown };
18
+ if (reader.getFocusedComponent) focused = () => reader.getFocusedComponent?.();
19
+ return { render: () => [], invalidate() {} };
20
+ });
21
+ return {
22
+ promptText: () => (focused?.() as Editorish | null | undefined)?.getExpandedText?.(),
23
+ known: () => focused !== null,
24
+ stop: () => {
25
+ try {
26
+ ctx.ui.setWidget(widget, undefined);
27
+ } catch {
28
+ /* the session it belonged to has already gone, and its widget with it */
29
+ }
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Tells the board when a session is waiting on you. A permission prompt or any other dialog takes
3
+ * the keyboard from the prompt while a run is under way, whichever extension opened it.
4
+ */
5
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
+ import { watchFocus } from "./focus.ts";
7
+
8
+ const CHECK_MS = 500;
9
+
10
+ export function watchWaiting(ctx: ExtensionContext, changed: (waiting: boolean) => void): () => void {
11
+ const focus = watchFocus(ctx, "agent-board-waiting");
12
+ let waiting = false;
13
+ const timer = setInterval(() => {
14
+ try {
15
+ const now = focus.known() && !ctx.isIdle() && focus.promptText() === undefined;
16
+ if (now === waiting) return;
17
+ waiting = now;
18
+ // If the run has ended too, its own end has already said so, and this would undo that.
19
+ if (now || !ctx.isIdle()) changed(now);
20
+ } catch {
21
+ // The session was replaced before this could be stopped. An error here would end Pi.
22
+ clearInterval(timer);
23
+ }
24
+ }, CHECK_MS);
25
+ timer.unref?.();
26
+ return () => {
27
+ clearInterval(timer);
28
+ focus.stop();
29
+ };
30
+ }
@@ -1,59 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * postinstall: patch vulnerable transitive dependencies that are locked behind
4
- * @earendil-works/pi-coding-agent's npm-shrinkwrap.json (which prevents npm
5
- * "overrides" from taking effect).
6
- *
7
- * Vulnerabilities patched:
8
- * - brace-expansion <=5.0.7 → 5.0.8 (GHSA-3jxr-9vmj-r5cp, GHSA-mh99-v99m-4gvg)
9
- * - protobufjs 7.5.0–7.6.4 → 7.6.5 (GHSA-j3f2-48v5-ccww)
10
- */
11
- import { execSync } from "node:child_process";
12
- import { existsSync, rmSync, mkdirSync } from "node:fs";
13
- import { join, dirname } from "node:path";
14
- import { fileURLToPath } from "node:url";
15
-
16
- const root = join(dirname(fileURLToPath(import.meta.url)), "..");
17
- const nested = join(
18
- root,
19
- "node_modules",
20
- "@earendil-works",
21
- "pi-coding-agent",
22
- "node_modules",
23
- );
24
-
25
- const patches = [
26
- { name: "brace-expansion", version: "5.0.8" },
27
- { name: "protobufjs", version: "7.6.5" },
28
- ];
29
-
30
- for (const { name, version } of patches) {
31
- const target = join(nested, name);
32
- if (!existsSync(target)) {
33
- console.log(`[patch-vulns] ${name} not found nested, skipping`);
34
- continue;
35
- }
36
-
37
- console.log(`[patch-vulns] patching ${name} → ${version}`);
38
- rmSync(target, { recursive: true, force: true });
39
- mkdirSync(target, { recursive: true });
40
-
41
- // Install the patched version into a temp dir, then move it into place
42
- const tmp = join(root, `.patch-tmp-${name}`);
43
- rmSync(tmp, { recursive: true, force: true });
44
- mkdirSync(tmp, { recursive: true });
45
-
46
- try {
47
- execSync(`npm install ${name}@${version} --prefix "${tmp}" --ignore-scripts`, {
48
- stdio: "pipe",
49
- cwd: root,
50
- });
51
- const installed = join(tmp, "node_modules", name);
52
- rmSync(target, { recursive: true, force: true });
53
- execSync(`mv "${installed}" "${target}"`);
54
- } finally {
55
- rmSync(tmp, { recursive: true, force: true });
56
- }
57
- }
58
-
59
- console.log("[patch-vulns] done");