diff --git a/src/tui-opentui/list-modal.ts b/src/tui-opentui/list-modal.ts index cae1e7a5e..ffc8984b4 100644 --- a/src/tui-opentui/list-modal.ts +++ b/src/tui-opentui/list-modal.ts @@ -41,7 +41,15 @@ export async function runListModal( ): Promise { const renderer = config.createRenderer ? await config.createRenderer() - : await createCliRenderer({ exitOnCtrlC: false, targetFps: 30 }) + : await createCliRenderer({ + exitOnCtrlC: false, + targetFps: 30, + // Same trade as the product host (CL-5540): reporting off by default + // so the terminal owns drag-select and its own copy in these satellite + // pickers too. + useMouse: false, + enableMouseMovement: false, + }) const shell = createAppShell(renderer, { title: config.title, run: "idle" }) diff --git a/src/tui-opentui/mouse-reporting-disabled.test.ts b/src/tui-opentui/mouse-reporting-disabled.test.ts new file mode 100644 index 000000000..05c02423b --- /dev/null +++ b/src/tui-opentui/mouse-reporting-disabled.test.ts @@ -0,0 +1,83 @@ +/** + * CL-5540: the onboarding provider picker and the satellite list modals + * (session resume, session mode) mount their own renderer and must disable + * DEC mouse reporting the same way the product host does, or the terminal + * never gets button-1 drags to run its own text selection. These tests mock + * `@opentui/core` so the real (non-test-injected) `createCliRenderer` branch + * runs, and assert on the options it was actually called with. + */ +import { afterAll, describe, expect, mock, test } from "bun:test" +import type { Harness } from "./harness.js" + +type CapturedRendererOptions = { + readonly useMouse?: boolean + readonly enableMouseMovement?: boolean +} + +const capturedOptions: CapturedRendererOptions[] = [] +const mountedHarnesses: Harness[] = [] + +// The mock must be registered before anything (including this file's own +// helpers) does a real `@opentui/core` import, or that import wins the module +// cache and the mock never takes effect. Every dependency below is loaded +// with a dynamic `import()` after `mock.module` for that reason. +const realCore = await import("@opentui/core") + +mock.module("@opentui/core", () => ({ + ...realCore, + createCliRenderer: async (options: CapturedRendererOptions) => { + capturedOptions.push(options) + const { createHarness } = await import("./harness.js") + const harness = await createHarness({ width: 80, height: 24 }) + mountedHarnesses.push(harness) + return harness.renderer + }, +})) + +// `mock.module` replaces the shared module cache for the whole test process, +// not just this file — every other test that imports `@opentui/core` runs in +// the same process. Put the real module back once this file is done so a +// later un-injected `createCliRenderer` caller does not silently get this +// fake harness renderer instead. +afterAll(() => { + mock.module("@opentui/core", () => realCore) +}) + +const { runListModal } = await import("./list-modal.js") +const { runProviderSetup } = await import("./provider-setup.js") + +async function waitForMount(): Promise { + for (let i = 0; i < 100 && capturedOptions.length === 0; i++) { + await new Promise((resolve) => setTimeout(resolve, 10)) + } +} + +describe("default renderer mount disables DEC mouse reporting (CL-5540)", () => { + test("runListModal", async () => { + capturedOptions.length = 0 + // Fire-and-forget: the mounted renderer never resolves this promise in + // this test (nothing presses a key), so only await the mount itself. + void runListModal({ + title: "resume session", + options: [{ id: "s-1", label: "First session" }], + }) + await waitForMount() + expect(capturedOptions).toHaveLength(1) + expect(capturedOptions[0]?.useMouse).toBe(false) + expect(capturedOptions[0]?.enableMouseMovement).toBe(false) + mountedHarnesses.pop()?.destroy() + }) + + test("runProviderSetup", async () => { + capturedOptions.length = 0 + void runProviderSetup({ + onSubmit: async () => undefined, + showTelemetryNotice: false, + }) + await waitForMount() + expect(capturedOptions).toHaveLength(1) + expect(capturedOptions[0]?.useMouse).toBe(false) + expect(capturedOptions[0]?.enableMouseMovement).toBe(false) + mountedHarnesses.pop()?.destroy() + }) +}) diff --git a/src/tui-opentui/provider-setup.ts b/src/tui-opentui/provider-setup.ts index eb6a6d41f..f368ff3ea 100644 --- a/src/tui-opentui/provider-setup.ts +++ b/src/tui-opentui/provider-setup.ts @@ -583,7 +583,14 @@ export async function runProviderSetup( ): Promise { const renderer = config.createRenderer ? await config.createRenderer() - : await createCliRenderer({ exitOnCtrlC: false, targetFps: 30 }) + : await createCliRenderer({ + exitOnCtrlC: false, + targetFps: 30, + // Same trade as the product host (CL-5540): reporting off by default + // so the terminal owns drag-select and its own copy during onboarding. + useMouse: false, + enableMouseMovement: false, + }) const choices = providerChoices() const values: ProviderFormValues = {