Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/runtime/chat-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
import { InteractiveMode, type AgentSessionRuntime } from "@mariozechner/pi-coding-agent";

export type RebindableRuntimeHost = AgentSessionRuntime & {
setRebindSession(handler?: (session: unknown) => Promise<void>): void;
};

type InteractiveSessionRebindTarget = {
rebindCurrentSession?: () => Promise<void>;
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<void> {
const interactiveMode = new InteractiveMode(runtime, {
migratedProviders: [],
Expand All @@ -8,5 +37,10 @@ export async function runChatSession(runtime: AgentSessionRuntime): Promise<void
initialMessages: [],
});

installInteractiveSessionRebindRecovery(
runtime as RebindableRuntimeHost,
interactiveMode as unknown as InteractiveSessionRebindTarget,
);

await interactiveMode.run();
}
89 changes: 89 additions & 0 deletions tests/runtime/chat-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -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<void>) | undefined;
const runtime = {
setRebindSession: vi.fn((handler: () => Promise<void>) => {
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<void>) | 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<typeof import("@mariozechner/pi-coding-agent")>(
"@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<void>) => {
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);
});
});
Loading