@agnishc/edb-agent-steer 0.1.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 +7 -0
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/package.json +26 -0
- package/src/component.ts +52 -0
- package/src/index.ts +93 -0
- package/src/types.ts +3 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agnish Chakraborty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @agnishc/edb-agent-steer
|
|
2
|
+
|
|
3
|
+
A Pi CLI extension that intercepts messages sent while the agent is already running and presents a compact single-keypress prompt:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
↵ "your message here"
|
|
7
|
+
|
|
8
|
+
s steer q queue d discard e edit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
| Action | Behaviour |
|
|
12
|
+
|--------|-----------|
|
|
13
|
+
| `s` steer | Delivers the message before the next LLM call (same turn) |
|
|
14
|
+
| `q` queue | Delivers the message after the agent fully finishes |
|
|
15
|
+
| `d` discard | Throws the message away |
|
|
16
|
+
| `e` edit / Esc | Restores the text to the editor |
|
|
17
|
+
|
|
18
|
+
When the agent is **idle**, messages pass through normally — no prompt appears.
|
|
19
|
+
|
|
20
|
+
Also registers a `/steer <text>` command that bypasses the prompt and steers directly.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pi install npm:@agnishc/edb-agent-steer
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
Just type while the agent is running. The prompt appears automatically.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
/steer reconsider the approach — use a queue instead
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
[MIT](LICENSE) © Agnish Chakraborty
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agnishc/edb-agent-steer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi extension: intercepts mid-turn messages with a steer / queue / discard / edit prompt",
|
|
5
|
+
"keywords": ["pi-package", "pi-extension", "edb"],
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Agnish Chakraborty",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/agnishcc/pi-extention-monorepo.git",
|
|
12
|
+
"directory": "packages/edb-agent-steer"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/agnishcc/pi-extention-monorepo/tree/main/packages/edb-agent-steer#readme",
|
|
15
|
+
"bugs": { "url": "https://github.com/agnishcc/pi-extention-monorepo/issues" },
|
|
16
|
+
"publishConfig": { "access": "public" },
|
|
17
|
+
"scripts": { "test": "vitest run" },
|
|
18
|
+
"files": ["src", "README.md", "LICENSE", "CHANGELOG.md"],
|
|
19
|
+
"pi": {
|
|
20
|
+
"extensions": ["./src/index.ts"]
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
24
|
+
"@mariozechner/pi-tui": "*"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/component.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
2
|
+
import type { SteerChoice } from "./types";
|
|
3
|
+
|
|
4
|
+
// ── Component ──────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export class SteerPromptComponent {
|
|
7
|
+
private cachedWidth?: number;
|
|
8
|
+
private cachedLines?: string[];
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly message: string,
|
|
12
|
+
private readonly theme: any,
|
|
13
|
+
private readonly done: (choice: SteerChoice) => void,
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
handleInput(data: string): void {
|
|
17
|
+
if (matchesKey(data, "s")) this.done("steer");
|
|
18
|
+
else if (matchesKey(data, "q")) this.done("queue");
|
|
19
|
+
else if (matchesKey(data, "d")) this.done("discard");
|
|
20
|
+
else if (matchesKey(data, "e") || matchesKey(data, "escape")) this.done("edit");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
render(width: number): string[] {
|
|
24
|
+
if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
|
|
25
|
+
|
|
26
|
+
const th = this.theme;
|
|
27
|
+
const maxMsgW = Math.max(10, width - 8);
|
|
28
|
+
const preview = this.message.length > maxMsgW ? `${this.message.slice(0, maxMsgW - 1)}…` : this.message;
|
|
29
|
+
|
|
30
|
+
const key = (k: string, label: string) => `${th.fg("accent", k)}${th.fg("dim", ` ${label}`)}`;
|
|
31
|
+
|
|
32
|
+
const lines: string[] = [
|
|
33
|
+
"",
|
|
34
|
+
truncateToWidth(` ${th.fg("muted", "↵")} ${th.fg("dim", `"${preview}"`)}`, width),
|
|
35
|
+
"",
|
|
36
|
+
truncateToWidth(
|
|
37
|
+
` ${key("s", "steer")} ${key("q", "queue")} ${key("d", "discard")} ${key("e", "edit")}`,
|
|
38
|
+
width,
|
|
39
|
+
),
|
|
40
|
+
"",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
this.cachedWidth = width;
|
|
44
|
+
this.cachedLines = lines;
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
invalidate(): void {
|
|
49
|
+
this.cachedWidth = undefined;
|
|
50
|
+
this.cachedLines = undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-agent-steer
|
|
3
|
+
*
|
|
4
|
+
* When the user submits a message while the agent is already running, a
|
|
5
|
+
* compact inline prompt appears showing four single-key options:
|
|
6
|
+
*
|
|
7
|
+
* s steer — deliver before the next LLM call (same session)
|
|
8
|
+
* q queue — deliver after the agent fully finishes
|
|
9
|
+
* d discard — throw the message away
|
|
10
|
+
* e edit — restore the text to the editor (also: Esc)
|
|
11
|
+
*
|
|
12
|
+
* A single keypress acts immediately — no Enter, no dialog, no Esc conflicts.
|
|
13
|
+
* When the agent is idle the message is passed through normally — no prompt.
|
|
14
|
+
* The /steer <text> slash command skips the prompt entirely and always steers directly.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
18
|
+
import { SteerPromptComponent } from "./component";
|
|
19
|
+
import type { SteerChoice } from "./types";
|
|
20
|
+
|
|
21
|
+
// ── Extension ──────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export default function steerExtension(pi: ExtensionAPI): void {
|
|
24
|
+
// ── Input interceptor ─────────────────────────────────────────────────────
|
|
25
|
+
pi.on("input", async (event, ctx) => {
|
|
26
|
+
// Only intercept typed messages, not extension-injected ones.
|
|
27
|
+
if (event.source !== "interactive") return { action: "continue" };
|
|
28
|
+
|
|
29
|
+
// Don't intercept empty input — Esc in the editor fires input with
|
|
30
|
+
// event.text === "" and we must not block that.
|
|
31
|
+
if (!event.text?.trim()) return { action: "continue" };
|
|
32
|
+
|
|
33
|
+
// Agent is idle — pass through normally.
|
|
34
|
+
if (ctx.isIdle()) return { action: "continue" };
|
|
35
|
+
|
|
36
|
+
// No interactive UI (print / JSON mode) — pass through.
|
|
37
|
+
if (!ctx.hasUI) return { action: "continue" };
|
|
38
|
+
|
|
39
|
+
// Show the single-keypress prompt.
|
|
40
|
+
const choice = await ctx.ui.custom<SteerChoice>(
|
|
41
|
+
(_tui, theme, _kb, done) => new SteerPromptComponent(event.text, theme, done),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// edit / Esc / unexpected null → restore text to editor, do nothing else.
|
|
45
|
+
if (!choice || choice === "edit") {
|
|
46
|
+
ctx.ui.setEditorText(event.text);
|
|
47
|
+
return { action: "handled" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (choice === "discard") {
|
|
51
|
+
ctx.ui.notify("Message discarded", "info");
|
|
52
|
+
return { action: "handled" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (choice === "steer") {
|
|
56
|
+
pi.sendUserMessage(event.text, { deliverAs: "steer" });
|
|
57
|
+
ctx.ui.notify("◀ Steered — before the next LLM call", "info");
|
|
58
|
+
return { action: "handled" };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (choice === "queue") {
|
|
62
|
+
pi.sendUserMessage(event.text, { deliverAs: "followUp" });
|
|
63
|
+
ctx.ui.notify("⏳ Queued — after agent finishes", "info");
|
|
64
|
+
return { action: "handled" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { action: "continue" };
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ── /steer slash command — explicit steer, bypasses the prompt ────────────
|
|
71
|
+
pi.registerCommand("steer", {
|
|
72
|
+
description: "Steer a message directly into the running agent turn. " + "Usage: /steer <text>",
|
|
73
|
+
|
|
74
|
+
handler: async (args, ctx) => {
|
|
75
|
+
const content = args?.trim();
|
|
76
|
+
|
|
77
|
+
if (!content) {
|
|
78
|
+
ctx.ui.notify("/steer <text> — steer text before the next LLM call", "info");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const running = !ctx.isIdle();
|
|
83
|
+
|
|
84
|
+
if (running) {
|
|
85
|
+
pi.sendUserMessage(content, { deliverAs: "steer" });
|
|
86
|
+
} else {
|
|
87
|
+
pi.sendUserMessage(content);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
ctx.ui.notify(running ? "◀ Steered — before the next LLM call" : "◀ Steered — triggering response", "info");
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
package/src/types.ts
ADDED