diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 656466b16..f7d1311e2 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -90,6 +90,9 @@ ### Fixed - Fixed `/context` counting hidden, explicit-only skills (`hide: true` / `disable-model-invocation`) in the Skills category and subtracting that inflated estimate from the first system-prompt block, which reported `System prompt: 0 tokens` and inflated Skills usage. Accounting now counts only the skills actually rendered into the system prompt — mirroring `buildSystemPrompt`'s filter, so hidden skills and all skills when the `read` tool is unavailable contribute zero ([#6498](https://github.com/can1357/oh-my-pi/issues/6498)). +### Fixed + +- Fixed `pi-sprite` failing plugin validation because the legacy Pi compatibility shims omitted `createExtensionRuntime` and terminal capability/image-deletion helpers used by the extension ([#6506](https://github.com/can1357/oh-my-pi/issues/6506)). ## [17.1.1] - 2026-07-24 diff --git a/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts b/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts index b2a7be365..6d2040124 100644 --- a/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts +++ b/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts @@ -861,6 +861,11 @@ export interface ResourceLoader { readonly __ompLegacyPiLoader?: true; } +/** Create a pre-initialization runtime for legacy extension resource loaders. */ +export function createExtensionRuntime(): ExtensionRuntime { + return new ExtensionRuntime(); +} + /** * Loader-owned inputs that {@link createAgentSession} needs regardless of * whether the caller provided extra options. `cwd`/`agentDir` fall back to @@ -894,7 +899,7 @@ export class DefaultResourceLoader implements ResourceLoader { readonly __ompLegacyPiLoader = true as const; #state: ResolvedLoaderState; #options: DefaultResourceLoaderOptions; - #extensionsResult: LoadExtensionsResult = { extensions: [], errors: [], runtime: new ExtensionRuntime() }; + #extensionsResult: LoadExtensionsResult = { extensions: [], errors: [], runtime: createExtensionRuntime() }; #skills: Skill[] = []; #skillDiagnostics: ResourceDiagnostic[] = []; #prompts: PromptTemplate[] = []; @@ -1043,7 +1048,7 @@ export class DefaultResourceLoader implements ResourceLoader { const { cwd, noExtensions, additionalExtensionPaths, extensionFactories, eventBus } = this.#state; if (noExtensions && additionalExtensionPaths.length === 0 && extensionFactories.length === 0) { - return { extensions: [], errors: [], runtime: new ExtensionRuntime() }; + return { extensions: [], errors: [], runtime: createExtensionRuntime() }; } const paths = await discoverSessionExtensionPaths( diff --git a/packages/coding-agent/src/extensibility/legacy-pi-tui-shim.ts b/packages/coding-agent/src/extensibility/legacy-pi-tui-shim.ts index 4ac5f57fd..7b448933a 100644 --- a/packages/coding-agent/src/extensibility/legacy-pi-tui-shim.ts +++ b/packages/coding-agent/src/extensibility/legacy-pi-tui-shim.ts @@ -6,5 +6,31 @@ * exposes the equivalent, broader `decodePrintableKey` helper. Keep the legacy * name available without reintroducing it into the canonical package surface. */ +import { ImageProtocol, isInsideTmux, TERMINAL, wrapTmuxPassthrough } from "@oh-my-pi/pi-tui"; + export * from "@oh-my-pi/pi-tui"; -export { decodePrintableKey as decodeKittyPrintable } from "@oh-my-pi/pi-tui"; +export { + decodePrintableKey as decodeKittyPrintable, + encodeKittyDeleteImage as deleteKittyImage, +} from "@oh-my-pi/pi-tui"; + +/** Report canonical terminal capabilities through the legacy Pi TUI shape. */ +export function getCapabilities(): { + images: "kitty" | "iterm2" | null; + trueColor: boolean; + hyperlinks: boolean; +} { + const images = + TERMINAL.imageProtocol === ImageProtocol.Kitty + ? "kitty" + : TERMINAL.imageProtocol === ImageProtocol.Iterm2 + ? "iterm2" + : null; + return { images, trueColor: TERMINAL.trueColor, hyperlinks: TERMINAL.hyperlinks }; +} + +/** Delete every Kitty graphics image using the legacy Pi TUI control sequence. */ +export function deleteAllKittyImages(): string { + const sequence = "\x1b_Ga=d,d=A,q=2\x1b\\"; + return isInsideTmux() ? wrapTmuxPassthrough(sequence) : sequence; +} diff --git a/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts b/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts index a26a02eb4..90ebf4d6b 100644 --- a/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts +++ b/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts @@ -265,6 +265,28 @@ describe("legacy pi package root remaps (issue #1474)", () => { expect(loaded.printable).toBe("a"); }); + it("loads pi-sprite's legacy terminal helpers", async () => { + const entry = await writeFixtureExtension( + [ + 'import { deleteAllKittyImages, deleteKittyImage, getCapabilities } from "@earendil-works/pi-tui";', + "export const deleteOne = deleteKittyImage(42);", + "export const deleteAll = deleteAllKittyImages();", + "export const capabilities = getCapabilities();", + ].join("\n"), + ); + + const loaded = (await loadLegacyPiModule(entry)) as { + deleteOne: string; + deleteAll: string; + capabilities: { images: "kitty" | "iterm2" | null; trueColor: boolean; hyperlinks: boolean }; + }; + expect(loaded.deleteOne).toContain("a=d,d=I,i=42,q=2"); + expect(loaded.deleteAll).toContain("a=d,d=A,q=2"); + expect(["kitty", "iterm2", null]).toContain(loaded.capabilities.images); + expect(typeof loaded.capabilities.trueColor).toBe("boolean"); + expect(typeof loaded.capabilities.hyperlinks).toBe("boolean"); + }); + it("preserves legacy defineTool root imports and usable coding tools", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-legacy-coding-tools-")); tempRoots.push(dir); diff --git a/packages/coding-agent/test/extensibility/legacy-pi-inplace-load.test.ts b/packages/coding-agent/test/extensibility/legacy-pi-inplace-load.test.ts index d84c2dc3f..0fb021b89 100644 --- a/packages/coding-agent/test/extensibility/legacy-pi-inplace-load.test.ts +++ b/packages/coding-agent/test/extensibility/legacy-pi-inplace-load.test.ts @@ -850,6 +850,45 @@ describe("legacy-pi in-place module loading (issue #1674)", () => { }); }); + it("exposes a fresh legacy extension runtime through the package root", async () => { + const dir = await writePackage({ + "package.json": JSON.stringify({ name: "legacy-extension-runtime-ext", version: "1.0.0" }), + "index.ts": [ + 'import { createExtensionRuntime } from "@earendil-works/pi-coding-agent";', + "const first = createExtensionRuntime();", + "const second = createExtensionRuntime();", + "first.flagValues.set('sprite', true);", + "let initializationError;", + "try {", + " first.getActiveTools();", + "} catch (error) {", + " initializationError = error instanceof Error ? error.message : String(error);", + "}", + "export const runtimeContract = {", + " firstFlag: first.flagValues.get('sprite'),", + " secondHasFlag: second.flagValues.has('sprite'),", + " initializationError,", + "};", + "export default function (pi) { void pi; }", + ].join("\n"), + }); + + const mod = (await loadLegacyPiModule(path.join(dir, "index.ts"))) as { + runtimeContract: { + firstFlag: boolean; + secondHasFlag: boolean; + initializationError: string; + }; + }; + + expect(mod.runtimeContract).toEqual({ + firstFlag: true, + secondHasFlag: false, + initializationError: + "Extension runtime not initialized. Action methods cannot be called during extension loading.", + }); + }); + it("honors legacy bash operations overrides", async () => { const dir = await writePackage({ "package.json": JSON.stringify({ name: "legacy-bash-ops-ext", version: "1.0.0" }),