@monotykamary/pi-localterm 0.3.1 → 0.3.2
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 +2 -2
- package/extensions/agent-notify.ts +70 -25
- package/extensions/index.ts +2 -2
- package/package.json +3 -3
- package/src/constants.ts +4 -0
- package/src/utils/retry-event-id.ts +4 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A [pi](https://github.com/earendil-works/pi-coding-agent) extension that integra
|
|
|
4
4
|
|
|
5
5
|
1. **Kitty graphics + OSC 8 links** — localterm renders xterm.js with the Kitty graphics and web-links addons loaded, but sets `TERM=xterm-256color` and strips terminal-identity env vars (so Ink TUIs don't probe for a protocol xterm.js lacks). pi-tui therefore reports images/hyperlinks as unsupported. This extension detects `LOCALTERM=1` (injected into every localterm PTY) and force-enables those capabilities, so images and links render in the browser.
|
|
6
6
|
2. **Secret scrubbing for the agent's bash tool** — localterm injects a secret only into the shimmed process's env (pi's), not its parent shell. But pi's bash tool spawns commands with `{ ...process.env }`, so without this the agent's commands would inherit every secret pi received. This extension overrides the `bash` tool with a spawn hook that deletes the `pi` process's localterm-managed secret env vars from each command's child env only — pi's own `process.env` (and its provider calls) keep them.
|
|
7
|
-
3. **Desktop notifications on agent completion** — pi's only notification primitive is `ctx.ui.notify`, an in-TUI banner invisible once you switch away from the pi tab. localterm already has an OSC 9 (`ESC ] 9 ; MESSAGE BEL`) → browser desktop-notification pipeline (opt-in via "Desktop alerts" in Settings). The extension writes an OSC 9 on `
|
|
7
|
+
3. **Desktop notifications on agent completion** — pi's only notification primitive is `ctx.ui.notify`, an in-TUI banner invisible once you switch away from the pi tab. localterm already has an OSC 9 (`ESC ] 9 ; MESSAGE BEL`) → browser desktop-notification pipeline (opt-in via "Desktop alerts" in Settings). The extension writes an OSC 9 on `agent_settled`, reusing that pipeline so a user who stepped away gets an OS notification only after the agent has no automatic retry, compaction, or queued continuation left. It also coordinates with `@monotykamary/pi-retry` through Pi's shared extension event bus so that extension's delayed hidden retries do not produce intermediate notifications. Threshold-gated (turns ≥ 30 s) so quick back-and-forth doesn't spam a focused user; TUI-mode-guarded so `json`/`rpc`/`-p` stdout isn't polluted with OSC bytes. Note: emitting OSC 9 also fires localterm's `notification` automation trigger, so a `notification`-event automation will fire on agent completion.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -64,7 +64,7 @@ The scrub overrides pi's built-in `bash` tool **by name** (extensions apply afte
|
|
|
64
64
|
|
|
65
65
|
## Requirements
|
|
66
66
|
|
|
67
|
-
- pi ≥ 0.80 (uses the `spawnHook` tool option and `createBashToolDefinition` export).
|
|
67
|
+
- pi ≥ 0.80.7 (uses `agent_settled`, the shared extension event bus, the `spawnHook` tool option, and the `createBashToolDefinition` export).
|
|
68
68
|
- localterm with `LOCALTERM=1` in the PTY environment (v0.7+). The scrub additionally needs localterm's secrets/processes policy files on the same machine.
|
|
69
69
|
|
|
70
70
|
## License
|
|
@@ -1,40 +1,85 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
AgentEndEvent,
|
|
3
|
+
ExtensionAPI,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import {
|
|
7
|
+
AGENT_NOTIFY_MIN_ELAPSED_MS,
|
|
8
|
+
PI_RETRY_CANCELLED_EVENT,
|
|
9
|
+
PI_RETRY_COMPLETED_EVENT,
|
|
10
|
+
PI_RETRY_STARTED_EVENT,
|
|
11
|
+
} from "../src/constants.js";
|
|
3
12
|
import { extractAssistantExcerpt, formatAgentEndBody } from "../src/utils/agent-notify-body.js";
|
|
13
|
+
import { retryEventId } from "../src/utils/retry-event-id.js";
|
|
4
14
|
import { buildOsc9Sequence } from "../src/utils/osc-sequence.js";
|
|
5
15
|
|
|
6
|
-
// pi's only notification primitive is ctx.ui.notify — an in-TUI banner that's
|
|
7
|
-
// invisible the moment you switch away from the pi tab. localterm already has
|
|
8
|
-
// a desktop-notification pipeline: anything writing an OSC 9 sequence
|
|
9
|
-
// (ESC ] 9 ; MESSAGE BEL) to the PTY is parsed by the localterm daemon and
|
|
10
|
-
// broadcast to every attached web client, which shows an OS notification when
|
|
11
|
-
// the user enabled "Desktop alerts". pi runs inside that PTY, so this bridges
|
|
12
|
-
// the two by writing an OSC 9 on agent_end.
|
|
13
|
-
//
|
|
14
|
-
// Threshold-gated (AGENT_NOTIFY_MIN_ELAPSED_MS) so quick back-and-forth turns
|
|
15
|
-
// don't spam a user watching the pi tab — only turns long enough that the user
|
|
16
|
-
// likely stepped away ping. Not error-gated: pi's bash tool flags every
|
|
17
|
-
// non-zero exit as an error, so an "always notify on error" rule would fire
|
|
18
|
-
// on every failed test/build and defeat the threshold. Guarded to TUI mode
|
|
19
|
-
// because in json/rpc/print mode pi's stdout is the protocol/answer stream,
|
|
20
|
-
// and an OSC 9 injected there is binary garbage (and a pointless desktop ping
|
|
21
|
-
// for a non-human consumer).
|
|
22
16
|
export const registerAgentNotify = (pi: ExtensionAPI): void => {
|
|
17
|
+
let activeRetryId: number | undefined;
|
|
18
|
+
let latestMessages: AgentEndEvent["messages"] = [];
|
|
19
|
+
let settledContext: ExtensionContext | undefined;
|
|
23
20
|
let turnStartedAt: number | undefined;
|
|
21
|
+
let hasCurrentRunSettled = false;
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const resetNotificationState = (): void => {
|
|
24
|
+
latestMessages = [];
|
|
25
|
+
settledContext = undefined;
|
|
26
|
+
turnStartedAt = undefined;
|
|
27
|
+
hasCurrentRunSettled = false;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const emitNotification = (): void => {
|
|
31
|
+
if (!hasCurrentRunSettled || settledContext === undefined || turnStartedAt === undefined)
|
|
32
|
+
return;
|
|
28
33
|
|
|
29
|
-
pi.on("agent_end", (event, ctx) => {
|
|
30
|
-
if (ctx.mode !== "tui" || turnStartedAt === undefined) return;
|
|
31
34
|
const elapsedMs = Date.now() - turnStartedAt;
|
|
32
|
-
|
|
35
|
+
const context = settledContext;
|
|
36
|
+
const messages = latestMessages;
|
|
37
|
+
resetNotificationState();
|
|
38
|
+
|
|
39
|
+
if (context.mode !== "tui" || elapsedMs < AGENT_NOTIFY_MIN_ELAPSED_MS) return;
|
|
40
|
+
|
|
33
41
|
const body = formatAgentEndBody(
|
|
34
42
|
elapsedMs,
|
|
35
43
|
pi.getSessionName(),
|
|
36
|
-
extractAssistantExcerpt(
|
|
44
|
+
extractAssistantExcerpt(messages),
|
|
37
45
|
);
|
|
38
46
|
process.stdout.write(buildOsc9Sequence(body));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const unsubscribeRetryStarted = pi.events.on(PI_RETRY_STARTED_EVENT, (event) => {
|
|
50
|
+
activeRetryId = retryEventId(event);
|
|
51
|
+
});
|
|
52
|
+
const unsubscribeRetryCompleted = pi.events.on(PI_RETRY_COMPLETED_EVENT, (event) => {
|
|
53
|
+
if (retryEventId(event) !== activeRetryId) return;
|
|
54
|
+
activeRetryId = undefined;
|
|
55
|
+
emitNotification();
|
|
56
|
+
});
|
|
57
|
+
const unsubscribeRetryCancelled = pi.events.on(PI_RETRY_CANCELLED_EVENT, (event) => {
|
|
58
|
+
if (retryEventId(event) !== activeRetryId) return;
|
|
59
|
+
activeRetryId = undefined;
|
|
60
|
+
resetNotificationState();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
pi.on("agent_start", () => {
|
|
64
|
+
turnStartedAt ??= Date.now();
|
|
65
|
+
hasCurrentRunSettled = false;
|
|
66
|
+
settledContext = undefined;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
pi.on("agent_end", (event) => {
|
|
70
|
+
latestMessages = event.messages;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
pi.on("agent_settled", (_event, context) => {
|
|
74
|
+
hasCurrentRunSettled = true;
|
|
75
|
+
settledContext = context;
|
|
76
|
+
if (activeRetryId === undefined) emitNotification();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
pi.on("session_shutdown", () => {
|
|
80
|
+
unsubscribeRetryStarted();
|
|
81
|
+
unsubscribeRetryCompleted();
|
|
82
|
+
unsubscribeRetryCancelled();
|
|
83
|
+
resetNotificationState();
|
|
39
84
|
});
|
|
40
85
|
};
|
package/extensions/index.ts
CHANGED
|
@@ -10,9 +10,9 @@ import { enableKittyImages } from "./kitty-images.js";
|
|
|
10
10
|
// detect, (2) scrubs localterm-managed secret env vars from the agent's
|
|
11
11
|
// bash-tool children so a generated command can't read keys the shim injected
|
|
12
12
|
// into pi's own env, and (3) writes an OSC 9 desktop notification on
|
|
13
|
-
//
|
|
13
|
+
// agent_settled (reusing localterm's existing OSC 9 -> browser notification
|
|
14
14
|
// pipeline) so a user who stepped away from the pi tab learns the agent
|
|
15
|
-
// finished.
|
|
15
|
+
// finished, including after retries and queued continuations.
|
|
16
16
|
export default (pi: ExtensionAPI): void => {
|
|
17
17
|
if (process.env.LOCALTERM !== "1") return;
|
|
18
18
|
enableKittyImages();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monotykamary/pi-localterm",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "localterm <-> pi integration: Kitty graphics for the browser renderer, scrubbing localterm-managed secret env vars from the agent's bash-tool children, and OSC 9 desktop notifications when the agent finishes a long turn.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"localterm",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@earendil-works/pi-coding-agent": "^0.80.
|
|
33
|
-
"@earendil-works/pi-tui": "^0.80.
|
|
32
|
+
"@earendil-works/pi-coding-agent": "^0.80.10",
|
|
33
|
+
"@earendil-works/pi-tui": "^0.80.10",
|
|
34
34
|
"@types/node": "^26.1.1",
|
|
35
35
|
"typescript": "^7.0.2",
|
|
36
36
|
"vite-plus": "^0.2.4"
|
package/src/constants.ts
CHANGED
|
@@ -35,6 +35,10 @@ export const NOTIFICATION_MAX_LENGTH = 1024;
|
|
|
35
35
|
// tab. Tunable: lower to notify sooner, raise to stay quieter while focused.
|
|
36
36
|
export const AGENT_NOTIFY_MIN_ELAPSED_MS = 30_000;
|
|
37
37
|
|
|
38
|
+
export const PI_RETRY_STARTED_EVENT = "pi-retry:started";
|
|
39
|
+
export const PI_RETRY_COMPLETED_EVENT = "pi-retry:completed";
|
|
40
|
+
export const PI_RETRY_CANCELLED_EVENT = "pi-retry:cancelled";
|
|
41
|
+
|
|
38
42
|
// Cap on the assistant-response excerpt carried by the "pi finished"
|
|
39
43
|
// notification body, counted in UTF-16 code units after whitespace is
|
|
40
44
|
// collapsed. Long enough to surface a sentence or two of the agent's final
|