@juicesharp/rpiv-warp 1.4.2 → 1.5.1
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 +11 -0
- package/config.ts +10 -0
- package/index.ts +100 -3
- package/package.json +1 -1
- package/payload.ts +19 -22
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to `@juicesharp/rpiv-warp` are documented here.
|
|
|
5
5
|
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.5.1] - 2026-05-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Idle prompt emission after agent turns complete, carrying the assistant summary.
|
|
12
|
+
- Configurable heartbeat re-emitting `prompt_submit` during active work (default 15 s, set `heartbeatMs` in config to override, `0` to disable).
|
|
13
|
+
- `session_shutdown` handler clearing all timers and spinner state.
|
|
14
|
+
- Blocking tool input capture attached to `tool_complete` payloads.
|
|
15
|
+
- `before_agent_start` handler capturing the user's query for accurate `prompt_submit` events.
|
|
16
|
+
|
|
17
|
+
## [1.5.0] - 2026-05-12
|
|
18
|
+
|
|
8
19
|
## [1.4.2] - 2026-05-11
|
|
9
20
|
|
|
10
21
|
## [1.4.1] - 2026-05-11
|
package/config.ts
CHANGED
|
@@ -7,9 +7,11 @@ export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
|
7
7
|
|
|
8
8
|
export interface RpivWarpConfig {
|
|
9
9
|
readonly blockingTools?: readonly string[];
|
|
10
|
+
readonly heartbeatMs?: number;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export const DEFAULT_BLOCKING_TOOLS: readonly string[] = ["ask_user_question"];
|
|
14
|
+
export const DEFAULT_HEARTBEAT_MS = 15000;
|
|
13
15
|
|
|
14
16
|
export function loadConfig(): RpivWarpConfig {
|
|
15
17
|
if (!existsSync(CONFIG_PATH)) return {};
|
|
@@ -22,6 +24,14 @@ export function loadConfig(): RpivWarpConfig {
|
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
export function getHeartbeatMs(): number {
|
|
28
|
+
const config = loadConfig();
|
|
29
|
+
const ms = config.heartbeatMs;
|
|
30
|
+
if (ms === 0) return 0; // explicitly disabled
|
|
31
|
+
if (typeof ms !== "number" || ms <= 0) return DEFAULT_HEARTBEAT_MS;
|
|
32
|
+
return ms;
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
export function getBlockingTools(): ReadonlySet<string> {
|
|
26
36
|
const config = loadConfig();
|
|
27
37
|
const list = Array.isArray(config.blockingTools) ? config.blockingTools : DEFAULT_BLOCKING_TOOLS;
|
package/index.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { getBlockingTools } from "./config.js";
|
|
3
|
+
import { DEFAULT_HEARTBEAT_MS, getBlockingTools, getHeartbeatMs } from "./config.js";
|
|
4
4
|
import {
|
|
5
|
+
buildIdlePromptPayload,
|
|
5
6
|
buildPromptSubmitPayload,
|
|
6
7
|
buildQuestionAskedPayload,
|
|
7
8
|
buildSessionStartPayload,
|
|
8
9
|
buildStopPayload,
|
|
9
10
|
buildToolCompletePayload,
|
|
11
|
+
lastAssistantText,
|
|
10
12
|
serializePayload,
|
|
11
13
|
type WarpPayload,
|
|
12
14
|
} from "./payload.js";
|
|
@@ -14,12 +16,85 @@ import { detectWarpEnvironment } from "./protocol.js";
|
|
|
14
16
|
import { startSpinner, stopSpinner } from "./title-spinner.js";
|
|
15
17
|
import { writeOSC777 } from "./warp-notify.js";
|
|
16
18
|
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Module-level timer state — __resetState() clears for test isolation
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
let pendingQuery = "";
|
|
24
|
+
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
25
|
+
let heartbeatInterval: ReturnType<typeof setInterval> | undefined;
|
|
26
|
+
let heartbeatMs = DEFAULT_HEARTBEAT_MS;
|
|
27
|
+
const toolInputCapture = new Map<string, Record<string, unknown>>();
|
|
28
|
+
|
|
29
|
+
export function __resetState(): void {
|
|
30
|
+
pendingQuery = "";
|
|
31
|
+
if (idleTimer !== undefined) {
|
|
32
|
+
clearTimeout(idleTimer);
|
|
33
|
+
idleTimer = undefined;
|
|
34
|
+
}
|
|
35
|
+
if (heartbeatInterval !== undefined) {
|
|
36
|
+
clearInterval(heartbeatInterval);
|
|
37
|
+
heartbeatInterval = undefined;
|
|
38
|
+
}
|
|
39
|
+
heartbeatMs = DEFAULT_HEARTBEAT_MS;
|
|
40
|
+
toolInputCapture.clear();
|
|
41
|
+
}
|
|
42
|
+
|
|
17
43
|
const TITLE = "warp://cli-agent";
|
|
18
44
|
|
|
19
45
|
function emit(payload: WarpPayload): void {
|
|
20
46
|
writeOSC777(TITLE, serializePayload(payload));
|
|
21
47
|
}
|
|
22
48
|
|
|
49
|
+
function cancelIdleTimer(): void {
|
|
50
|
+
if (idleTimer !== undefined) {
|
|
51
|
+
clearTimeout(idleTimer);
|
|
52
|
+
idleTimer = undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function startIdleTimer(ctx: ExtensionContext, branch: SessionEntry[]): void {
|
|
57
|
+
cancelIdleTimer();
|
|
58
|
+
idleTimer = setTimeout(() => {
|
|
59
|
+
idleTimer = undefined;
|
|
60
|
+
const summary = lastAssistantText(branch);
|
|
61
|
+
emit(buildIdlePromptPayload(ctx, summary));
|
|
62
|
+
}, 300);
|
|
63
|
+
if (typeof (idleTimer as ReturnType<typeof setTimeout>).unref === "function") {
|
|
64
|
+
(idleTimer as ReturnType<typeof setTimeout>).unref();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function startHeartbeat(ctx: ExtensionContext, ms: number): void {
|
|
69
|
+
stopHeartbeat();
|
|
70
|
+
if (ms <= 0) return; // disabled
|
|
71
|
+
heartbeatInterval = setInterval(() => {
|
|
72
|
+
emit(buildPromptSubmitPayload(ctx, pendingQuery));
|
|
73
|
+
}, ms);
|
|
74
|
+
if (typeof heartbeatInterval.unref === "function") {
|
|
75
|
+
heartbeatInterval.unref();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stopHeartbeat(): void {
|
|
80
|
+
if (heartbeatInterval !== undefined) {
|
|
81
|
+
clearInterval(heartbeatInterval);
|
|
82
|
+
heartbeatInterval = undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function captureToolInput(toolCallId: string, input: unknown): void {
|
|
87
|
+
if (typeof input === "object" && input !== null) {
|
|
88
|
+
toolInputCapture.set(toolCallId, input as Record<string, unknown>);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function consumeToolInput(toolCallId: string): Record<string, unknown> | undefined {
|
|
93
|
+
const input = toolInputCapture.get(toolCallId);
|
|
94
|
+
toolInputCapture.delete(toolCallId);
|
|
95
|
+
return input;
|
|
96
|
+
}
|
|
97
|
+
|
|
23
98
|
function readBranch(ctx: ExtensionContext): SessionEntry[] {
|
|
24
99
|
return ctx.sessionManager.getBranch() as SessionEntry[];
|
|
25
100
|
}
|
|
@@ -36,31 +111,53 @@ export default function (pi: ExtensionAPI): void {
|
|
|
36
111
|
if (!warp.isWarp || !warp.supportsStructured) return;
|
|
37
112
|
|
|
38
113
|
const blockingTools = getBlockingTools();
|
|
114
|
+
heartbeatMs = getHeartbeatMs();
|
|
39
115
|
|
|
40
116
|
pi.on("session_start", async (event, ctx) => {
|
|
41
117
|
if (event.reason !== "startup") return;
|
|
42
118
|
emit(buildSessionStartPayload(ctx));
|
|
43
119
|
});
|
|
44
120
|
|
|
121
|
+
pi.on("before_agent_start", async (event) => {
|
|
122
|
+
pendingQuery = event.prompt ?? "";
|
|
123
|
+
});
|
|
124
|
+
|
|
45
125
|
pi.on("agent_start", async (_event, ctx) => {
|
|
46
|
-
emit(
|
|
126
|
+
emit(buildSessionStartPayload(ctx)); // Item 2: defensive re-announce
|
|
127
|
+
emit(buildPromptSubmitPayload(ctx, pendingQuery));
|
|
47
128
|
startSpinner(titleSuffix(ctx));
|
|
129
|
+
cancelIdleTimer(); // Item 3: cancel pending idle from previous turn
|
|
130
|
+
startHeartbeat(ctx, heartbeatMs); // Item 4: heartbeat
|
|
48
131
|
});
|
|
49
132
|
|
|
50
133
|
pi.on("agent_end", async (_event, ctx) => {
|
|
51
134
|
emit(buildStopPayload(ctx, readBranch(ctx)));
|
|
52
135
|
stopSpinner();
|
|
136
|
+
stopHeartbeat(); // Item 4: stop heartbeat
|
|
137
|
+
startIdleTimer(ctx, readBranch(ctx)); // Item 3: schedule idle_prompt after 300ms
|
|
53
138
|
});
|
|
54
139
|
|
|
55
140
|
pi.on("tool_call", async (event, ctx) => {
|
|
56
141
|
if (!blockingTools.has(event.toolName)) return;
|
|
142
|
+
captureToolInput(event.toolCallId, event.input); // Item 6: capture input
|
|
57
143
|
emit(buildQuestionAskedPayload(ctx));
|
|
58
144
|
stopSpinner();
|
|
145
|
+
stopHeartbeat(); // Item 4: pause heartbeat
|
|
59
146
|
});
|
|
60
147
|
|
|
61
148
|
pi.on("tool_execution_end", async (event, ctx) => {
|
|
62
149
|
if (!blockingTools.has(event.toolName)) return;
|
|
63
|
-
|
|
150
|
+
const toolInput = consumeToolInput(event.toolCallId); // Item 6: consume input
|
|
151
|
+
emit(buildToolCompletePayload(ctx, event.toolName, toolInput));
|
|
64
152
|
startSpinner(titleSuffix(ctx));
|
|
153
|
+
startHeartbeat(ctx, heartbeatMs); // Item 4: resume heartbeat
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
pi.on("session_shutdown", async () => {
|
|
157
|
+
cancelIdleTimer();
|
|
158
|
+
stopHeartbeat();
|
|
159
|
+
pendingQuery = "";
|
|
160
|
+
toolInputCapture.clear();
|
|
161
|
+
stopSpinner();
|
|
65
162
|
});
|
|
66
163
|
}
|
package/package.json
CHANGED
package/payload.ts
CHANGED
|
@@ -16,24 +16,8 @@ import { negotiateProtocolVersion, type WarpEvent } from "./protocol.js";
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Warp's `CLIAgent`
|
|
20
|
-
* `
|
|
21
|
-
* correct identity for this extension. However, the session listener at
|
|
22
|
-
* `app/src/terminal/cli_agent_sessions/listener/mod.rs:48-57` currently routes
|
|
23
|
-
* only `Claude | OpenCode | Gemini | Auggie | Codex` to a notification handler
|
|
24
|
-
* and drops every other variant (including `Pi` and `Unknown`). So `"pi"`
|
|
25
|
-
* parses correctly but produces no toast in current Warp builds.
|
|
26
|
-
*
|
|
27
|
-
* Workaround options, all bad:
|
|
28
|
-
* - `agent: "claude"` → toasts render, but tab gets the Claude Code icon &
|
|
29
|
-
* "Claude" label via `SessionType::CliAgent(CLIAgent::Claude)`. Identity-
|
|
30
|
-
* misrepresenting; user-visibly wrong.
|
|
31
|
-
* - any non-allowlisted ID → no toast.
|
|
32
|
-
*
|
|
33
|
-
* Real fix is upstream: PR `warpdotdev/warp` moving `CLIAgent::Pi` into the
|
|
34
|
-
* `DefaultSessionListener` arm + adding `icon()` / `brand_color()` cases
|
|
35
|
-
* (template: `specs/APP-4067/TECH.md` did this for Gemini). Until that ships,
|
|
36
|
-
* we keep the correct identity and accept that Warp won't render the toast.
|
|
19
|
+
* Warp's `CLIAgent::Pi` is routed to `DefaultSessionListener` since Warp's
|
|
20
|
+
* open-source release (warpdotdev/warp `listener/mod.rs:93-99`).
|
|
37
21
|
*/
|
|
38
22
|
export const AGENT_ID = "pi";
|
|
39
23
|
export const TRUNCATE_LIMIT = 200;
|
|
@@ -58,11 +42,16 @@ export interface StopExtras {
|
|
|
58
42
|
export interface IdlePromptExtras {
|
|
59
43
|
readonly summary: string;
|
|
60
44
|
}
|
|
45
|
+
export interface PromptSubmitExtras {
|
|
46
|
+
readonly query: string;
|
|
47
|
+
}
|
|
61
48
|
export interface ToolCompleteExtras {
|
|
62
49
|
readonly tool_name: string;
|
|
50
|
+
readonly tool_input?: Record<string, unknown>;
|
|
63
51
|
}
|
|
64
52
|
|
|
65
|
-
export type WarpPayload = WarpPayloadBase &
|
|
53
|
+
export type WarpPayload = WarpPayloadBase &
|
|
54
|
+
Partial<StopExtras & IdlePromptExtras & PromptSubmitExtras & ToolCompleteExtras>;
|
|
66
55
|
|
|
67
56
|
// ---------------------------------------------------------------------------
|
|
68
57
|
// Text helpers — small, single-purpose, composable
|
|
@@ -141,8 +130,11 @@ export function buildSessionStartPayload(ctx: ExtensionContext): WarpPayload {
|
|
|
141
130
|
return baseEnvelope("session_start", ctx);
|
|
142
131
|
}
|
|
143
132
|
|
|
144
|
-
export function buildPromptSubmitPayload(ctx: ExtensionContext): WarpPayload {
|
|
145
|
-
return
|
|
133
|
+
export function buildPromptSubmitPayload(ctx: ExtensionContext, query: string = ""): WarpPayload {
|
|
134
|
+
return {
|
|
135
|
+
...baseEnvelope("prompt_submit", ctx),
|
|
136
|
+
query,
|
|
137
|
+
};
|
|
146
138
|
}
|
|
147
139
|
|
|
148
140
|
export function buildQuestionAskedPayload(ctx: ExtensionContext): WarpPayload {
|
|
@@ -164,10 +156,15 @@ export function buildIdlePromptPayload(ctx: ExtensionContext, summary: string):
|
|
|
164
156
|
};
|
|
165
157
|
}
|
|
166
158
|
|
|
167
|
-
export function buildToolCompletePayload(
|
|
159
|
+
export function buildToolCompletePayload(
|
|
160
|
+
ctx: ExtensionContext,
|
|
161
|
+
toolName: string,
|
|
162
|
+
toolInput?: Record<string, unknown>,
|
|
163
|
+
): WarpPayload {
|
|
168
164
|
return {
|
|
169
165
|
...baseEnvelope("tool_complete", ctx),
|
|
170
166
|
tool_name: toolName,
|
|
167
|
+
...(toolInput !== undefined ? { tool_input: toolInput } : {}),
|
|
171
168
|
};
|
|
172
169
|
}
|
|
173
170
|
|