diff --git a/src/runtime/chat-runtime.ts b/src/runtime/chat-runtime.ts index 99d46ad..f3fa6b9 100644 --- a/src/runtime/chat-runtime.ts +++ b/src/runtime/chat-runtime.ts @@ -1,5 +1,34 @@ import { InteractiveMode, type AgentSessionRuntime } from "@mariozechner/pi-coding-agent"; +export type RebindableRuntimeHost = AgentSessionRuntime & { + setRebindSession(handler?: (session: unknown) => Promise): void; +}; + +type InteractiveSessionRebindTarget = { + rebindCurrentSession?: () => Promise; + setupEditorSubmitHandler?: () => void; + editor?: unknown; + ui?: { + setFocus?: (component: unknown) => void; + requestRender?: () => void; + }; +}; + +export function installInteractiveSessionRebindRecovery( + runtime: RebindableRuntimeHost, + interactiveMode: InteractiveSessionRebindTarget, +): void { + runtime.setRebindSession(async () => { + await interactiveMode.rebindCurrentSession?.(); + interactiveMode.setupEditorSubmitHandler?.(); + + if (interactiveMode.editor) { + interactiveMode.ui?.setFocus?.(interactiveMode.editor); + } + interactiveMode.ui?.requestRender?.(); + }); +} + export async function runChatSession(runtime: AgentSessionRuntime): Promise { const interactiveMode = new InteractiveMode(runtime, { migratedProviders: [], @@ -8,5 +37,10 @@ export async function runChatSession(runtime: AgentSessionRuntime): Promise { + vi.restoreAllMocks(); + vi.resetModules(); +}); + +describe("chat runtime", () => { + test("refreshes interactive input bindings after session replacement", async () => { + let rebindSession: (() => Promise) | undefined; + const runtime = { + setRebindSession: vi.fn((handler: () => Promise) => { + rebindSession = handler; + }), + } as unknown as RebindableRuntimeHost; + const interactiveMode = { + rebindCurrentSession: vi.fn(async () => undefined), + setupEditorSubmitHandler: vi.fn(), + editor: {}, + ui: { + setFocus: vi.fn(), + requestRender: vi.fn(), + }, + }; + + installInteractiveSessionRebindRecovery(runtime, interactiveMode); + await rebindSession?.(); + + expect(interactiveMode.rebindCurrentSession).toHaveBeenCalledTimes(1); + expect(interactiveMode.setupEditorSubmitHandler).toHaveBeenCalledTimes(1); + expect(interactiveMode.ui.setFocus).toHaveBeenCalledWith(interactiveMode.editor); + expect(interactiveMode.ui.requestRender).toHaveBeenCalledTimes(1); + }); + + test("runChatSession installs resume input recovery before starting interactive mode", async () => { + let rebindSession: (() => Promise) | undefined; + const interactiveMode = { + rebindCurrentSession: vi.fn(async () => undefined), + setupEditorSubmitHandler: vi.fn(), + editor: {}, + ui: { + setFocus: vi.fn(), + requestRender: vi.fn(), + }, + run: vi.fn(async () => undefined), + }; + const InteractiveMode = vi.fn(function MockInteractiveMode() { + return interactiveMode; + }); + + vi.doMock("@mariozechner/pi-coding-agent", async () => { + const actual = await vi.importActual( + "@mariozechner/pi-coding-agent", + ); + + return { + ...actual, + InteractiveMode, + }; + }); + + const { runChatSession } = await import("../../src/runtime/chat-runtime.js"); + const runtime = { + modelFallbackMessage: undefined, + setRebindSession: vi.fn((handler: () => Promise) => { + rebindSession = handler; + }), + } as unknown as RebindableRuntimeHost; + + await runChatSession(runtime); + await rebindSession?.(); + + expect(InteractiveMode).toHaveBeenCalledWith( + runtime, + expect.objectContaining({ + migratedProviders: [], + initialImages: [], + initialMessages: [], + }), + ); + expect(interactiveMode.run).toHaveBeenCalledTimes(1); + expect(interactiveMode.rebindCurrentSession).toHaveBeenCalledTimes(1); + expect(interactiveMode.setupEditorSubmitHandler).toHaveBeenCalledTimes(1); + }); +});