@huseynsnmz/pi-board 0.14.0 → 0.15.1

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,26 @@
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
23
  stop();
34
- ctx.ui.setWidget(WIDGET, undefined);
24
+ focus.stop();
35
25
  };
36
26
  }
@@ -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,26 @@
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: () => ctx.ui.setWidget(widget, undefined),
25
+ };
26
+ }
@@ -0,0 +1,25 @@
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
+ const now = focus.known() && !ctx.isIdle() && focus.promptText() === undefined;
15
+ if (now === waiting) return;
16
+ waiting = now;
17
+ // If the run has ended too, its own end has already said so, and this would undo that.
18
+ if (now || !ctx.isIdle()) changed(now);
19
+ }, CHECK_MS);
20
+ timer.unref?.();
21
+ return () => {
22
+ clearInterval(timer);
23
+ focus.stop();
24
+ };
25
+ }