diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 81495eef5..e4cd9f10e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -13,6 +13,7 @@ ### Fixed +- Fixed extension TUI method wrappers recursing indefinitely when delegating to the original method ([#7731](https://github.com/earendil-works/pi/issues/7731)). - Fixed right-click not pasting clipboard text in fullscreen mode on Windows. ## [0.84.0] - 2026-08-06 diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 912de637f..720f9b0f9 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -358,11 +358,19 @@ export function createInteractiveTuiReference(getTui: () => TUI): TUI { const tui = getTui(); const value = Reflect.get(tui, property, tui); if (typeof value !== "function") return value; + let methodTui = tui; + let method = value; return (...args: unknown[]) => { - const tui = getTui(); - const method = Reflect.get(tui, property, tui); - if (typeof method !== "function") throw new TypeError(`TUI property ${String(property)} is not callable`); - return Reflect.apply(method, tui, args); + const currentTui = getTui(); + if (currentTui !== methodTui) { + const currentMethod = Reflect.get(currentTui, property, currentTui); + if (typeof currentMethod !== "function") { + throw new TypeError(`TUI property ${String(property)} is not callable`); + } + methodTui = currentTui; + method = currentMethod; + } + return Reflect.apply(method, methodTui, args); }; }, set: (_target, property, value) => { diff --git a/packages/coding-agent/test/suite/regressions/7731-tui-method-wrapping.test.ts b/packages/coding-agent/test/suite/regressions/7731-tui-method-wrapping.test.ts new file mode 100644 index 000000000..ae91246ab --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/7731-tui-method-wrapping.test.ts @@ -0,0 +1,31 @@ +import type { TUI } from "@earendil-works/pi-tui"; +import { describe, expect, it, vi } from "vitest"; +import { createInteractiveTuiReference } from "../../../src/modes/interactive/interactive-mode.ts"; + +describe("TUI method wrapping", () => { + it("calls the method captured before a replacement", () => { + const renderer = { + render: (width: number) => [`width: ${width}`], + } as unknown as TUI; + const tui = createInteractiveTuiReference(() => renderer); + const originalRender = tui.render; + tui.render = (width: number) => originalRender(width); + + expect(tui.render(80)).toEqual(["width: 80"]); + }); + + it("routes a captured method to a replacement renderer", () => { + const regularRequestRender = vi.fn(); + const fullscreenRequestRender = vi.fn(); + let renderer = { requestRender: regularRequestRender } as unknown as TUI; + const tui = createInteractiveTuiReference(() => renderer); + const requestRender = tui.requestRender; + + requestRender(); + renderer = { requestRender: fullscreenRequestRender } as unknown as TUI; + requestRender(); + + expect(regularRequestRender).toHaveBeenCalledOnce(); + expect(fullscreenRequestRender).toHaveBeenCalledOnce(); + }); +});