Merge pull request #37 from danielkuykendall23-boop/fm/openrig-pi-reply-mirror

fix(daemon): show Pi replies in the seat pane with Pi 0.84+
This commit is contained in:
Mike Schwarz authored and GitHub committed 2026-09-24 21:59:18 -07:00
commit b374dde300
2 files changed
+58 -36

No files matched your search

+8 -24
View File
@@ -82,29 +82,6 @@ export interface MirrorAndActivity {
streaming?: boolean;
}
/** Tolerant text extraction from a Pi message shape (string, {text}, or
* content-block arrays). Exact field calibration is a VM-proof concern; this
* covers the documented shapes without throwing on unknowns. */
export function extractMessageText(message: unknown): string {
if (typeof message === "string") return message;
if (message === null || typeof message !== "object") return "";
const m = message as Record<string, unknown>;
if (typeof m.text === "string") return m.text;
if (Array.isArray(m.content)) {
return m.content
.map((block) => {
if (typeof block === "string") return block;
if (block !== null && typeof block === "object" && typeof (block as Record<string, unknown>).text === "string") {
return (block as Record<string, unknown>).text as string;
}
return "";
})
.join("");
}
if (typeof m.content === "string") return m.content;
return "";
}
export function mapPiEvent(event: Record<string, unknown>): MirrorAndActivity {
const type = typeof event.type === "string" ? event.type : "";
switch (type) {
@@ -117,7 +94,14 @@ export function mapPiEvent(event: Record<string, unknown>): MirrorAndActivity {
case "message_start":
return { mirrorLines: [] };
case "message_update": {
const delta = extractMessageText(event.message ?? event.delta);
// Pi streams assistant text as `assistantMessageEvent` text_delta
// records. Only the delta is mirrored: Pi 0.84.0 dropped the cumulative
// `message` from RPC message_update, and older Pi sent it alongside the
// same deltas, so appending it repeated the growing text. Thinking and
// tool-call argument deltas stay out of the pane; tool calls get their
// own one-line summaries from tool_execution_*.
const update = event.assistantMessageEvent as Record<string, unknown> | undefined;
const delta = update?.type === "text_delta" && typeof update.delta === "string" ? update.delta : "";
return delta ? { mirrorLines: [], mirrorAppend: delta } : { mirrorLines: [] };
}
case "message_end": {
+50 -12
View File
@@ -6,7 +6,7 @@
import { describe, it, expect, vi } from "vitest";
import {
PasteAggregator, RunnerCore, mapPiEvent, extractMessageText, parseRunnerArgs,
PasteAggregator, RunnerCore, mapPiEvent, parseRunnerArgs,
prepareRunnerSidecar,
type RunnerIo,
} from "../src/adapters/pi-runner.js";
@@ -171,6 +171,37 @@ describe("RunnerCore identity + sidecar", () => {
// ── event → mirror / activity mapping ────────────────────────────────────────
// Shapes from a real `pi --mode rpc` 0.87.1 run (OpenRig's child argv) that
// streamed a reply from a local OpenAI-compatible mock. Since Pi 0.84.0,
// message_update carries only `usage` and the `assistantMessageEvent` delta.
const PI_USAGE = {
input: 12, output: 5, cacheRead: 0, cacheWrite: 0, reasoning: 0, totalTokens: 17,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};
const PI_REPLY = {
role: "assistant",
content: [
{ type: "thinking", thinking: "User wants a greeting.", thinkingSignature: "reasoning_content" },
{ type: "text", text: "Hello from the mock server." },
],
api: "openai-completions", provider: "mock", model: "mock-model", usage: PI_USAGE,
stopReason: "stop", timestamp: 1790281248272, responseId: "chatcmpl-mock",
};
const piUpdate = (assistantMessageEvent: Record<string, unknown>) =>
({ type: "message_update", usage: PI_USAGE, assistantMessageEvent });
const PI_REPLY_EVENTS = [
{ type: "message_start", message: { ...PI_REPLY, content: [], stopReason: "pending" } },
piUpdate({ type: "thinking_start", contentIndex: 0 }),
piUpdate({ type: "thinking_delta", contentIndex: 0, delta: "User wants a greeting." }),
piUpdate({ type: "text_start", contentIndex: 1 }),
piUpdate({ type: "text_delta", contentIndex: 1, delta: "Hello" }),
piUpdate({ type: "text_delta", contentIndex: 1, delta: " from the" }),
piUpdate({ type: "text_delta", contentIndex: 1, delta: " mock server." }),
piUpdate({ type: "thinking_end", contentIndex: 0, content: "User wants a greeting." }),
piUpdate({ type: "text_end", contentIndex: 1, content: "Hello from the mock server." }),
{ type: "message_end", message: PI_REPLY },
];
describe("mapPiEvent", () => {
it("agent_start/agent_end drive streaming + running/idle activity", () => {
expect(mapPiEvent({ type: "agent_start" })).toMatchObject({
@@ -181,10 +212,14 @@ describe("mapPiEvent", () => {
});
});
it("message_update deltas append to the mirror; message_end terminates the line", () => {
const update = mapPiEvent({ type: "message_update", message: { content: [{ type: "text", text: "hel" }] } });
expect(update.mirrorAppend).toBe("hel");
expect(mapPiEvent({ type: "message_end" }).mirrorLines).toEqual([""]);
it("pre-0.84 message_update appends only the delta, never the cumulative message", () => {
const partial = { ...PI_REPLY, content: [{ type: "text", text: "Hello from the" }], stopReason: "pending" };
const legacy = mapPiEvent({
type: "message_update",
message: partial,
assistantMessageEvent: { type: "text_delta", contentIndex: 1, delta: " from the", partial },
});
expect(legacy.mirrorAppend).toBe(" from the");
});
it("tool executions render compact one-line summaries + PreToolUse activity", () => {
@@ -199,14 +234,17 @@ describe("mapPiEvent", () => {
expect(mapPiEvent({ type: "compaction_start" }).activity).toEqual({ hookEvent: "active", subtype: "compaction" });
expect(mapPiEvent({ type: "auto_retry_start" }).activity).toEqual({ hookEvent: "active", subtype: "auto_retry" });
});
});
it("extractMessageText tolerates string / {text} / content-block shapes", () => {
expect(extractMessageText("plain")).toBe("plain");
expect(extractMessageText({ text: "t" })).toBe("t");
expect(extractMessageText({ content: [{ type: "text", text: "a" }, { type: "text", text: "b" }] })).toBe("ab");
expect(extractMessageText({ content: "inline" })).toBe("inline");
expect(extractMessageText(null)).toBe("");
expect(extractMessageText({ weird: true })).toBe("");
describe("RunnerCore assistant reply mirror", () => {
it("streams a Pi 0.84+ reply from text deltas and ends the line on message_end", () => {
const f = fakeIo();
const { core } = readyCore(f);
let pane = "";
f.io.mirrorLine = (line) => { pane += `${line}\n`; };
f.io.mirrorAppend = (text) => { pane += text; };
for (const event of PI_REPLY_EVENTS) core.handlePiLine(JSON.stringify(event));
expect(pane).toBe("Hello from the mock server.\n");
});
});