pi-async-bash 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 +3 -0
- package/LICENSE +22 -0
- package/README.md +105 -0
- package/RELEASING.md +23 -0
- package/index.ts +1 -0
- package/package.json +67 -0
- package/src/commands.ts +23 -0
- package/src/format.ts +78 -0
- package/src/hint.ts +37 -0
- package/src/index.ts +91 -0
- package/src/input.ts +35 -0
- package/src/lifecycle.ts +450 -0
- package/src/log-search.ts +129 -0
- package/src/monitor-follow.ts +103 -0
- package/src/monitor-session.ts +164 -0
- package/src/monitor-source.ts +62 -0
- package/src/monitor-ws.ts +132 -0
- package/src/monitoring.ts +170 -0
- package/src/notify.ts +140 -0
- package/src/output.ts +132 -0
- package/src/registry.ts +343 -0
- package/src/render.ts +56 -0
- package/src/spawn.ts +141 -0
- package/src/state.ts +30 -0
- package/src/tools/bash-bg.ts +116 -0
- package/src/tools/bash-params.ts +22 -0
- package/src/tools/bash.ts +347 -0
- package/src/tools/job-decide.ts +60 -0
- package/src/tools/jobs.ts +365 -0
- package/src/tools/monitor.ts +165 -0
- package/src/types.ts +148 -0
- package/src/ui.ts +311 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 patty.io
|
|
4
|
+
Copyright (c) 2026 Dustin Fox (fork modifications)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# pi-async-bash
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/pi-async-bash)
|
|
4
|
+
[](https://www.npmjs.com/package/pi-async-bash)
|
|
5
|
+
[](https://github.com/unrelentingfox/pi-async-bash/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/pi-async-bash)
|
|
7
|
+
[](https://github.com/unrelentingfox/pi-async-bash/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
`pi-async-bash` adds asynchronous Bash execution, task management, and event
|
|
10
|
+
watches to [Pi](https://github.com/badlogic/pi-mono).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
Install from npm:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pi install npm:pi-async-bash
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
During development, add the local checkout to Pi settings:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"packages": [
|
|
25
|
+
"/local/home/dustinfx/workplace/pi/pi-async-bash"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Agent tools
|
|
31
|
+
|
|
32
|
+
| Tool | Parameters | Behavior |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `bash` | `command`; optional `timeout`, `run_async`, `description` | Overrides Pi's Bash tool. Commands start in the foreground by default. `run_async: true` starts detached work immediately. A long foreground command continues asynchronously after `timeout`. |
|
|
35
|
+
| `bash_async` | `command`; optional `name`, `timeout`, `notify` | Starts a command asynchronously, shows the command in the call row, and returns its job ID and `/tmp/pi-bg/<jobId>.log` path. It sends one terminal notification unless `notify: false`. |
|
|
36
|
+
| `bash_async_list` | `action`: `list`, `output`, `kill`, `attach`, `search`, `cleanup`, or `stats`; action-specific `jobId`, `pattern`, `wait` | Lists, reads, follows, searches, stops, cleans up, and reports on async jobs and watches. |
|
|
37
|
+
| `bash_async_watch` | exactly one of `command` or `ws`; `description`; optional `persistent`, `timeout_ms` | Returns immediately and delivers each stdout line or WebSocket text frame as an event. It stops on source exit, timeout, event-rate protection, or `bash_async_list` `kill`. |
|
|
38
|
+
| `bash_async_decide` | `jobId`; `decision`: `keep`, `kill`, or `check` | Resolves a `bash_async` command that has exceeded its optional timeout. |
|
|
39
|
+
|
|
40
|
+
`run_in_background` is not supported. Use `run_async` instead.
|
|
41
|
+
|
|
42
|
+
## Slash commands
|
|
43
|
+
|
|
44
|
+
| Command | Behavior |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `/bash-async` | Send the currently running bash command to the background. Return control to the agent. |
|
|
47
|
+
| `/bash-async-list` | Open the interactive task manager. `j`/`k` and arrow keys move the highlight, `Enter` opens the highlighted task's output, `Ctrl+x` starts kill confirmation, and `Escape` closes the root list, returns from output, or cancels confirmation. |
|
|
48
|
+
|
|
49
|
+
There are no keyboard shortcuts. While a foreground command runs, the editor
|
|
50
|
+
shows `/bash-async to run asynchronously`. Submitting a new user message also
|
|
51
|
+
hands active foreground Bash work to async execution and queues the message as
|
|
52
|
+
a follow-up.
|
|
53
|
+
|
|
54
|
+
## Behavior and safety
|
|
55
|
+
|
|
56
|
+
- Async commands use a detached process group and capture combined output in
|
|
57
|
+
`/tmp/pi-bg`.
|
|
58
|
+
- `bash_async_list attach` streams output while it waits. Use `output` for a
|
|
59
|
+
bounded non-blocking read.
|
|
60
|
+
- `bash_async_watch` is for many events. Use `bash` with `run_async: true` or
|
|
61
|
+
`bash_async` when only the terminal result matters.
|
|
62
|
+
- The extension rejects naïve long `sleep` commands, warns about interactive
|
|
63
|
+
prompts, caps non-persistent log growth at 100 MiB, limits concurrent jobs,
|
|
64
|
+
and removes stale log files at startup.
|
|
65
|
+
- Same-process `/reload` restores compatible running job records. A fresh Pi
|
|
66
|
+
process never adopts or signals an old process ID, preventing PID reuse bugs.
|
|
67
|
+
|
|
68
|
+
## Migration from upstream
|
|
69
|
+
|
|
70
|
+
| Upstream | pi-async-bash |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| `bash` with `run_in_background` | `bash` with `run_async` |
|
|
73
|
+
| `bash_bg` | `bash_async` |
|
|
74
|
+
| `jobs` | `bash_async_list` |
|
|
75
|
+
| `monitor` | `bash_async_watch` |
|
|
76
|
+
| `job_decide` | `bash_async_decide` |
|
|
77
|
+
| `/bg` | `/bash-async` |
|
|
78
|
+
| `/bg-list` | `/bash-async-list` |
|
|
79
|
+
| `/bg-version` | removed |
|
|
80
|
+
| `agent_bg` | removed; use Pi's own subagent tooling |
|
|
81
|
+
| keyboard shortcuts | removed |
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pnpm install --frozen-lockfile
|
|
87
|
+
pnpm run check
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## License and attribution
|
|
91
|
+
|
|
92
|
+
Copyright (c) 2026 patty.io
|
|
93
|
+
Copyright (c) 2026 Dustin Fox (fork modifications)
|
|
94
|
+
|
|
95
|
+
Licensed under the MIT License. See [LICENSE](LICENSE).
|
|
96
|
+
|
|
97
|
+
## Fork and attribution
|
|
98
|
+
|
|
99
|
+
`pi-async-bash` is based on Patrick Rho's
|
|
100
|
+
[`patty-io/pi-patty-bg-tasks`](https://github.com/patty-io/pi-patty-bg-tasks).
|
|
101
|
+
It retains the upstream Git history and MIT notice. This project removes
|
|
102
|
+
background Pi agent spawning and global keyboard shortcuts, and renames the
|
|
103
|
+
public interface to use `async` consistently. The unscoped name
|
|
104
|
+
`pi-background-bash` was already published, so this project uses
|
|
105
|
+
`pi-async-bash`.
|
package/RELEASING.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
## Automated releases
|
|
4
|
+
|
|
5
|
+
Every push to `main` runs Release Please. It opens or updates a release pull
|
|
6
|
+
request from [Conventional Commits](https://github.com/googleapis/release-please#how-should-i-write-my-commits).
|
|
7
|
+
Use `feat` for a minor release, `fix` for a patch release, `deps` for a
|
|
8
|
+
dependency release, and `!` in the pull request title for a major release.
|
|
9
|
+
Merging the release pull request creates the matching GitHub release. The
|
|
10
|
+
workflow publishes that version when it is missing from npm, so rerunning a
|
|
11
|
+
failed publish is safe.
|
|
12
|
+
|
|
13
|
+
Repository rules for `main` must require `Validate Conventional Commit title`.
|
|
14
|
+
Enable this requirement after the title workflow first lands on `main`.
|
|
15
|
+
|
|
16
|
+
The `npm` GitHub environment must exist, and npm trusted publishing must
|
|
17
|
+
authorize:
|
|
18
|
+
|
|
19
|
+
- Repository: `unrelentingfox/pi-async-bash`
|
|
20
|
+
- Workflow: `release.yml`
|
|
21
|
+
- Environment: `npm`
|
|
22
|
+
|
|
23
|
+
Confirm each release in the GitHub Actions Release run and on npm.
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./src/index.ts";
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-async-bash",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Asynchronous Bash tools for Pi: foreground Bash handoff, async jobs, event watches, and task management.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"async",
|
|
10
|
+
"bash",
|
|
11
|
+
"background-jobs",
|
|
12
|
+
"monitoring"
|
|
13
|
+
],
|
|
14
|
+
"author": "Dustin Fox",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/unrelentingfox/pi-async-bash.git"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src/",
|
|
22
|
+
"!src/__tests__/**",
|
|
23
|
+
"index.ts",
|
|
24
|
+
"README.md",
|
|
25
|
+
"RELEASING.md",
|
|
26
|
+
"CHANGELOG.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"pi": {
|
|
30
|
+
"extensions": [
|
|
31
|
+
"./index.ts"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"provenance": true
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "node --experimental-strip-types --test --test-force-exit 'src/__tests__/*.test.ts' 'src/__tests__/*.test.mjs'",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"check:pack": "node scripts/check-pack.mjs",
|
|
42
|
+
"check": "pnpm test && pnpm run typecheck && pnpm run check:pack"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@earendil-works/pi-agent-core": ">=0.79.0",
|
|
46
|
+
"@earendil-works/pi-ai": ">=0.79.0",
|
|
47
|
+
"@earendil-works/pi-coding-agent": ">=0.79.0",
|
|
48
|
+
"@earendil-works/pi-tui": ">=0.79.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@earendil-works/pi-agent-core": "^0.83.0",
|
|
52
|
+
"@earendil-works/pi-ai": "^0.85.1",
|
|
53
|
+
"@earendil-works/pi-coding-agent": "^0.83.0",
|
|
54
|
+
"@earendil-works/pi-tui": "^0.83.0",
|
|
55
|
+
"@types/node": "25.6.2",
|
|
56
|
+
"typebox": "^1.3.30",
|
|
57
|
+
"typescript": "6.0.3"
|
|
58
|
+
},
|
|
59
|
+
"packageManager": "pnpm@10.33.1",
|
|
60
|
+
"bugs": {
|
|
61
|
+
"url": "https://github.com/unrelentingfox/pi-async-bash/issues"
|
|
62
|
+
},
|
|
63
|
+
"homepage": "https://github.com/unrelentingfox/pi-async-bash#readme",
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=22.19.0"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Async Bash slash commands. */
|
|
2
|
+
|
|
3
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { BackgroundRegistry } from "./state.ts";
|
|
5
|
+
import { takeControl, type ControlContext } from "./lifecycle.ts";
|
|
6
|
+
import { openBgListPanel } from "./ui.ts";
|
|
7
|
+
|
|
8
|
+
/** Register async handoff and task-list commands. */
|
|
9
|
+
export function registerCommands(pi: ExtensionAPI, reg: BackgroundRegistry): void {
|
|
10
|
+
pi.registerCommand("bash-async", {
|
|
11
|
+
description: "Run the active Bash command asynchronously and return control to the agent",
|
|
12
|
+
handler: async (_args, ctx) => {
|
|
13
|
+
takeControl(reg, ctx as unknown as ControlContext);
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
pi.registerCommand("bash-async-list", {
|
|
18
|
+
description: "Open the interactive asynchronous Bash task manager",
|
|
19
|
+
handler: async (_args, ctx: ExtensionCommandContext) => {
|
|
20
|
+
await openBgListPanel(reg, ctx);
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
package/src/format.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting helpers — duration strings, status pills, output tails.
|
|
3
|
+
* The <task-notification> builder lives in src/notify.ts; this module is
|
|
4
|
+
* the primitives layer that the rest of the codebase shares.
|
|
5
|
+
*/
|
|
6
|
+
import type { Job } from "./types.ts";
|
|
7
|
+
import { PREVIEW_CHARS } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
/** The standard short label for a job: its name, or its id when unnamed. */
|
|
10
|
+
export function jobLabel(job: Job): string {
|
|
11
|
+
return job.name ?? job.id;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Collapse all whitespace runs to single spaces — one-line rendering. */
|
|
15
|
+
export function oneLine(s: string): string {
|
|
16
|
+
return s.replace(/\s+/g, " ").trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Human/agent prose label: the name when set, else the command collapsed to
|
|
20
|
+
* one line and truncated. (jobLabel = name ?? id for registry lookups;
|
|
21
|
+
* describeJob = name ?? one-line command for prose.) */
|
|
22
|
+
export function describeJob(name: string | undefined, command: string): string {
|
|
23
|
+
if (name) return name;
|
|
24
|
+
const line = oneLine(command);
|
|
25
|
+
return line.length > 80 ? `${line.slice(0, 80)}…` : line;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** "1m23s" / "45s" / "0s" — short human-readable duration. */
|
|
29
|
+
export function formatDuration(ms: number): string {
|
|
30
|
+
const totalSecs = Math.floor(ms / 1000);
|
|
31
|
+
const mins = Math.floor(totalSecs / 60);
|
|
32
|
+
const secs = totalSecs % 60;
|
|
33
|
+
return mins > 0 ? `${mins}m${secs}s` : `${secs}s`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Human-readable status pill, including duration for running jobs. */
|
|
37
|
+
export function statusLabel(job: Job, duration?: string): string {
|
|
38
|
+
const dur = duration ?? formatDuration(Date.now() - job.startTime);
|
|
39
|
+
switch (job.status) {
|
|
40
|
+
case "pending":
|
|
41
|
+
return "◌ pending";
|
|
42
|
+
case "running":
|
|
43
|
+
if (job.kind === "monitor") return `◉ monitor (${dur})`;
|
|
44
|
+
return job.isBackgrounded ? `▶ bg (${dur})` : `▶ fg (${dur})`;
|
|
45
|
+
case "completed":
|
|
46
|
+
return "✓ completed";
|
|
47
|
+
case "failed":
|
|
48
|
+
return "✗ failed";
|
|
49
|
+
case "killed":
|
|
50
|
+
return "✗ killed";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** "b7f3k9a2x1 [shell]: ls -la (last 80 chars)" — single line for `bash_async_list list`.
|
|
55
|
+
* Every line carries the kind tag (shell/agent/monitor) so the unified list
|
|
56
|
+
* shows what each task is. */
|
|
57
|
+
export function formatJobLine(job: Job): string {
|
|
58
|
+
const head = job.name ? `${job.name} (${job.id})` : job.id;
|
|
59
|
+
const kind = job.kind ?? "shell";
|
|
60
|
+
const duration =
|
|
61
|
+
job.status === "running"
|
|
62
|
+
? ` (${formatDuration(Date.now() - job.startTime)})`
|
|
63
|
+
: "";
|
|
64
|
+
return `${head} [${kind}]: ${job.command.slice(0, PREVIEW_CHARS.line)} - ${statusLabel(job)}${duration}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Truncate a tail with a consistent "showing last N chars" marker. */
|
|
68
|
+
export function truncateTail(content: string, maxChars: number): string {
|
|
69
|
+
if (content.length <= maxChars) return content;
|
|
70
|
+
return `...[truncated, showing last ${maxChars} chars]\n${content.slice(-maxChars)}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Text content block builder — shared across all tools. The explicit return
|
|
74
|
+
* type documents the shared contract for the five tool call sites that
|
|
75
|
+
* import this — they rely on the literal shape, not inference. */
|
|
76
|
+
export function textBlock(s: string): { type: "text"; text: string } {
|
|
77
|
+
return { type: "text" as const, text: s };
|
|
78
|
+
}
|
package/src/hint.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live "(/bash-async to run asynchronously)" hint shown below the editor while
|
|
3
|
+
* a foreground bash command is running — mirrors Claude Code's BackgroundHint,
|
|
4
|
+
* which appears once a command has run past the quick-completion window.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { UiContext } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
const HINT_KEY = "bg-hint";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Ref-count of foreground commands currently showing the hint. The widget is a
|
|
13
|
+
* single shared key, but bash commands run in parallel — so we only render it on
|
|
14
|
+
* the 0→1 transition and clear it on the last 1→0, keeping the hint up as long
|
|
15
|
+
* as any foreground command is still running. Each caller must pair exactly one
|
|
16
|
+
* showBackgroundHint() with one clearBackgroundHint().
|
|
17
|
+
*/
|
|
18
|
+
let activeHints = 0;
|
|
19
|
+
|
|
20
|
+
/** Show the async handoff hint below the editor (idempotent across parallel commands). */
|
|
21
|
+
export function showBackgroundHint(ctx: UiContext): void {
|
|
22
|
+
activeHints++;
|
|
23
|
+
if (activeHints === 1) {
|
|
24
|
+
ctx.ui.setWidget(HINT_KEY, ["(/bash-async to run asynchronously)"], {
|
|
25
|
+
placement: "belowEditor",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Release one hint; clears the widget only when the last command is done. */
|
|
31
|
+
export function clearBackgroundHint(ctx: UiContext): void {
|
|
32
|
+
if (activeHints === 0) return;
|
|
33
|
+
activeHints--;
|
|
34
|
+
if (activeHints === 0) {
|
|
35
|
+
ctx.ui.setWidget(HINT_KEY, undefined);
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-async-bash — asynchronous Bash execution for the Pi agent.
|
|
3
|
+
*
|
|
4
|
+
* Registers `bash`, `bash_async`, `bash_async_list`, `bash_async_watch`, and
|
|
5
|
+
* `bash_async_decide`, plus `/bash-async` and `/bash-async-list`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { createBashToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { BackgroundRegistry } from "./state.ts";
|
|
11
|
+
import { cleanupStaleRuntimeArtifacts, detectNonInteractive, reviveAndValidate, serializeJobs, terminateJobSilently } from "./lifecycle.ts";
|
|
12
|
+
import { stopSidebarTicker } from "./registry.ts";
|
|
13
|
+
import { EVENT } from "./types.ts";
|
|
14
|
+
import { registerBashTool } from "./tools/bash.ts";
|
|
15
|
+
import { registerBashBgTool } from "./tools/bash-bg.ts";
|
|
16
|
+
import { registerJobsTool } from "./tools/jobs.ts";
|
|
17
|
+
import { registerMonitorTool } from "./tools/monitor.ts";
|
|
18
|
+
import { registerCommands } from "./commands.ts";
|
|
19
|
+
import { registerJobDecideTool } from "./tools/job-decide.ts";
|
|
20
|
+
import { registerInputHandlers } from "./input.ts";
|
|
21
|
+
|
|
22
|
+
/** Extension entry point. */
|
|
23
|
+
export default function (pi: ExtensionAPI): void {
|
|
24
|
+
const reg = new BackgroundRegistry();
|
|
25
|
+
|
|
26
|
+
// ── Tool registration ─────────────────────────────────────────
|
|
27
|
+
// Use the unwrapped tool *definition* so the override inherits Pi's native
|
|
28
|
+
// bash renderCall/renderResult (createBashTool returns a wrapped AgentTool
|
|
29
|
+
// that drops them).
|
|
30
|
+
const originalBash = createBashToolDefinition(process.cwd());
|
|
31
|
+
registerBashTool(pi, reg, originalBash);
|
|
32
|
+
registerBashBgTool(pi, reg);
|
|
33
|
+
registerJobsTool(pi, reg);
|
|
34
|
+
registerMonitorTool(pi, reg);
|
|
35
|
+
registerJobDecideTool(pi, reg);
|
|
36
|
+
|
|
37
|
+
// ── Commands and cooperative steering ─────────────────────────
|
|
38
|
+
registerCommands(pi, reg);
|
|
39
|
+
registerInputHandlers(pi, reg);
|
|
40
|
+
|
|
41
|
+
// ── Message rendering ─────────────────────────────────────────
|
|
42
|
+
// <task-notification> messages render as one colored line: green for
|
|
43
|
+
// completed, red for failed, yellow for killed and for the statusless
|
|
44
|
+
// stall warning (CC's unread/attention color).
|
|
45
|
+
const renderTaskNotification = (
|
|
46
|
+
message: { content: unknown; details?: unknown },
|
|
47
|
+
theme: { fg(colour: string, text: string): string }
|
|
48
|
+
) => {
|
|
49
|
+
const details = message.details as
|
|
50
|
+
| { status?: string; summary?: string }
|
|
51
|
+
| undefined;
|
|
52
|
+
const colour =
|
|
53
|
+
details?.status === "completed"
|
|
54
|
+
? "success"
|
|
55
|
+
: details?.status === "failed"
|
|
56
|
+
? "error"
|
|
57
|
+
: "warning";
|
|
58
|
+
const line = theme.fg(colour, `● ${details?.summary ?? String(message.content)}`);
|
|
59
|
+
return { render: () => [line], invalidate: () => {} };
|
|
60
|
+
};
|
|
61
|
+
pi.registerMessageRenderer(EVENT.taskNotification, (message, _options, theme) =>
|
|
62
|
+
renderTaskNotification(message, theme)
|
|
63
|
+
);
|
|
64
|
+
pi.registerMessageRenderer(EVENT.stall, (message, _options, theme) =>
|
|
65
|
+
renderTaskNotification(message, theme)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Restore only jobs that belong to this Pi process. This preserves async
|
|
69
|
+
// work through `/reload` but refuses to signal reused PIDs after restart.
|
|
70
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
71
|
+
reg.nonInteractive = detectNonInteractive(process.argv, Boolean(process.stdin.isTTY));
|
|
72
|
+
const entries = ctx.sessionManager.getEntries();
|
|
73
|
+
const state = [...entries].reverse().find((entry) =>
|
|
74
|
+
entry.type === "custom" && entry.customType === "pi-async-bash-state"
|
|
75
|
+
) as { type: "custom"; data?: { jobs?: Array<import("./types.ts").Job> } } | undefined;
|
|
76
|
+
for (const job of state?.data?.jobs ?? []) {
|
|
77
|
+
if (reviveAndValidate(job) === "alive") reg.jobs.set(job.id, job);
|
|
78
|
+
}
|
|
79
|
+
void cleanupStaleRuntimeArtifacts();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
pi.on("session_shutdown", async (event, _ctx) => {
|
|
83
|
+
stopSidebarTicker(reg);
|
|
84
|
+
if (event.reason === "quit") {
|
|
85
|
+
for (const job of reg.jobs.values()) {
|
|
86
|
+
if (job.status === "running") terminateJobSilently(reg, job);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
pi.appendEntry("pi-async-bash-state", { jobs: serializeJobs(reg.jobs.values()) });
|
|
90
|
+
});
|
|
91
|
+
}
|
package/src/input.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/input.ts
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { BackgroundRegistry } from "./state.ts";
|
|
4
|
+
import type { UiContext } from "./types.ts";
|
|
5
|
+
import { backgroundActiveForeground } from "./lifecycle.ts";
|
|
6
|
+
|
|
7
|
+
export function registerInputHandlers(pi: ExtensionAPI, reg: BackgroundRegistry): void {
|
|
8
|
+
pi.on("input", async (event, ctx) => {
|
|
9
|
+
// Cooperative steering (Claude Code parity): ANY input typed while a
|
|
10
|
+
// foreground bash command is running backgrounds that command and
|
|
11
|
+
// re-delivers the input as the next turn — regardless of the message's
|
|
12
|
+
// steer/followUp streamingBehavior. We only intercept when a foreground
|
|
13
|
+
// slot is active; everything else falls through to Pi.
|
|
14
|
+
if (reg.foreground.size === 0) return { action: "continue" };
|
|
15
|
+
// Don't intercept extension-sourced messages.
|
|
16
|
+
if (event.source === "extension") return { action: "continue" };
|
|
17
|
+
|
|
18
|
+
const text = event.text;
|
|
19
|
+
const bg = backgroundActiveForeground(reg, ctx as UiContext);
|
|
20
|
+
if (!bg) return { action: "continue" };
|
|
21
|
+
|
|
22
|
+
// Abort the current turn so the bash tool returns the "backgrounded" result.
|
|
23
|
+
ctx.abort?.();
|
|
24
|
+
|
|
25
|
+
// Resubmit the user's message as a follow-up — Pi delivers it
|
|
26
|
+
// after the current turn settles. No polling needed.
|
|
27
|
+
try {
|
|
28
|
+
pi.sendUserMessage(text, { deliverAs: "followUp" });
|
|
29
|
+
} catch {
|
|
30
|
+
// Session ended between abort and resubmit — nothing to deliver to.
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { action: "handled" };
|
|
34
|
+
});
|
|
35
|
+
}
|