@mariozechner/pi-coding-agent 0.37.8 → 0.39.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 +115 -4
- package/README.md +11 -0
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +8 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts +23 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +75 -35
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts +6 -0
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +77 -0
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/extensions/index.d.ts +3 -3
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +8 -6
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +94 -211
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +27 -30
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +102 -45
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +155 -30
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/extensions/wrapper.d.ts +5 -3
- package/dist/core/extensions/wrapper.d.ts.map +1 -1
- package/dist/core/extensions/wrapper.js +6 -4
- package/dist/core/extensions/wrapper.js.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/model-resolver.d.ts +4 -2
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +8 -9
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/sdk.d.ts +8 -5
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +39 -87
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +8 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +9 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -5
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +25 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +103 -73
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts +17 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +12 -5
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts +18 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +68 -18
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts +15 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +22 -10
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +7 -7
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts +21 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +80 -72
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts +14 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +12 -5
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/write.d.ts +15 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +9 -4
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +58 -116
- package/dist/main.js.map +1 -1
- package/dist/modes/index.d.ts +2 -2
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +7 -3
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/countdown-timer.d.ts +14 -0
- package/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -0
- package/dist/modes/interactive/components/countdown-timer.js +33 -0
- package/dist/modes/interactive/components/countdown-timer.js.map +1 -0
- package/dist/modes/interactive/components/custom-editor.d.ts +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-input.d.ts +10 -2
- package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-input.js +18 -14
- package/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +10 -2
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +18 -22
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +50 -23
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +44 -3
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +440 -139
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +7 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +34 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts +14 -7
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +45 -21
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +111 -101
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +1 -1
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +35 -7
- package/dist/utils/clipboard.js.map +1 -1
- package/docs/extensions.md +211 -15
- package/docs/sdk.md +68 -9
- package/docs/tui.md +81 -4
- package/examples/extensions/README.md +3 -0
- package/examples/extensions/claude-rules.ts +5 -2
- package/examples/extensions/handoff.ts +1 -1
- package/examples/extensions/interactive-shell.ts +196 -0
- package/examples/extensions/mac-system-theme.ts +25 -0
- package/examples/extensions/modal-editor.ts +85 -0
- package/examples/extensions/overlay-test.ts +145 -0
- package/examples/extensions/pirate.ts +7 -4
- package/examples/extensions/preset.ts +3 -3
- package/examples/extensions/qna.ts +1 -1
- package/examples/extensions/rainbow-editor.ts +95 -0
- package/examples/extensions/shutdown-command.ts +63 -0
- package/examples/extensions/snake.ts +1 -1
- package/examples/extensions/ssh.ts +220 -0
- package/examples/extensions/timed-confirm.ts +32 -25
- package/examples/extensions/todo.ts +1 -1
- package/examples/extensions/tool-override.ts +143 -0
- package/examples/extensions/tools.ts +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/04-skills.ts +4 -1
- package/package.json +6 -6
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive Shell Commands Extension
|
|
3
|
+
*
|
|
4
|
+
* Enables running interactive commands (vim, git rebase -i, htop, etc.)
|
|
5
|
+
* with full terminal access. The TUI suspends while they run.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* pi -e examples/extensions/interactive-shell.ts
|
|
9
|
+
*
|
|
10
|
+
* !vim file.txt # Auto-detected as interactive
|
|
11
|
+
* !i any-command # Force interactive mode with !i prefix
|
|
12
|
+
* !git rebase -i HEAD~3
|
|
13
|
+
* !htop
|
|
14
|
+
*
|
|
15
|
+
* Configuration via environment variables:
|
|
16
|
+
* INTERACTIVE_COMMANDS - Additional commands (comma-separated)
|
|
17
|
+
* INTERACTIVE_EXCLUDE - Commands to exclude (comma-separated)
|
|
18
|
+
*
|
|
19
|
+
* Note: This only intercepts user `!` commands, not agent bash tool calls.
|
|
20
|
+
* If the agent runs an interactive command, it will fail (which is fine).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { spawnSync } from "node:child_process";
|
|
24
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
25
|
+
|
|
26
|
+
// Default interactive commands - editors, pagers, git ops, TUIs
|
|
27
|
+
const DEFAULT_INTERACTIVE_COMMANDS = [
|
|
28
|
+
// Editors
|
|
29
|
+
"vim",
|
|
30
|
+
"nvim",
|
|
31
|
+
"vi",
|
|
32
|
+
"nano",
|
|
33
|
+
"emacs",
|
|
34
|
+
"pico",
|
|
35
|
+
"micro",
|
|
36
|
+
"helix",
|
|
37
|
+
"hx",
|
|
38
|
+
"kak",
|
|
39
|
+
// Pagers
|
|
40
|
+
"less",
|
|
41
|
+
"more",
|
|
42
|
+
"most",
|
|
43
|
+
// Git interactive
|
|
44
|
+
"git commit",
|
|
45
|
+
"git rebase",
|
|
46
|
+
"git merge",
|
|
47
|
+
"git cherry-pick",
|
|
48
|
+
"git revert",
|
|
49
|
+
"git add -p",
|
|
50
|
+
"git add --patch",
|
|
51
|
+
"git add -i",
|
|
52
|
+
"git add --interactive",
|
|
53
|
+
"git stash -p",
|
|
54
|
+
"git stash --patch",
|
|
55
|
+
"git reset -p",
|
|
56
|
+
"git reset --patch",
|
|
57
|
+
"git checkout -p",
|
|
58
|
+
"git checkout --patch",
|
|
59
|
+
"git difftool",
|
|
60
|
+
"git mergetool",
|
|
61
|
+
// System monitors
|
|
62
|
+
"htop",
|
|
63
|
+
"top",
|
|
64
|
+
"btop",
|
|
65
|
+
"glances",
|
|
66
|
+
// File managers
|
|
67
|
+
"ranger",
|
|
68
|
+
"nnn",
|
|
69
|
+
"lf",
|
|
70
|
+
"mc",
|
|
71
|
+
"vifm",
|
|
72
|
+
// Git TUIs
|
|
73
|
+
"tig",
|
|
74
|
+
"lazygit",
|
|
75
|
+
"gitui",
|
|
76
|
+
// Fuzzy finders
|
|
77
|
+
"fzf",
|
|
78
|
+
"sk",
|
|
79
|
+
// Remote sessions
|
|
80
|
+
"ssh",
|
|
81
|
+
"telnet",
|
|
82
|
+
"mosh",
|
|
83
|
+
// Database clients
|
|
84
|
+
"psql",
|
|
85
|
+
"mysql",
|
|
86
|
+
"sqlite3",
|
|
87
|
+
"mongosh",
|
|
88
|
+
"redis-cli",
|
|
89
|
+
// Kubernetes/Docker
|
|
90
|
+
"kubectl edit",
|
|
91
|
+
"kubectl exec -it",
|
|
92
|
+
"docker exec -it",
|
|
93
|
+
"docker run -it",
|
|
94
|
+
// Other
|
|
95
|
+
"tmux",
|
|
96
|
+
"screen",
|
|
97
|
+
"ncdu",
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
function getInteractiveCommands(): string[] {
|
|
101
|
+
const additional =
|
|
102
|
+
process.env.INTERACTIVE_COMMANDS?.split(",")
|
|
103
|
+
.map((s) => s.trim())
|
|
104
|
+
.filter(Boolean) ?? [];
|
|
105
|
+
const excluded = new Set(process.env.INTERACTIVE_EXCLUDE?.split(",").map((s) => s.trim().toLowerCase()) ?? []);
|
|
106
|
+
return [...DEFAULT_INTERACTIVE_COMMANDS, ...additional].filter((cmd) => !excluded.has(cmd.toLowerCase()));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isInteractiveCommand(command: string): boolean {
|
|
110
|
+
const trimmed = command.trim().toLowerCase();
|
|
111
|
+
const commands = getInteractiveCommands();
|
|
112
|
+
|
|
113
|
+
for (const cmd of commands) {
|
|
114
|
+
const cmdLower = cmd.toLowerCase();
|
|
115
|
+
// Match at start
|
|
116
|
+
if (trimmed === cmdLower || trimmed.startsWith(`${cmdLower} `) || trimmed.startsWith(`${cmdLower}\t`)) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
// Match after pipe: "cat file | less"
|
|
120
|
+
const pipeIdx = trimmed.lastIndexOf("|");
|
|
121
|
+
if (pipeIdx !== -1) {
|
|
122
|
+
const afterPipe = trimmed.slice(pipeIdx + 1).trim();
|
|
123
|
+
if (afterPipe === cmdLower || afterPipe.startsWith(`${cmdLower} `)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default function (pi: ExtensionAPI) {
|
|
132
|
+
pi.on("user_bash", async (event, ctx) => {
|
|
133
|
+
let command = event.command;
|
|
134
|
+
let forceInteractive = false;
|
|
135
|
+
|
|
136
|
+
// Check for !i prefix (command comes without the leading !)
|
|
137
|
+
// The prefix parsing happens before this event, so we check if command starts with "i "
|
|
138
|
+
if (command.startsWith("i ") || command.startsWith("i\t")) {
|
|
139
|
+
forceInteractive = true;
|
|
140
|
+
command = command.slice(2).trim();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const shouldBeInteractive = forceInteractive || isInteractiveCommand(command);
|
|
144
|
+
if (!shouldBeInteractive) {
|
|
145
|
+
return; // Let normal handling proceed
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// No UI available (print mode, RPC, etc.)
|
|
149
|
+
if (!ctx.hasUI) {
|
|
150
|
+
return {
|
|
151
|
+
result: { output: "(interactive commands require TUI)", exitCode: 1, cancelled: false, truncated: false },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Use ctx.ui.custom() to get TUI access, then run the command
|
|
156
|
+
const exitCode = await ctx.ui.custom<number | null>((tui, _theme, _kb, done) => {
|
|
157
|
+
// Stop TUI to release terminal
|
|
158
|
+
tui.stop();
|
|
159
|
+
|
|
160
|
+
// Clear screen
|
|
161
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
162
|
+
|
|
163
|
+
// Run command with full terminal access
|
|
164
|
+
const shell = process.env.SHELL || "/bin/sh";
|
|
165
|
+
const result = spawnSync(shell, ["-c", command], {
|
|
166
|
+
stdio: "inherit",
|
|
167
|
+
env: process.env,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Restart TUI
|
|
171
|
+
tui.start();
|
|
172
|
+
tui.requestRender(true);
|
|
173
|
+
|
|
174
|
+
// Signal completion
|
|
175
|
+
done(result.status);
|
|
176
|
+
|
|
177
|
+
// Return empty component (immediately disposed since done() was called)
|
|
178
|
+
return { render: () => [], invalidate: () => {} };
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Return result to prevent default bash handling
|
|
182
|
+
const output =
|
|
183
|
+
exitCode === 0
|
|
184
|
+
? "(interactive command completed successfully)"
|
|
185
|
+
: `(interactive command exited with code ${exitCode})`;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
result: {
|
|
189
|
+
output,
|
|
190
|
+
exitCode: exitCode ?? 1,
|
|
191
|
+
cancelled: false,
|
|
192
|
+
truncated: false,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syncs pi theme with macOS system appearance (dark/light mode).
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* pi -e examples/extensions/mac-system-theme.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
function isDarkMode(): boolean {
|
|
12
|
+
try {
|
|
13
|
+
execSync("defaults read -g AppleInterfaceStyle", { encoding: "utf-8" });
|
|
14
|
+
return true; // Returns "Dark" if dark mode
|
|
15
|
+
} catch {
|
|
16
|
+
return false; // Throws if light mode
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function (pi: ExtensionAPI) {
|
|
21
|
+
pi.on("session_start", (_event, ctx) => {
|
|
22
|
+
const theme = isDarkMode() ? "dark" : "light";
|
|
23
|
+
ctx.ui.setTheme(theme);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal Editor - vim-like modal editing example
|
|
3
|
+
*
|
|
4
|
+
* Usage: pi --extension ./examples/extensions/modal-editor.ts
|
|
5
|
+
*
|
|
6
|
+
* - Escape: insert → normal mode (in normal mode, aborts agent)
|
|
7
|
+
* - i: normal → insert mode
|
|
8
|
+
* - hjkl: navigation in normal mode
|
|
9
|
+
* - ctrl+c, ctrl+d, etc. work in both modes
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { CustomEditor, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
14
|
+
|
|
15
|
+
// Normal mode key mappings: key -> escape sequence (or null for mode switch)
|
|
16
|
+
const NORMAL_KEYS: Record<string, string | null> = {
|
|
17
|
+
h: "\x1b[D", // left
|
|
18
|
+
j: "\x1b[B", // down
|
|
19
|
+
k: "\x1b[A", // up
|
|
20
|
+
l: "\x1b[C", // right
|
|
21
|
+
"0": "\x01", // line start
|
|
22
|
+
$: "\x05", // line end
|
|
23
|
+
x: "\x1b[3~", // delete char
|
|
24
|
+
i: null, // insert mode
|
|
25
|
+
a: null, // append (insert + right)
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
class ModalEditor extends CustomEditor {
|
|
29
|
+
private mode: "normal" | "insert" = "insert";
|
|
30
|
+
|
|
31
|
+
handleInput(data: string): void {
|
|
32
|
+
// Escape toggles to normal mode, or passes through for app handling
|
|
33
|
+
if (matchesKey(data, "escape")) {
|
|
34
|
+
if (this.mode === "insert") {
|
|
35
|
+
this.mode = "normal";
|
|
36
|
+
} else {
|
|
37
|
+
super.handleInput(data); // abort agent, etc.
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Insert mode: pass everything through
|
|
43
|
+
if (this.mode === "insert") {
|
|
44
|
+
super.handleInput(data);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Normal mode: check mapped keys
|
|
49
|
+
if (data in NORMAL_KEYS) {
|
|
50
|
+
const seq = NORMAL_KEYS[data];
|
|
51
|
+
if (data === "i") {
|
|
52
|
+
this.mode = "insert";
|
|
53
|
+
} else if (data === "a") {
|
|
54
|
+
this.mode = "insert";
|
|
55
|
+
super.handleInput("\x1b[C"); // move right first
|
|
56
|
+
} else if (seq) {
|
|
57
|
+
super.handleInput(seq);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Pass control sequences (ctrl+c, etc.) to super, ignore printable chars
|
|
63
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) return;
|
|
64
|
+
super.handleInput(data);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
render(width: number): string[] {
|
|
68
|
+
const lines = super.render(width);
|
|
69
|
+
if (lines.length === 0) return lines;
|
|
70
|
+
|
|
71
|
+
// Add mode indicator to bottom border
|
|
72
|
+
const label = this.mode === "normal" ? " NORMAL " : " INSERT ";
|
|
73
|
+
const last = lines.length - 1;
|
|
74
|
+
if (visibleWidth(lines[last]!) >= label.length) {
|
|
75
|
+
lines[last] = truncateToWidth(lines[last]!, width - label.length, "") + label;
|
|
76
|
+
}
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default function (pi: ExtensionAPI) {
|
|
82
|
+
pi.on("session_start", (_event, ctx) => {
|
|
83
|
+
ctx.ui.setEditorComponent((_tui, theme, kb) => new ModalEditor(theme, kb));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overlay Test - validates overlay compositing with inline text inputs
|
|
3
|
+
*
|
|
4
|
+
* Usage: pi --extension ./examples/extensions/overlay-test.ts
|
|
5
|
+
*
|
|
6
|
+
* Run /overlay-test to show a floating overlay with:
|
|
7
|
+
* - Inline text inputs within menu items
|
|
8
|
+
* - Edge case tests (wide chars, styled text, emoji)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import { matchesKey, visibleWidth } from "@mariozechner/pi-tui";
|
|
13
|
+
|
|
14
|
+
export default function (pi: ExtensionAPI) {
|
|
15
|
+
pi.registerCommand("overlay-test", {
|
|
16
|
+
description: "Test overlay rendering with edge cases",
|
|
17
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
18
|
+
const result = await ctx.ui.custom<{ action: string; query?: string } | undefined>(
|
|
19
|
+
(_tui, theme, _keybindings, done) => new OverlayTestComponent(theme, done),
|
|
20
|
+
{ overlay: true },
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (result) {
|
|
24
|
+
const msg = result.query ? `${result.action}: "${result.query}"` : result.action;
|
|
25
|
+
ctx.ui.notify(msg, "info");
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class OverlayTestComponent {
|
|
32
|
+
readonly width = 70;
|
|
33
|
+
|
|
34
|
+
private selected = 0;
|
|
35
|
+
private items = [
|
|
36
|
+
{ label: "Search", hasInput: true, text: "", cursor: 0 },
|
|
37
|
+
{ label: "Run", hasInput: true, text: "", cursor: 0 },
|
|
38
|
+
{ label: "Settings", hasInput: false, text: "", cursor: 0 },
|
|
39
|
+
{ label: "Cancel", hasInput: false, text: "", cursor: 0 },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
private theme: Theme,
|
|
44
|
+
private done: (result: { action: string; query?: string } | undefined) => void,
|
|
45
|
+
) {}
|
|
46
|
+
|
|
47
|
+
handleInput(data: string): void {
|
|
48
|
+
if (matchesKey(data, "escape")) {
|
|
49
|
+
this.done(undefined);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const current = this.items[this.selected]!;
|
|
54
|
+
|
|
55
|
+
if (matchesKey(data, "return")) {
|
|
56
|
+
this.done({ action: current.label, query: current.hasInput ? current.text : undefined });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (matchesKey(data, "up")) {
|
|
61
|
+
this.selected = Math.max(0, this.selected - 1);
|
|
62
|
+
} else if (matchesKey(data, "down")) {
|
|
63
|
+
this.selected = Math.min(this.items.length - 1, this.selected + 1);
|
|
64
|
+
} else if (current.hasInput) {
|
|
65
|
+
if (matchesKey(data, "backspace")) {
|
|
66
|
+
if (current.cursor > 0) {
|
|
67
|
+
current.text = current.text.slice(0, current.cursor - 1) + current.text.slice(current.cursor);
|
|
68
|
+
current.cursor--;
|
|
69
|
+
}
|
|
70
|
+
} else if (matchesKey(data, "left")) {
|
|
71
|
+
current.cursor = Math.max(0, current.cursor - 1);
|
|
72
|
+
} else if (matchesKey(data, "right")) {
|
|
73
|
+
current.cursor = Math.min(current.text.length, current.cursor + 1);
|
|
74
|
+
} else if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
75
|
+
current.text = current.text.slice(0, current.cursor) + data + current.text.slice(current.cursor);
|
|
76
|
+
current.cursor++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
render(_width: number): string[] {
|
|
82
|
+
const w = this.width;
|
|
83
|
+
const th = this.theme;
|
|
84
|
+
const innerW = w - 2;
|
|
85
|
+
const lines: string[] = [];
|
|
86
|
+
|
|
87
|
+
const pad = (s: string, len: number) => {
|
|
88
|
+
const vis = visibleWidth(s);
|
|
89
|
+
return s + " ".repeat(Math.max(0, len - vis));
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const row = (content: string) => th.fg("border", "│") + pad(content, innerW) + th.fg("border", "│");
|
|
93
|
+
|
|
94
|
+
lines.push(th.fg("border", `╭${"─".repeat(innerW)}╮`));
|
|
95
|
+
lines.push(row(` ${th.fg("accent", "🧪 Overlay Test")}`));
|
|
96
|
+
lines.push(row(""));
|
|
97
|
+
|
|
98
|
+
// Edge cases - full width lines to test compositing at boundaries
|
|
99
|
+
lines.push(row(` ${th.fg("dim", "─── Edge Cases (borders should align) ───")}`));
|
|
100
|
+
lines.push(row(` Wide: ${th.fg("warning", "中文日本語한글テスト漢字繁體简体ひらがなカタカナ가나다라마바")}`));
|
|
101
|
+
lines.push(
|
|
102
|
+
row(
|
|
103
|
+
` Styled: ${th.fg("error", "RED")} ${th.fg("success", "GREEN")} ${th.fg("warning", "YELLOW")} ${th.fg("accent", "ACCENT")} ${th.fg("dim", "DIM")} ${th.fg("error", "more")} ${th.fg("success", "colors")}`,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
lines.push(row(" Emoji: 👨👩👧👦 🇯🇵 🚀 💻 🎉 🔥 😀 🎯 🌟 💡 🎨 🔧 📦 🏆 🌈 🎪 🎭 🎬 🎮 🎲"));
|
|
107
|
+
lines.push(row(""));
|
|
108
|
+
|
|
109
|
+
// Menu with inline inputs
|
|
110
|
+
lines.push(row(` ${th.fg("dim", "─── Actions ───")}`));
|
|
111
|
+
|
|
112
|
+
for (let i = 0; i < this.items.length; i++) {
|
|
113
|
+
const item = this.items[i]!;
|
|
114
|
+
const isSelected = i === this.selected;
|
|
115
|
+
const prefix = isSelected ? " ▶ " : " ";
|
|
116
|
+
|
|
117
|
+
let content: string;
|
|
118
|
+
if (item.hasInput) {
|
|
119
|
+
const label = isSelected ? th.fg("accent", `${item.label}:`) : th.fg("text", `${item.label}:`);
|
|
120
|
+
|
|
121
|
+
let inputDisplay = item.text;
|
|
122
|
+
if (isSelected) {
|
|
123
|
+
const before = inputDisplay.slice(0, item.cursor);
|
|
124
|
+
const cursorChar = item.cursor < inputDisplay.length ? inputDisplay[item.cursor] : " ";
|
|
125
|
+
const after = inputDisplay.slice(item.cursor + 1);
|
|
126
|
+
inputDisplay = `${before}\x1b[7m${cursorChar}\x1b[27m${after}`;
|
|
127
|
+
}
|
|
128
|
+
content = `${prefix + label} ${inputDisplay}`;
|
|
129
|
+
} else {
|
|
130
|
+
content = prefix + (isSelected ? th.fg("accent", item.label) : th.fg("text", item.label));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
lines.push(row(content));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
lines.push(row(""));
|
|
137
|
+
lines.push(row(` ${th.fg("dim", "↑↓ navigate • type to input • Enter select • Esc cancel")}`));
|
|
138
|
+
lines.push(th.fg("border", `╰${"─".repeat(innerW)}╯`));
|
|
139
|
+
|
|
140
|
+
return lines;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
invalidate(): void {}
|
|
144
|
+
dispose(): void {}
|
|
145
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pirate Extension
|
|
3
3
|
*
|
|
4
|
-
* Demonstrates
|
|
5
|
-
*
|
|
4
|
+
* Demonstrates modifying the system prompt in before_agent_start to dynamically
|
|
5
|
+
* change agent behavior based on extension state.
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
8
|
* 1. Copy this file to ~/.pi/agent/extensions/ or your project's .pi/extensions/
|
|
@@ -25,10 +25,13 @@ export default function pirateExtension(pi: ExtensionAPI) {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
// Append to system prompt when pirate mode is enabled
|
|
28
|
-
pi.on("before_agent_start", async () => {
|
|
28
|
+
pi.on("before_agent_start", async (event) => {
|
|
29
29
|
if (pirateMode) {
|
|
30
30
|
return {
|
|
31
|
-
|
|
31
|
+
systemPrompt:
|
|
32
|
+
event.systemPrompt +
|
|
33
|
+
`
|
|
34
|
+
|
|
32
35
|
IMPORTANT: You are now in PIRATE MODE. You must:
|
|
33
36
|
- Speak like a stereotypical pirate in all responses
|
|
34
37
|
- Use phrases like "Arrr!", "Ahoy!", "Shiver me timbers!", "Avast!", "Ye scurvy dog!"
|
|
@@ -206,7 +206,7 @@ export default function presetExtension(pi: ExtensionAPI) {
|
|
|
206
206
|
description: "Clear active preset, restore defaults",
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
-
const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
|
|
209
|
+
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
210
210
|
const container = new Container();
|
|
211
211
|
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
|
|
212
212
|
|
|
@@ -345,10 +345,10 @@ export default function presetExtension(pi: ExtensionAPI) {
|
|
|
345
345
|
});
|
|
346
346
|
|
|
347
347
|
// Inject preset instructions into system prompt
|
|
348
|
-
pi.on("before_agent_start", async () => {
|
|
348
|
+
pi.on("before_agent_start", async (event) => {
|
|
349
349
|
if (activePreset?.instructions) {
|
|
350
350
|
return {
|
|
351
|
-
|
|
351
|
+
systemPrompt: `${event.systemPrompt}\n\n${activePreset.instructions}`,
|
|
352
352
|
};
|
|
353
353
|
}
|
|
354
354
|
});
|
|
@@ -71,7 +71,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// Run extraction with loader UI
|
|
74
|
-
const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
|
|
74
|
+
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
75
75
|
const loader = new BorderedLoader(tui, theme, `Extracting questions using ${ctx.model!.id}...`);
|
|
76
76
|
loader.onAbort = () => done(null);
|
|
77
77
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rainbow Editor - highlights "ultrathink" with animated shine effect
|
|
3
|
+
*
|
|
4
|
+
* Usage: pi --extension ./examples/extensions/rainbow-editor.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { CustomEditor, type ExtensionAPI, type KeybindingsManager } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import type { EditorTheme, TUI } from "@mariozechner/pi-tui";
|
|
9
|
+
|
|
10
|
+
// Base colors (coral → yellow → green → teal → blue → purple → pink)
|
|
11
|
+
const COLORS: [number, number, number][] = [
|
|
12
|
+
[233, 137, 115], // coral
|
|
13
|
+
[228, 186, 103], // yellow
|
|
14
|
+
[141, 192, 122], // green
|
|
15
|
+
[102, 194, 179], // teal
|
|
16
|
+
[121, 157, 207], // blue
|
|
17
|
+
[157, 134, 195], // purple
|
|
18
|
+
[206, 130, 172], // pink
|
|
19
|
+
];
|
|
20
|
+
const RESET = "\x1b[0m";
|
|
21
|
+
|
|
22
|
+
function brighten(rgb: [number, number, number], factor: number): string {
|
|
23
|
+
const [r, g, b] = rgb.map((c) => Math.round(c + (255 - c) * factor));
|
|
24
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function colorize(text: string, shinePos: number): string {
|
|
28
|
+
return (
|
|
29
|
+
[...text]
|
|
30
|
+
.map((c, i) => {
|
|
31
|
+
const baseColor = COLORS[i % COLORS.length]!;
|
|
32
|
+
// 3-letter shine: center bright, adjacent dimmer
|
|
33
|
+
let factor = 0;
|
|
34
|
+
if (shinePos >= 0) {
|
|
35
|
+
const dist = Math.abs(i - shinePos);
|
|
36
|
+
if (dist === 0) factor = 0.7;
|
|
37
|
+
else if (dist === 1) factor = 0.35;
|
|
38
|
+
}
|
|
39
|
+
return `${brighten(baseColor, factor)}${c}`;
|
|
40
|
+
})
|
|
41
|
+
.join("") + RESET
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class RainbowEditor extends CustomEditor {
|
|
46
|
+
private animationTimer?: ReturnType<typeof setInterval>;
|
|
47
|
+
private tui: TUI;
|
|
48
|
+
private frame = 0;
|
|
49
|
+
|
|
50
|
+
constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) {
|
|
51
|
+
super(theme, keybindings);
|
|
52
|
+
this.tui = tui;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private hasUltrathink(): boolean {
|
|
56
|
+
return /ultrathink/i.test(this.getText());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private startAnimation(): void {
|
|
60
|
+
if (this.animationTimer) return;
|
|
61
|
+
this.animationTimer = setInterval(() => {
|
|
62
|
+
this.frame++;
|
|
63
|
+
this.tui.requestRender();
|
|
64
|
+
}, 60);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private stopAnimation(): void {
|
|
68
|
+
if (this.animationTimer) {
|
|
69
|
+
clearInterval(this.animationTimer);
|
|
70
|
+
this.animationTimer = undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
handleInput(data: string): void {
|
|
75
|
+
super.handleInput(data);
|
|
76
|
+
if (this.hasUltrathink()) {
|
|
77
|
+
this.startAnimation();
|
|
78
|
+
} else {
|
|
79
|
+
this.stopAnimation();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
render(width: number): string[] {
|
|
84
|
+
// Cycle: 10 shine positions + 10 pause frames
|
|
85
|
+
const cycle = this.frame % 20;
|
|
86
|
+
const shinePos = cycle < 10 ? cycle : -1; // -1 means no shine (pause)
|
|
87
|
+
return super.render(width).map((line) => line.replace(/ultrathink/gi, (m) => colorize(m, shinePos)));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default function (pi: ExtensionAPI) {
|
|
92
|
+
pi.on("session_start", (_event, ctx) => {
|
|
93
|
+
ctx.ui.setEditorComponent((tui, theme, kb) => new RainbowEditor(tui, theme, kb));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shutdown Command Extension
|
|
3
|
+
*
|
|
4
|
+
* Adds a /quit command that allows extensions to trigger clean shutdown.
|
|
5
|
+
* Demonstrates how extensions can use ctx.shutdown() to exit pi cleanly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { Type } from "@sinclair/typebox";
|
|
10
|
+
|
|
11
|
+
export default function (pi: ExtensionAPI) {
|
|
12
|
+
// Register a /quit command that cleanly exits pi
|
|
13
|
+
pi.registerCommand("quit", {
|
|
14
|
+
description: "Exit pi cleanly",
|
|
15
|
+
handler: async (_args, ctx) => {
|
|
16
|
+
ctx.shutdown();
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// You can also create a tool that shuts down after completing work
|
|
21
|
+
pi.registerTool({
|
|
22
|
+
name: "finish_and_exit",
|
|
23
|
+
label: "Finish and Exit",
|
|
24
|
+
description: "Complete a task and exit pi",
|
|
25
|
+
parameters: Type.Object({}),
|
|
26
|
+
async execute(_toolCallId, _params, _onUpdate, ctx, _signal) {
|
|
27
|
+
// Do any final work here...
|
|
28
|
+
// Request graceful shutdown (deferred until agent is idle)
|
|
29
|
+
ctx.shutdown();
|
|
30
|
+
|
|
31
|
+
// This return is sent to the LLM before shutdown occurs
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text", text: "Shutdown requested. Exiting after this response." }],
|
|
34
|
+
details: {},
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// You could also create a more complex tool with parameters
|
|
40
|
+
pi.registerTool({
|
|
41
|
+
name: "deploy_and_exit",
|
|
42
|
+
label: "Deploy and Exit",
|
|
43
|
+
description: "Deploy the application and exit pi",
|
|
44
|
+
parameters: Type.Object({
|
|
45
|
+
environment: Type.String({ description: "Target environment (e.g., production, staging)" }),
|
|
46
|
+
}),
|
|
47
|
+
async execute(_toolCallId, params, onUpdate, ctx, _signal) {
|
|
48
|
+
onUpdate?.({ content: [{ type: "text", text: `Deploying to ${params.environment}...` }], details: {} });
|
|
49
|
+
|
|
50
|
+
// Example deployment logic
|
|
51
|
+
// const result = await pi.exec("npm", ["run", "deploy", params.environment], { signal });
|
|
52
|
+
|
|
53
|
+
// On success, request graceful shutdown
|
|
54
|
+
onUpdate?.({ content: [{ type: "text", text: "Deployment complete, exiting..." }], details: {} });
|
|
55
|
+
ctx.shutdown();
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text", text: "Done! Shutdown requested." }],
|
|
59
|
+
details: { environment: params.environment },
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|