Archived
fix(sessions): provide plain-text extension theme
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@jmfederico/pi-web": patch
|
||||
---
|
||||
|
||||
Keep PI WEB-managed sessions running when extensions use `ctx.ui.theme`, preserving formatted output as readable plain text.
|
||||
@@ -48,6 +48,12 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
it("starts sessions through an injected runtime creator", async () => {
|
||||
const hub = new CapturingSessionEventHub();
|
||||
const fake = fakeRuntime();
|
||||
let sessionStartText: string | undefined;
|
||||
const bindExtensions = fake.session.bindExtensions.bind(fake.session);
|
||||
fake.session.bindExtensions = (bindings) => {
|
||||
sessionStartText = bindings.uiContext?.theme.fg("accent", "session started");
|
||||
return bindExtensions(bindings);
|
||||
};
|
||||
let createCalls = 0;
|
||||
let runtimeAgentDir: string | undefined;
|
||||
const createAgentRuntime: RuntimeCreator = async (_createRuntime, options) => {
|
||||
@@ -69,6 +75,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
expect(createCalls).toBe(1);
|
||||
expect(runtimeAgentDir).toBe(TEST_AGENT_DIR);
|
||||
expect(fake.calls.bindExtensions).toHaveLength(1);
|
||||
expect(sessionStartText).toBe("session started");
|
||||
expect(session).toMatchObject({ id: "session-1", cwd: "/workspace", messageCount: 0 });
|
||||
expect(service.activeCount()).toBe(1);
|
||||
expect(hub.globalEvents.some((event) => event.type === "status.update" && event.status.sessionId === "session-1")).toBe(true);
|
||||
@@ -293,6 +300,12 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
const hub = new CapturingSessionEventHub();
|
||||
const fake = fakeRuntime("session-1");
|
||||
const replacement = fakeRuntime("session-2");
|
||||
let replacementSessionStartText: string | undefined;
|
||||
const bindReplacementExtensions = replacement.session.bindExtensions.bind(replacement.session);
|
||||
replacement.session.bindExtensions = (bindings) => {
|
||||
replacementSessionStartText = bindings.uiContext?.theme.fg("success", "replacement started");
|
||||
return bindReplacementExtensions(bindings);
|
||||
};
|
||||
let rebindSession: ((session: PiAgentSession) => Promise<void>) | undefined;
|
||||
fake.runtime.setRebindSession = (callback) => { rebindSession = callback; };
|
||||
const service = new PiSessionService(hub, {
|
||||
@@ -309,6 +322,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
|
||||
expect(fake.calls.bindExtensions).toHaveLength(1);
|
||||
expect(replacement.calls.bindExtensions).toHaveLength(1);
|
||||
expect(replacementSessionStartText).toBe("replacement started");
|
||||
expect(service.activeCount()).toBe(1);
|
||||
expect(await service.status("session-2")).toMatchObject({ sessionId: "session-2" });
|
||||
|
||||
@@ -440,7 +454,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
await service.dispose();
|
||||
});
|
||||
|
||||
it("commits Pi /reload only after replacement session_start notifications are bound", async () => {
|
||||
it("commits Pi /reload only after replacement session_start notifications use the plain-text theme", async () => {
|
||||
const hub = new CapturingSessionEventHub();
|
||||
const store = notificationStore();
|
||||
const fake = fakeRuntime("runtime-reload-notifications");
|
||||
@@ -459,7 +473,8 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
||||
fake.session.reload = async (options) => {
|
||||
oldNotify("shutdown notification", "info");
|
||||
await options?.beforeSessionStart?.();
|
||||
currentNotify(fake)("replacement startup", "error");
|
||||
const replacementStartup = fake.session.extensionRunner.getUIContext().theme.fg("error", "replacement startup");
|
||||
currentNotify(fake)(replacementStartup, "error");
|
||||
};
|
||||
|
||||
await expect(service.runCommand(sessionRef("runtime-reload-notifications"), "/reload")).resolves.toMatchObject({ type: "done" });
|
||||
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
type SessionNotificationGeneration,
|
||||
type SessionNotificationMutation,
|
||||
} from "./sessionNotificationStore.js";
|
||||
import { plainTextTheme } from "./plainTextTheme.js";
|
||||
|
||||
/**
|
||||
* Minimal structured-logging seam, shaped like Fastify's logger so sessiond can
|
||||
@@ -2361,12 +2362,13 @@ export class PiSessionService implements SessionRouteService {
|
||||
notificationId: added.notification.id,
|
||||
});
|
||||
};
|
||||
// PI WEB is a remote UI host, but currently only extension notifications
|
||||
// cross this boundary. Delegate every other UI method to Pi's headless
|
||||
// defaults so unsupported dialogs cancel safely instead of hanging.
|
||||
// PI WEB owns the browser-facing notification and text-formatting
|
||||
// boundaries. Delegate every other UI method to Pi's headless defaults so
|
||||
// unsupported dialogs cancel safely instead of hanging.
|
||||
return new Proxy(baseUiContext, {
|
||||
get(target, property, receiver): unknown {
|
||||
if (property === "notify") return notify;
|
||||
if (property === "theme") return plainTextTheme;
|
||||
const value: unknown = Reflect.get(target, property, receiver);
|
||||
return value;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Theme } from "@earendil-works/pi-coding-agent";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { plainTextTheme } from "./plainTextTheme.js";
|
||||
|
||||
describe("plainTextTheme", () => {
|
||||
it("preserves Pi Theme compatibility without adding formatting", () => {
|
||||
const text = "plain extension output";
|
||||
const formatters: ((value: string) => string)[] = [
|
||||
(value) => plainTextTheme.fg("accent", value),
|
||||
(value) => plainTextTheme.bg("selectedBg", value),
|
||||
(value) => plainTextTheme.bold(value),
|
||||
(value) => plainTextTheme.italic(value),
|
||||
(value) => plainTextTheme.underline(value),
|
||||
(value) => plainTextTheme.inverse(value),
|
||||
(value) => plainTextTheme.strikethrough(value),
|
||||
plainTextTheme.getThinkingBorderColor("high"),
|
||||
plainTextTheme.getBashModeBorderColor(),
|
||||
];
|
||||
|
||||
expect(plainTextTheme).toBeInstanceOf(Theme);
|
||||
for (const format of formatters) expect(format(text)).toBe(text);
|
||||
});
|
||||
|
||||
it("returns no terminal ANSI prefixes", () => {
|
||||
expect(plainTextTheme.getFgAnsi("accent")).toBe("");
|
||||
expect(plainTextTheme.getBgAnsi("selectedBg")).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Theme } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
type ThemeConstructorParameters = ConstructorParameters<typeof Theme>;
|
||||
|
||||
const RESET_COLOR = "";
|
||||
|
||||
// Theme requires complete color tables for nominal class construction. Every
|
||||
// method that could expose the resulting reset codes is overridden below.
|
||||
const SUPERCLASS_FOREGROUND_COLORS = {
|
||||
accent: RESET_COLOR,
|
||||
border: RESET_COLOR,
|
||||
borderAccent: RESET_COLOR,
|
||||
borderMuted: RESET_COLOR,
|
||||
success: RESET_COLOR,
|
||||
error: RESET_COLOR,
|
||||
warning: RESET_COLOR,
|
||||
muted: RESET_COLOR,
|
||||
dim: RESET_COLOR,
|
||||
text: RESET_COLOR,
|
||||
thinkingText: RESET_COLOR,
|
||||
userMessageText: RESET_COLOR,
|
||||
customMessageText: RESET_COLOR,
|
||||
customMessageLabel: RESET_COLOR,
|
||||
toolTitle: RESET_COLOR,
|
||||
toolOutput: RESET_COLOR,
|
||||
mdHeading: RESET_COLOR,
|
||||
mdLink: RESET_COLOR,
|
||||
mdLinkUrl: RESET_COLOR,
|
||||
mdCode: RESET_COLOR,
|
||||
mdCodeBlock: RESET_COLOR,
|
||||
mdCodeBlockBorder: RESET_COLOR,
|
||||
mdQuote: RESET_COLOR,
|
||||
mdQuoteBorder: RESET_COLOR,
|
||||
mdHr: RESET_COLOR,
|
||||
mdListBullet: RESET_COLOR,
|
||||
toolDiffAdded: RESET_COLOR,
|
||||
toolDiffRemoved: RESET_COLOR,
|
||||
toolDiffContext: RESET_COLOR,
|
||||
syntaxComment: RESET_COLOR,
|
||||
syntaxKeyword: RESET_COLOR,
|
||||
syntaxFunction: RESET_COLOR,
|
||||
syntaxVariable: RESET_COLOR,
|
||||
syntaxString: RESET_COLOR,
|
||||
syntaxNumber: RESET_COLOR,
|
||||
syntaxType: RESET_COLOR,
|
||||
syntaxOperator: RESET_COLOR,
|
||||
syntaxPunctuation: RESET_COLOR,
|
||||
thinkingOff: RESET_COLOR,
|
||||
thinkingMinimal: RESET_COLOR,
|
||||
thinkingLow: RESET_COLOR,
|
||||
thinkingMedium: RESET_COLOR,
|
||||
thinkingHigh: RESET_COLOR,
|
||||
thinkingXhigh: RESET_COLOR,
|
||||
thinkingMax: RESET_COLOR,
|
||||
bashMode: RESET_COLOR,
|
||||
} satisfies ThemeConstructorParameters[0];
|
||||
|
||||
const SUPERCLASS_BACKGROUND_COLORS = {
|
||||
selectedBg: RESET_COLOR,
|
||||
userMessageBg: RESET_COLOR,
|
||||
customMessageBg: RESET_COLOR,
|
||||
toolPendingBg: RESET_COLOR,
|
||||
toolSuccessBg: RESET_COLOR,
|
||||
toolErrorBg: RESET_COLOR,
|
||||
} satisfies ThemeConstructorParameters[1];
|
||||
|
||||
function preserveText(text: string): string {
|
||||
return text;
|
||||
}
|
||||
|
||||
class PlainTextTheme extends Theme {
|
||||
constructor() {
|
||||
super(SUPERCLASS_FOREGROUND_COLORS, SUPERCLASS_BACKGROUND_COLORS, "256color", { name: "pi-web-plain-text" });
|
||||
}
|
||||
|
||||
override fg(color: Parameters<Theme["fg"]>[0], text: string): string {
|
||||
void color;
|
||||
return text;
|
||||
}
|
||||
|
||||
override bg(color: Parameters<Theme["bg"]>[0], text: string): string {
|
||||
void color;
|
||||
return text;
|
||||
}
|
||||
|
||||
override bold(text: string): string {
|
||||
return text;
|
||||
}
|
||||
|
||||
override italic(text: string): string {
|
||||
return text;
|
||||
}
|
||||
|
||||
override underline(text: string): string {
|
||||
return text;
|
||||
}
|
||||
|
||||
override inverse(text: string): string {
|
||||
return text;
|
||||
}
|
||||
|
||||
override strikethrough(text: string): string {
|
||||
return text;
|
||||
}
|
||||
|
||||
override getFgAnsi(color: Parameters<Theme["getFgAnsi"]>[0]): string {
|
||||
void color;
|
||||
return "";
|
||||
}
|
||||
|
||||
override getBgAnsi(color: Parameters<Theme["getBgAnsi"]>[0]): string {
|
||||
void color;
|
||||
return "";
|
||||
}
|
||||
|
||||
override getColorMode(): ReturnType<Theme["getColorMode"]> {
|
||||
return "256color";
|
||||
}
|
||||
|
||||
override getThinkingBorderColor(
|
||||
level: Parameters<Theme["getThinkingBorderColor"]>[0],
|
||||
): ReturnType<Theme["getThinkingBorderColor"]> {
|
||||
void level;
|
||||
return preserveText;
|
||||
}
|
||||
|
||||
override getBashModeBorderColor(): ReturnType<Theme["getBashModeBorderColor"]> {
|
||||
return preserveText;
|
||||
}
|
||||
}
|
||||
|
||||
/** Shared ANSI-free Theme facade for extension code running without a terminal. */
|
||||
export const plainTextTheme: Theme = new PlainTextTheme();
|
||||
Reference in New Issue
Block a user