shariq-pi-extensions 0.2.7 → 0.2.8

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/README.md CHANGED
@@ -31,6 +31,7 @@ The package contains:
31
31
  - Firecrawl search and scraping
32
32
  - Git status UI
33
33
  - persistent task goals
34
+ - configurable steer, interrupt, or follow-up input behavior
34
35
  - dedicated multi-agent orchestration
35
36
  - Pi Memory
36
37
  - per-response TPS, TTFT, elapsed-time, and output status
@@ -80,6 +80,10 @@ Adds repository state to Pi's interface. `/lg` opens the local Git view and `/pr
80
80
 
81
81
  `/copy-all` copies the current conversation in a readable form while omitting tool protocol noise that does not belong in the transcript.
82
82
 
83
+ ### [Input mode](../extensions/input-mode/README.md)
84
+
85
+ `/input-mode` chooses what ordinary Enter does while Pi is running: steer before the next model step, interrupt the active run, or wait as a follow-up. The global selection lives in `<agent-dir>/input-mode.json`; explicit Alt+Enter follow-ups and non-interactive inputs retain native behavior. Pi does not expose extension rows in core `/settings`, so this is a dedicated extension settings picker rather than a private TUI patch.
86
+
83
87
  ### [Shell shortcuts](../extensions/shell-shortcuts/README.md)
84
88
 
85
89
  Adds `/exit` as an alias for Pi's normal quit command. Keep this extension limited to small, low-risk conveniences.
@@ -0,0 +1,13 @@
1
+ # Input mode
2
+
3
+ Controls what ordinary **Enter** does when the interactive Pi agent is already running:
4
+
5
+ - `steer` (default) queues the message before the agent's next model step.
6
+ - `interrupt` signals Pi's active abort controller immediately, then preserves the submitted text, images, prompt-template expansion, and normal delivery as the next input.
7
+ - `follow-up` queues the message until the active run finishes.
8
+
9
+ Use `/input-mode` for the picker or `/input-mode steer|interrupt|follow-up` for direct selection. The global choice is stored with restrictive permissions in `<agent-dir>/input-mode.json`; non-default modes appear in Pi's status area.
10
+
11
+ Pi does not expose an extension API for adding rows to its built-in `/settings` selector, so this extension owns a dedicated settings command rather than patching private TUI internals. The existing core **Steering mode** and **Follow-up mode** settings remain batching controls (`all` versus `one-at-a-time`).
12
+
13
+ Explicit Alt+Enter follow-ups, extension-originated messages, commands, idle input, compaction input, and RPC input retain Pi's native behavior. Interrupt cancellation is cooperative: Pi stops model streaming and abort-aware tools, but it cannot undo an external side effect that already completed or force a third-party operation that ignores its abort signal to stop.
@@ -0,0 +1,46 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
+
5
+ export const INPUT_MODES = ["steer", "interrupt", "follow-up"] as const;
6
+ export type InputMode = (typeof INPUT_MODES)[number];
7
+
8
+ interface InputModeDocument {
9
+ version: 1;
10
+ mode: InputMode;
11
+ }
12
+
13
+ export function inputModePath(): string {
14
+ return path.join(getAgentDir(), "input-mode.json");
15
+ }
16
+
17
+ export function isInputMode(value: unknown): value is InputMode {
18
+ return typeof value === "string" && INPUT_MODES.includes(value as InputMode);
19
+ }
20
+
21
+ export function loadInputMode(file = inputModePath()): InputMode {
22
+ try {
23
+ const document = JSON.parse(fs.readFileSync(file, "utf8")) as Partial<InputModeDocument>;
24
+ return document.version === 1 && isInputMode(document.mode) ? document.mode : "steer";
25
+ } catch {
26
+ return "steer";
27
+ }
28
+ }
29
+
30
+ export function saveInputMode(mode: InputMode, file = inputModePath()): void {
31
+ const directory = path.dirname(file);
32
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
33
+ const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
34
+ const document: InputModeDocument = { version: 1, mode };
35
+ try {
36
+ fs.writeFileSync(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600 });
37
+ fs.renameSync(temporary, file);
38
+ fs.chmodSync(file, 0o600);
39
+ } finally {
40
+ try {
41
+ fs.rmSync(temporary, { force: true });
42
+ } catch {
43
+ // Best-effort cleanup after a failed atomic replacement.
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,104 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext,
4
+ ExtensionUIContext,
5
+ InputEvent,
6
+ InputEventResult,
7
+ } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ INPUT_MODES,
10
+ isInputMode,
11
+ loadInputMode,
12
+ saveInputMode,
13
+ type InputMode,
14
+ } from "./config.ts";
15
+
16
+ const STATUS_KEY = "input-mode";
17
+
18
+ interface InputModeExtensionOptions {
19
+ configFile?: string;
20
+ }
21
+
22
+ function modelInput(event: InputEvent) {
23
+ if (!event.images?.length) return event.text;
24
+ return [{ type: "text" as const, text: event.text }, ...event.images];
25
+ }
26
+
27
+ export function createInputModeExtension(options: InputModeExtensionOptions = {}) {
28
+ return (pi: ExtensionAPI) => {
29
+ let mode = loadInputMode(options.configFile);
30
+ let ui: ExtensionUIContext | undefined;
31
+
32
+ const updateStatus = () => {
33
+ if (!ui) return;
34
+ ui.setStatus(STATUS_KEY, mode === "steer" ? undefined : `input: ${mode}`);
35
+ };
36
+
37
+ const selectMode = async (args: string, ctx: ExtensionCommandContext) => {
38
+ const requested = args.trim().toLowerCase();
39
+ let selected: InputMode | undefined;
40
+ if (requested) {
41
+ if (!isInputMode(requested)) {
42
+ ctx.ui.notify(`Usage: /input-mode [${INPUT_MODES.join("|")}]`, "warning");
43
+ return;
44
+ }
45
+ selected = requested;
46
+ } else if (ctx.hasUI) {
47
+ selected = await ctx.ui.select(
48
+ `Input behavior while agent is running (current: ${mode})`,
49
+ [...INPUT_MODES],
50
+ ) as InputMode | undefined;
51
+ } else {
52
+ ctx.ui.notify(`Input mode: ${mode}. Usage: /input-mode [${INPUT_MODES.join("|")}]`, "info");
53
+ return;
54
+ }
55
+ if (!selected) return;
56
+ mode = selected;
57
+ saveInputMode(mode, options.configFile);
58
+ updateStatus();
59
+ const explanation = mode === "interrupt"
60
+ ? "new Enter input aborts the active run before it is delivered"
61
+ : mode === "follow-up"
62
+ ? "new Enter input waits until the active run finishes"
63
+ : "new Enter input is injected before the agent's next step";
64
+ ctx.ui.notify(`Input mode: ${mode} — ${explanation}.`, "info");
65
+ };
66
+
67
+ pi.registerCommand("input-mode", {
68
+ description: "Choose Enter behavior while the agent runs: steer, interrupt, or follow-up",
69
+ handler: selectMode,
70
+ });
71
+
72
+ pi.on("session_start", (_event, ctx) => {
73
+ if (ctx.hasUI) ui = ctx.ui;
74
+ updateStatus();
75
+ });
76
+
77
+ pi.on("session_shutdown", () => {
78
+ ui?.setStatus(STATUS_KEY, undefined);
79
+ ui = undefined;
80
+ });
81
+
82
+ pi.on("input", (event, ctx): InputEventResult => {
83
+ // Extension-originated results and explicit Alt+Enter follow-ups retain
84
+ // their requested delivery. Idle input and commands use Pi unchanged.
85
+ if (event.source !== "interactive" || event.streamingBehavior !== "steer") {
86
+ return { action: "continue" };
87
+ }
88
+ if (mode === "steer") return { action: "continue" };
89
+ if (mode === "follow-up") {
90
+ pi.sendUserMessage(modelInput(event), { deliverAs: "followUp" });
91
+ return { action: "handled" };
92
+ }
93
+
94
+ // Abort is signalled synchronously. Returning continue preserves Pi's
95
+ // normal template expansion, image handling, history, and queue logic;
96
+ // the prompt becomes a fresh turn if abort settlement wins the race, or
97
+ // a steering message consumed immediately after the aborted run.
98
+ ctx.abort();
99
+ return { action: "continue" };
100
+ });
101
+ };
102
+ }
103
+
104
+ export default createInputModeExtension();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shariq-pi-extensions",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Cross-platform extension suite for the Pi coding agent.",
5
5
  "license": "MIT",
6
6
  "author": "Shariq Riaz",
@@ -47,6 +47,7 @@
47
47
  "./extensions/firecrawl-web/index.ts",
48
48
  "./extensions/git-info/index.ts",
49
49
  "./extensions/goal/index.ts",
50
+ "./extensions/input-mode/index.ts",
50
51
  "./extensions/orchestration/index.ts",
51
52
  "./extensions/performance-status/index.ts",
52
53
  "./extensions/pi-memory/index.ts",