From 2a0d3b097f7be4df7ee83e3f1b9492cae52d1e79 Mon Sep 17 00:00:00 2001 From: Xing Zhang Date: Fri, 1 May 2026 01:17:48 +0800 Subject: [PATCH 1/2] fix: call session.init() after switchSession to bind input handlers The upstream createAgentSessionRuntime creates a new InteractiveMode during switchSession but never calls init() on it. This leaves the editor's submit action unbound, causing typed input to be displayed in the input box instead of being sent as a message after resuming a session. Calling init() is safe: it has an isInitialized guard and is idempotent. --- src/session/create-session.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/session/create-session.ts b/src/session/create-session.ts index 67e1610..694404b 100644 --- a/src/session/create-session.ts +++ b/src/session/create-session.ts @@ -615,7 +615,12 @@ export async function createGlmRuntime( runtime.switchSession = async (sessionPath, cwdOverride) => { preferredSelection = getGlmModelSelection(runtime.session.model) ?? preferredSelection; - return originalSwitchSession(sessionPath, cwdOverride); + const result = await originalSwitchSession(sessionPath, cwdOverride); + const session = runtime.session as unknown as { init?: () => Promise }; + if (typeof session.init === "function") { + await session.init(); + } + return result; }; const originalFork = runtime.fork.bind(runtime); From b5debca1ac90d6479bc59ab1de4c109dd18c3edd Mon Sep 17 00:00:00 2001 From: Xing Zhang Date: Fri, 1 May 2026 02:26:13 +0800 Subject: [PATCH 2/2] fix: recover input bindings after resume --- src/runtime/chat-runtime.ts | 34 ++++++++++++ src/session/create-session.ts | 7 +-- tests/runtime/chat-runtime.test.ts | 89 ++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 tests/runtime/chat-runtime.test.ts 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 { preferredSelection = getGlmModelSelection(runtime.session.model) ?? preferredSelection; - const result = await originalSwitchSession(sessionPath, cwdOverride); - const session = runtime.session as unknown as { init?: () => Promise }; - if (typeof session.init === "function") { - await session.init(); - } - return result; + return originalSwitchSession(sessionPath, cwdOverride); }; const originalFork = runtime.fork.bind(runtime); diff --git a/tests/runtime/chat-runtime.test.ts b/tests/runtime/chat-runtime.test.ts new file mode 100644 index 0000000..4144284 --- /dev/null +++ b/tests/runtime/chat-runtime.test.ts @@ -0,0 +1,89 @@ +import { afterEach, describe, expect, test, vi } from "vitest"; +import { + installInteractiveSessionRebindRecovery, + type RebindableRuntimeHost, +} from "../../src/runtime/chat-runtime.js"; + +afterEach(() => { + 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); + }); +});