pi-cancel-current-shell 0.0.0-stage → 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/LICENSE +17 -0
- package/README.md +108 -2
- package/package.json +32 -4
- package/src/index.ts +213 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,109 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-cancel-current-shell
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Pi extension that adds `/cancel-current-shell`.
|
|
4
|
+
|
|
5
|
+
It requests cancellation of the most recently started active built-in `bash` or
|
|
6
|
+
`powershell` tool execution while allowing Pi's agent turn to continue. The
|
|
7
|
+
cancelled shell should return a normal tool result describing the cancellation,
|
|
8
|
+
so the model can inspect prior output and decide what to do next.
|
|
9
|
+
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
- Pi with extension support.
|
|
13
|
+
- A Pi version that exports `createBashTool` and `createPowerShellTool` from
|
|
14
|
+
`@earendil-works/pi-coding-agent`.
|
|
15
|
+
- Node.js runtime supported by your Pi installation.
|
|
16
|
+
|
|
17
|
+
## Install from a local folder
|
|
18
|
+
|
|
19
|
+
From the parent directory containing this package folder:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pi install ./pi-cancel-current-shell
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For npm publication, remove no files: the included `package.json` already
|
|
26
|
+
contains the Pi `pi.extensions` manifest. After publishing, install it with:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pi install npm:pi-cancel-current-shell
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Alternatively, copy the whole `pi-cancel-current-shell` folder into
|
|
33
|
+
`~/.pi/agent/extensions/`. Pi discovers packages with a `package.json` containing
|
|
34
|
+
a `pi.extensions` entry.
|
|
35
|
+
|
|
36
|
+
The extension imports the shell-tool factories from Pi's extension API
|
|
37
|
+
package. Pi documents this package as an available extension import. If your
|
|
38
|
+
Pi distribution uses a different package name/version, adjust the import to
|
|
39
|
+
match that distribution before installing.
|
|
40
|
+
|
|
41
|
+
Restart Pi or run `/reload`.
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
While Pi is executing a shell tool, enter:
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
/cancel-current-shell
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The extension selects the most recently started active `bash` or `powershell`
|
|
52
|
+
execution and aborts its tool-level signal. It does **not** call `ctx.abort()`.
|
|
53
|
+
|
|
54
|
+
Expected flow:
|
|
55
|
+
|
|
56
|
+
1. Pi starts a shell tool call.
|
|
57
|
+
2. You invoke `/cancel-current-shell`.
|
|
58
|
+
3. The extension aborts that shell tool's signal.
|
|
59
|
+
4. Pi's shell implementation performs its platform-specific process cleanup.
|
|
60
|
+
5. The tool returns cancellation information to the model.
|
|
61
|
+
6. The agent continues its existing turn.
|
|
62
|
+
|
|
63
|
+
## Shell and platform behavior
|
|
64
|
+
|
|
65
|
+
The extension delegates actual shell execution and process cleanup to Pi's
|
|
66
|
+
`createBashTool()` and `createPowerShellTool()` implementations. This is
|
|
67
|
+
intentional: it avoids hardcoding `bash`, `pwsh`, `taskkill`, Unix signals, or
|
|
68
|
+
custom process-tree logic.
|
|
69
|
+
|
|
70
|
+
- `bash` uses Pi's configured `shellPath` and normal shell resolution.
|
|
71
|
+
- `powershell` uses Pi's PowerShell tool and its configured platform behavior.
|
|
72
|
+
- A custom executable selected through `shellPath` is supported insofar as Pi's
|
|
73
|
+
Bash tool supports it.
|
|
74
|
+
- Windows, macOS, and Linux behavior depends on the process-management
|
|
75
|
+
implementation in the installed Pi version.
|
|
76
|
+
|
|
77
|
+
This does not guarantee termination of detached processes, remote jobs,
|
|
78
|
+
daemonized children, or work already delegated to another service.
|
|
79
|
+
|
|
80
|
+
## Scope and limitations
|
|
81
|
+
|
|
82
|
+
- The command cancels the most recently started active shell tool call. If
|
|
83
|
+
multiple shell calls are running concurrently, it targets only the newest.
|
|
84
|
+
- It wraps the built-in `bash` and `powershell` tools. Third-party extensions
|
|
85
|
+
that replace those tools later may supersede this wrapper.
|
|
86
|
+
- It cannot interrupt arbitrary custom tools or shell commands started outside
|
|
87
|
+
these Pi tools.
|
|
88
|
+
- If Pi itself aborts the whole turn (for example, via Escape), that separate
|
|
89
|
+
cancellation still takes precedence.
|
|
90
|
+
- The wrapper passes Pi's original abort signal through to the underlying tool.
|
|
91
|
+
If the agent turn is independently aborted, the original cancellation
|
|
92
|
+
behavior is preserved.
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
The extension is TypeScript and is loaded by Pi. No build step is required.
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
npm install
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Compatibility note
|
|
103
|
+
|
|
104
|
+
Pi's extension and shell-tool APIs evolve. This package relies on the exported
|
|
105
|
+
`createBashTool()` and `createPowerShellTool()` factories and their `execute()`
|
|
106
|
+
contract. Verify those exports against the version of Pi you run before
|
|
107
|
+
publishing this package to npm. The package is intentionally conservative about
|
|
108
|
+
process management: it delegates that work to Pi rather than attempting
|
|
109
|
+
cross-platform signal emulation itself.
|
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cancel-current-shell",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cancel the active Pi bash or PowerShell tool execution without aborting the agent turn.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "DrkWzrd",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/DrkWzrd/pi-cancel-current-shell.git"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"pi-package",
|
|
18
|
+
"pi-extension",
|
|
19
|
+
"shell",
|
|
20
|
+
"cancellation"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"pi": {
|
|
24
|
+
"extensions": [
|
|
25
|
+
"./src/index.ts"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBashTool,
|
|
3
|
+
createPowerShellTool,
|
|
4
|
+
type ExtensionAPI,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
type ToolLike = {
|
|
8
|
+
execute: (...args: any[]) => Promise<any>;
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type ActiveExecution = {
|
|
13
|
+
toolName: "bash" | "powershell";
|
|
14
|
+
controller: AbortController;
|
|
15
|
+
startedAt: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Combines several abort signals into one, with a dispose() that removes any
|
|
20
|
+
* listeners the fallback implementation attached.
|
|
21
|
+
*/
|
|
22
|
+
function combineSignals(signals: AbortSignal[]): {
|
|
23
|
+
signal: AbortSignal;
|
|
24
|
+
dispose: () => void;
|
|
25
|
+
} {
|
|
26
|
+
if (typeof AbortSignal.any === "function") {
|
|
27
|
+
return { signal: AbortSignal.any(signals), dispose: () => {} };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const listeners: Array<[AbortSignal, () => void]> = [];
|
|
32
|
+
|
|
33
|
+
const abort = () => {
|
|
34
|
+
if (!controller.signal.aborted) controller.abort();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
for (const signal of signals) {
|
|
38
|
+
if (signal.aborted) {
|
|
39
|
+
abort();
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
43
|
+
listeners.push([signal, abort]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
dispose: () => {
|
|
49
|
+
for (const [signal, listener] of listeners) {
|
|
50
|
+
signal.removeEventListener("abort", listener);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Pulls the concatenated text content out of a tool result / partial update. */
|
|
57
|
+
function extractText(result: any): string {
|
|
58
|
+
if (!result?.content || !Array.isArray(result.content)) return "";
|
|
59
|
+
return result.content
|
|
60
|
+
.filter((c: any) => c?.type === "text" && typeof c.text === "string")
|
|
61
|
+
.map((c: any) => c.text)
|
|
62
|
+
.join("");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Builds the message the model sees when the user cancels a shell command. */
|
|
66
|
+
function cancellationMessage(toolName: string, partialOutput: string): string {
|
|
67
|
+
const header = `The user cancelled the ${toolName} script execution.`;
|
|
68
|
+
const output = partialOutput.trim();
|
|
69
|
+
|
|
70
|
+
return output
|
|
71
|
+
? `${header} Output produced before cancellation:\n\n${partialOutput}\n\nInspect the output above and continue.`
|
|
72
|
+
: `${header} No output was produced before cancellation. Continue.`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cancellationResult(toolName: string, partialOutput: string) {
|
|
76
|
+
return {
|
|
77
|
+
content: [
|
|
78
|
+
{
|
|
79
|
+
type: "text" as const,
|
|
80
|
+
text: cancellationMessage(toolName, partialOutput),
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
details: {},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Errors the base tool may throw on abort that carry no useful output. */
|
|
88
|
+
function isGenericAbortMessage(message: string): boolean {
|
|
89
|
+
return /^(aborted|command aborted|the operation was aborted\.?)$/i.test(
|
|
90
|
+
message.trim(),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default function (pi: ExtensionAPI) {
|
|
95
|
+
const active = new Map<string, ActiveExecution>();
|
|
96
|
+
|
|
97
|
+
// Keep Pi responsible for shell resolution, streaming, truncation, and
|
|
98
|
+
// platform-specific process-tree termination.
|
|
99
|
+
const bash = createBashTool(pi.cwd);
|
|
100
|
+
const powershell = createPowerShellTool(pi.cwd);
|
|
101
|
+
|
|
102
|
+
function wrapTool<T extends ToolLike>(
|
|
103
|
+
toolName: "bash" | "powershell",
|
|
104
|
+
baseTool: T,
|
|
105
|
+
): T {
|
|
106
|
+
const wrapped = {
|
|
107
|
+
...baseTool,
|
|
108
|
+
async execute(...args: any[]) {
|
|
109
|
+
const [toolCallId, _params, piSignal, onUpdate] = args as [
|
|
110
|
+
string,
|
|
111
|
+
unknown,
|
|
112
|
+
AbortSignal,
|
|
113
|
+
((partial: any) => void) | undefined,
|
|
114
|
+
unknown,
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
const controller = new AbortController();
|
|
118
|
+
const { signal, dispose } = combineSignals([
|
|
119
|
+
piSignal,
|
|
120
|
+
controller.signal,
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
active.set(toolCallId, {
|
|
124
|
+
toolName,
|
|
125
|
+
controller,
|
|
126
|
+
startedAt: Date.now(),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Remember the latest streamed output while still forwarding every
|
|
130
|
+
// update to Pi so the UI keeps streaming normally.
|
|
131
|
+
// Assumes each update holds the current (possibly tail-truncated)
|
|
132
|
+
// output. If you only ever see the last chunk, switch to `+=`.
|
|
133
|
+
let lastPartial = "";
|
|
134
|
+
|
|
135
|
+
// Preserve every argument; replace only the signal and onUpdate.
|
|
136
|
+
const forwarded = [...args];
|
|
137
|
+
forwarded[2] = signal;
|
|
138
|
+
forwarded[3] = (partial: any) => {
|
|
139
|
+
const text = extractText(partial);
|
|
140
|
+
if (text) lastPartial = text;
|
|
141
|
+
if (typeof onUpdate === "function") onUpdate(partial);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const cancelledByUs = () =>
|
|
145
|
+
controller.signal.aborted && !piSignal.aborted;
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const result = await baseTool.execute(...forwarded);
|
|
149
|
+
|
|
150
|
+
// If the base tool resolves (rather than rejects) on abort, still
|
|
151
|
+
// surface the cancellation message along with whatever it returned.
|
|
152
|
+
if (cancelledByUs()) {
|
|
153
|
+
return cancellationResult(
|
|
154
|
+
toolName,
|
|
155
|
+
extractText(result) || lastPartial,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result;
|
|
160
|
+
} catch (error: any) {
|
|
161
|
+
// Never propagate our own AbortError to Pi's agent loop.
|
|
162
|
+
if (cancelledByUs()) {
|
|
163
|
+
// Prefer streamed output. Some versions of Pi's shell tools embed
|
|
164
|
+
// the captured output in the abort error, so fall back to that
|
|
165
|
+
// unless it's just a generic "aborted" message.
|
|
166
|
+
const fallback =
|
|
167
|
+
error instanceof Error && !isGenericAbortMessage(error.message)
|
|
168
|
+
? error.message
|
|
169
|
+
: "";
|
|
170
|
+
return cancellationResult(toolName, lastPartial || fallback);
|
|
171
|
+
}
|
|
172
|
+
throw error;
|
|
173
|
+
} finally {
|
|
174
|
+
dispose();
|
|
175
|
+
active.delete(toolCallId);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return wrapped as T;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// defaultActive: false so these re-registered wrappers do not force-activate
|
|
184
|
+
// on registration, keeping `-bash`/`-powershell` in defaultTools effective.
|
|
185
|
+
pi.registerTool({ ...wrapTool("bash", bash), defaultActive: false });
|
|
186
|
+
pi.registerTool({
|
|
187
|
+
...wrapTool("powershell", powershell),
|
|
188
|
+
defaultActive: false,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
pi.registerCommand("cancel-current-shell", {
|
|
192
|
+
description:
|
|
193
|
+
"Interrupt the most recently started bash or PowerShell command without cancelling the agent turn",
|
|
194
|
+
handler: async (_args, ctx) => {
|
|
195
|
+
const current = [...active.entries()].sort(
|
|
196
|
+
(a, b) => b[1].startedAt - a[1].startedAt,
|
|
197
|
+
)[0];
|
|
198
|
+
|
|
199
|
+
if (!current) {
|
|
200
|
+
ctx.ui.notify("No active shell execution to cancel.", "info");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const [toolCallId, execution] = current;
|
|
205
|
+
execution.controller.abort();
|
|
206
|
+
|
|
207
|
+
ctx.ui.notify(
|
|
208
|
+
`Cancellation requested for ${execution.toolName} execution (${toolCallId}).`,
|
|
209
|
+
"warning",
|
|
210
|
+
);
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|