Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 143 additions & 1 deletion apps/pi-extension/current-pi-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
notifyCurrentPiSession,
registerCurrentPiSession,
sendUserMessageToCurrentPiSession,
resolveIdleDeliveryOptions,
type CurrentPiSessionRegistration,
} from "./current-pi-session.ts";

Expand All @@ -14,7 +15,7 @@ afterEach(() => {
for (const registration of registrations.splice(0)) registration.clear();
});

function createContext(sessionId: string, notifications: string[]): ExtensionContext {
function createContext(sessionId: string, notifications: string[], idle = true): ExtensionContext {
return {
cwd: "/tmp",
mode: "tui",
Expand All @@ -24,6 +25,7 @@ function createContext(sessionId: string, notifications: string[]): ExtensionCon
getSessionName: () => sessionId,
},
ui: { notify: (message: string) => notifications.push(message) },
isIdle: () => idle,
} as unknown as ExtensionContext;
}

Expand Down Expand Up @@ -68,3 +70,143 @@ describe("current Pi session feedback routing", () => {
expect(replacementNotifications).toEqual(["feedback delivered"]);
});
});

describe("resolveIdleDeliveryOptions", () => {
test("idle host drops deliverAs so the host starts a turn", () => {
expect(
resolveIdleDeliveryOptions({ isIdle: () => true }, { deliverAs: "followUp" }),
).toBeUndefined();
});

test("streaming host keeps deliverAs", () => {
expect(
resolveIdleDeliveryOptions({ isIdle: () => false }, { deliverAs: "followUp" }),
).toEqual({ deliverAs: "followUp" });
});

test("idle host keeps sibling options and drops only deliverAs", () => {
expect(
resolveIdleDeliveryOptions(
{ isIdle: () => true },
{ deliverAs: "followUp", expandPromptTemplates: true },
),
).toEqual({ expandPromptTemplates: true });
});

test("host without isIdle passes options through unchanged", () => {
expect(resolveIdleDeliveryOptions({}, { deliverAs: "followUp" })).toEqual({
deliverAs: "followUp",
});
});

test("probe that throws passes options through unchanged", () => {
expect(
resolveIdleDeliveryOptions(
{
isIdle: () => {
throw new Error("probe exploded");
},
},
{ deliverAs: "followUp" },
),
).toEqual({ deliverAs: "followUp" });
});

test("undefined options stay undefined", () => {
expect(resolveIdleDeliveryOptions({ isIdle: () => true })).toBeUndefined();
});

test("options without deliverAs are untouched even when idle", () => {
expect(
resolveIdleDeliveryOptions({ isIdle: () => true }, { expandPromptTemplates: true }),
).toEqual({ expandPromptTemplates: true });
});

test("steer is also dropped when idle", () => {
expect(
resolveIdleDeliveryOptions({ isIdle: () => true }, { deliverAs: "steer" }),
).toBeUndefined();
});
});

describe("idle delivery through the current-session send path", () => {
test("idle replacement runtime drops deliverAs so the host starts a turn", () => {
const oldRuntime = registerSessionRuntime("same-session", [], []);
const origin = getPiSessionIdentity(oldRuntime.ctx);

const captured: Array<{ content: unknown; options: unknown }> = [];
const replacementPi = {
sendUserMessage: (content: unknown, options?: unknown) => {
captured.push({ content, options });
},
} as unknown as ExtensionAPI;
const replacement = registerCurrentPiSession(replacementPi);
registrations.push(replacement);
replacement.update(createContext("same-session", [], true));
oldRuntime.registration.clear();

const result = sendUserMessageToCurrentPiSession(
"annotation feedback",
{ deliverAs: "followUp" },
origin,
);

expect(result).toEqual({ ok: true });
expect(captured).toEqual([{ content: "annotation feedback", options: undefined }]);
});

test("streaming replacement runtime keeps deliverAs", () => {
const oldRuntime = registerSessionRuntime("same-session", [], []);
const origin = getPiSessionIdentity(oldRuntime.ctx);

const captured: Array<{ content: unknown; options: unknown }> = [];
const replacementPi = {
sendUserMessage: (content: unknown, options?: unknown) => {
captured.push({ content, options });
},
} as unknown as ExtensionAPI;
const replacement = registerCurrentPiSession(replacementPi);
registrations.push(replacement);
replacement.update(createContext("same-session", [], false));
oldRuntime.registration.clear();

const result = sendUserMessageToCurrentPiSession(
"annotation feedback",
{ deliverAs: "followUp" },
origin,
);

expect(result).toEqual({ ok: true });
expect(captured).toEqual([
{ content: "annotation feedback", options: { deliverAs: "followUp" } },
]);
});

test("isIdle on ExtensionAPI is ignored; the context is the probe", () => {
const oldRuntime = registerSessionRuntime("same-session", [], []);
const origin = getPiSessionIdentity(oldRuntime.ctx);

const captured: Array<{ content: unknown; options: unknown }> = [];
const lyingPi = {
isIdle: () => true,
sendUserMessage: (content: unknown, options?: unknown) => {
captured.push({ content, options });
},
} as unknown as ExtensionAPI;
const replacement = registerCurrentPiSession(lyingPi);
registrations.push(replacement);
replacement.update(createContext("same-session", [], false));
oldRuntime.registration.clear();

const result = sendUserMessageToCurrentPiSession(
"annotation feedback",
{ deliverAs: "followUp" },
origin,
);

expect(result).toEqual({ ok: true });
expect(captured).toEqual([
{ content: "annotation feedback", options: { deliverAs: "followUp" } },
]);
});
});
21 changes: 20 additions & 1 deletion apps/pi-extension/current-pi-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ type SendUserMessageContent = Parameters<ExtensionAPI["sendUserMessage"]>[0];
type SendUserMessageOptions = Parameters<ExtensionAPI["sendUserMessage"]>[1];
type NotificationType = "info" | "warning" | "error";

type IdleProbeHost = { isIdle?: () => boolean };

export function resolveIdleDeliveryOptions(
host: IdleProbeHost,
options?: SendUserMessageOptions,
): SendUserMessageOptions | undefined {
if (!options || options.deliverAs === undefined) return options;
const probe = host.isIdle;
if (typeof probe !== "function") return options;
try {
if (!probe.call(host)) return options;
} catch {
return options;
}
const next = { ...options };
delete next.deliverAs;
return Object.keys(next).length > 0 ? next : undefined;
}

type CurrentPiSession = {
token: symbol;
sendUserMessage: (content: SendUserMessageContent, options?: SendUserMessageOptions) => void;
Expand Down Expand Up @@ -96,7 +115,7 @@ function setCurrentPiSession(token: symbol, pi: ExtensionAPI, ctx?: ExtensionCon
const current: CurrentPiSession = {
token,
sendUserMessage: (content, options) => {
pi.sendUserMessage(content, options);
pi.sendUserMessage(content, resolveIdleDeliveryOptions(ctx ?? {}, options));
},
};
if (ctx) {
Expand Down
8 changes: 7 additions & 1 deletion apps/pi-extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
notifyCurrentPiSession,
type PiSessionIdentity,
registerCurrentPiSession,
resolveIdleDeliveryOptions,
sendUserMessageToCurrentPiSession,
withCurrentPiSessionFallbackHeader,
} from "./current-pi-session.ts";
Expand Down Expand Up @@ -268,11 +269,12 @@ function sendUserMessageWithCurrentSessionFallback(
options: Parameters<ExtensionAPI["sendUserMessage"]>[1],
errorMessage: string,
origin: PiSessionIdentity,
ctx?: ExtensionContext,
): void {
if (trySendUserMessageToDifferentCurrentSession(content, options, errorMessage, origin)) return;

try {
pi.sendUserMessage(content, options);
pi.sendUserMessage(content, resolveIdleDeliveryOptions(ctx ?? {}, options));
return;
} catch (err) {
if (trySendUserMessageToDifferentCurrentSession(content, options, errorMessage, origin)) return;
Expand Down Expand Up @@ -673,6 +675,7 @@ export default function plannotator(pi: ExtensionAPI): void {
{ deliverAs: "followUp" },
"Plannotator code review feedback could not be sent",
origin,
ctx,
);
return;
}
Expand All @@ -696,6 +699,7 @@ export default function plannotator(pi: ExtensionAPI): void {
{ deliverAs: "followUp" },
"Plannotator code review feedback could not be sent",
origin,
ctx,
);
} catch (err) {
reportBackgroundError(ctx, "Plannotator code review feedback could not be sent", err, origin);
Expand Down Expand Up @@ -994,6 +998,7 @@ export default function plannotator(pi: ExtensionAPI): void {
{ deliverAs: "followUp" },
"Plannotator annotation feedback could not be sent",
origin,
ctx,
);
if (outcome.notification === "approved") {
safeNotify(ctx, "Annotation approved.", "info", origin);
Expand Down Expand Up @@ -1088,6 +1093,7 @@ export default function plannotator(pi: ExtensionAPI): void {
{ deliverAs: "followUp" },
"Plannotator message annotation feedback could not be sent",
origin,
ctx,
);
if (outcome.notification === "approved") {
safeNotify(ctx, "Message approved.", "info", origin);
Expand Down