[BUG] Mods: mouse events land to the right of the pointer while a mod Client listens to pointer events (Orca, xterm.js)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code.
What's Wrong?
When a mod pane contains a Client that calls surface.onPointer, mouse events in Claude Code land to the right of the real pointer.
In the docked pane (columns 90 to 196 of a 197-column terminal), each click lands several columns to the right of the pointer. Near column 170, the offset is about 7 columns.
Claude Code's own pane divider has the same offset. To drag the divider, I must put the pointer several columns to the left of it.
When the pane shows only standard elements (no Client), clicks and the divider are exact.
What Should Happen?
A mouse event lands in the cell under the pointer, with or without a mod Client on screen.
Error Messages/Logs
None.
Steps to Reproduce
I saw this with a larger drawing mod. The 4-file mod below uses the same pointer path and shows the reported cell.
- Create a folder
pointer-driftwith the 4 files below. - In Orca, start Claude Code in fullscreen:
claude --plugin-dir ./pointer-drift - Run
/pointer-drift. A pane opens on the right. - Move the mouse over the pane. The
█block marks the cell that Claude Code reports. The last row shows the reported cell and thefineposition. - Near the right edge of the terminal, the block is several columns to the right of the pointer.
finehas a value, so pixel mouse reports are on. - Drag the pane divider. The divider has the same offset.
.claude-plugin/plugin.json
{ "name": "pointer-drift", "version": "0.1.0", "description": "Marks the cell Claude Code reports under the mouse pointer" }
hooks/hooks.json
{ "modules": ["./register.tsx"] }
hooks/register.tsx
import type { Register } from 'claude-code'
export const register: Register = on => {
on('session.start', async ($, e, next) => {
await $.command.register({ name: 'pointer-drift', description: 'Open a pane that marks the cell under the mouse pointer', immediate: true })
return next(e)
})
on('command.run', { command: 'pointer-drift' }, async $ => {
await $.ui.open({ id: 'pointer-drift', title: 'Pointer drift' })
return { text: 'Move the mouse over the pane. The block marks the cell Claude Code reports.' }
})
on('ui.render', { component: 'Pane', requestId: 'pointer-drift' }, ($, e, next) => {
if (e.surface !== 'terminal' && e.surface !== 'desktop') return next(e)
const { Box, Client } = $.ui.resolve(e)
return (
<Box>
<Client key="grid" module="./grid.tsx" props={null} width={e.props.bodyColumns} height={e.props.scroll.bodyRows} />
</Box>
)
})
}
hooks/grid.tsx
import type { ClientModule } from 'claude-code'
type Pointer = { x: number; y: number; fine: string }
const Grid: ClientModule<null, Pointer> = (_props, surface) => {
const at = surface.state ?? { x: -1, y: -1, fine: 'none' }
surface.onPointer(ev => surface.setState({ x: ev.x, y: ev.y, fine: ev.fine === undefined ? 'none' : `${ev.fine.x.toFixed(2)}, ${ev.fine.y.toFixed(2)}` }))
const { Box, Text } = surface.elements
const rows = Array.from({ length: Math.max(0, surface.rows - 1) }, (_, y) =>
Array.from({ length: surface.columns }, (_, x) => (x === at.x && y === at.y ? '█' : x % 10 === 0 ? '|' : '·')).join(''),
)
return (
<Box flexDirection="column">
{rows.map(row => (
<Text>{row}</Text>
))}
<Text>{`reported cell x=${at.x} y=${at.y}, fine=${at.fine}`}</Text>
</Box>
)
}
export default Grid
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.295 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Other: Orca 1.4.217 (based on xterm.js)
Additional Information
Cause, from the Claude Code 2.1.295 code:
- While a
Clienthas a pointer listener, the Client host callsretainFinePointer. ThensyncMousePixelsturns on SGR-Pixels mouse reports (DECSET 1016), because the terminal supports them and answered CSI 16 t. - Claude Code changes each pixel position to a cell as
floor(px / cellWidth) + 1.cellWidthcomes from the CSI 16 t answer. - Orca (xterm.js) sends SGR-Pixels positions in CSS pixels. Its cell width has a fraction, for example 8.2 CSS pixels. Orca answers CSI 16 t with
Math.round(screen width / columns), which gives 8. It answers CSI 14 t with that rounded width times the columns. - Each column adds about 0.2 pixels of error, so the error grows from left to right.
- Claude Code uses this conversion for all mouse events, so its own pane divider has the same offset while the
Clientis on screen.
Possible fixes:
- Turn on pixel mouse reports only when the reported cell size is exact.
- Let a
Clientask for cell positions only, so it does not turn on pixel reports.