Skip to content

Commit c79e421

Browse files
committed
Stop pickers turning on DEC mouse reporting outside the product host
createCliRenderer defaults useMouse and enableMouseMovement to true, so the onboarding provider picker and the satellite list modals (session resume, session mode) were emitting the DEC mouse-reporting sequences and stealing button-1 drags from the terminal before a session even starts, even though the main product host already disabled it. Native drag-select and copy now work with no modifier from the very first screen.
1 parent 7753fd5 commit c79e421

3 files changed

Lines changed: 100 additions & 2 deletions

File tree

src/tui-opentui/list-modal.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,15 @@ export async function runListModal(
4141
): Promise<string | null> {
4242
const renderer = config.createRenderer
4343
? await config.createRenderer()
44-
: await createCliRenderer({ exitOnCtrlC: false, targetFps: 30 })
44+
: await createCliRenderer({
45+
exitOnCtrlC: false,
46+
targetFps: 30,
47+
// Same trade as the product host (CL-5540): reporting off by default
48+
// so the terminal owns drag-select and its own copy in these satellite
49+
// pickers too.
50+
useMouse: false,
51+
enableMouseMovement: false,
52+
})
4553

4654
const shell = createAppShell(renderer, { title: config.title, run: "idle" })
4755

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* CL-5540: the onboarding provider picker and the satellite list modals
3+
* (session resume, session mode) mount their own renderer and must disable
4+
* DEC mouse reporting the same way the product host does, or the terminal
5+
* never gets button-1 drags to run its own text selection. These tests mock
6+
* `@opentui/core` so the real (non-test-injected) `createCliRenderer` branch
7+
* runs, and assert on the options it was actually called with.
8+
*/
9+
import { afterAll, describe, expect, mock, test } from "bun:test"
10+
import type { Harness } from "./harness.js"
11+
12+
type CapturedRendererOptions = {
13+
readonly useMouse?: boolean
14+
readonly enableMouseMovement?: boolean
15+
}
16+
17+
const capturedOptions: CapturedRendererOptions[] = []
18+
const mountedHarnesses: Harness[] = []
19+
20+
// The mock must be registered before anything (including this file's own
21+
// helpers) does a real `@opentui/core` import, or that import wins the module
22+
// cache and the mock never takes effect. Every dependency below is loaded
23+
// with a dynamic `import()` after `mock.module` for that reason.
24+
const realCore = await import("@opentui/core")
25+
26+
mock.module("@opentui/core", () => ({
27+
...realCore,
28+
createCliRenderer: async (options: CapturedRendererOptions) => {
29+
capturedOptions.push(options)
30+
const { createHarness } = await import("./harness.js")
31+
const harness = await createHarness({ width: 80, height: 24 })
32+
mountedHarnesses.push(harness)
33+
return harness.renderer
34+
},
35+
}))
36+
37+
// `mock.module` replaces the shared module cache for the whole test process,
38+
// not just this file — every other test that imports `@opentui/core` runs in
39+
// the same process. Put the real module back once this file is done so a
40+
// later un-injected `createCliRenderer` caller does not silently get this
41+
// fake harness renderer instead.
42+
afterAll(() => {
43+
mock.module("@opentui/core", () => realCore)
44+
})
45+
46+
const { runListModal } = await import("./list-modal.js")
47+
const { runProviderSetup } = await import("./provider-setup.js")
48+
49+
async function waitForMount(): Promise<void> {
50+
for (let i = 0; i < 100 && capturedOptions.length === 0; i++) {
51+
await new Promise((resolve) => setTimeout(resolve, 10))
52+
}
53+
}
54+
55+
describe("default renderer mount disables DEC mouse reporting (CL-5540)", () => {
56+
test("runListModal", async () => {
57+
capturedOptions.length = 0
58+
// Fire-and-forget: the mounted renderer never resolves this promise in
59+
// this test (nothing presses a key), so only await the mount itself.
60+
void runListModal({
61+
title: "resume session",
62+
options: [{ id: "s-1", label: "First session" }],
63+
})
64+
await waitForMount()
65+
expect(capturedOptions).toHaveLength(1)
66+
expect(capturedOptions[0]?.useMouse).toBe(false)
67+
expect(capturedOptions[0]?.enableMouseMovement).toBe(false)
68+
mountedHarnesses.pop()?.destroy()
69+
})
70+
71+
test("runProviderSetup", async () => {
72+
capturedOptions.length = 0
73+
void runProviderSetup({
74+
onSubmit: async () => undefined,
75+
showTelemetryNotice: false,
76+
})
77+
await waitForMount()
78+
expect(capturedOptions).toHaveLength(1)
79+
expect(capturedOptions[0]?.useMouse).toBe(false)
80+
expect(capturedOptions[0]?.enableMouseMovement).toBe(false)
81+
mountedHarnesses.pop()?.destroy()
82+
})
83+
})

src/tui-opentui/provider-setup.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,14 @@ export async function runProviderSetup(
583583
): Promise<boolean> {
584584
const renderer = config.createRenderer
585585
? await config.createRenderer()
586-
: await createCliRenderer({ exitOnCtrlC: false, targetFps: 30 })
586+
: await createCliRenderer({
587+
exitOnCtrlC: false,
588+
targetFps: 30,
589+
// Same trade as the product host (CL-5540): reporting off by default
590+
// so the terminal owns drag-select and its own copy during onboarding.
591+
useMouse: false,
592+
enableMouseMovement: false,
593+
})
587594

588595
const choices = providerChoices()
589596
const values: ProviderFormValues = {

0 commit comments

Comments
 (0)