[BUG] Desktop Code tab: plugin Select/Input labels are read twice by VoiceOver

Status Open
Reported on v2.1.286
Maintainer reply None cached
Activity 0 comments · opened Oct 6, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report

What's Wrong?

In the desktop app's Code tab, a plugin pane that uses Select or Input with a label prop makes VoiceOver read each label twice. The desktop surface draws the label as a separate visible text element and also uses it as the control's accessible name. Every setting therefore costs two VoiceOver stops, and the second one repeats the first.

This is the VoiceOver cursor text, read with VO-Right after interacting into the pane (captured with a VoiceOver scripting CLI, reported speech text):

"Ping when a turn finishes"
"Off Ping when a turn finishes list box pop up collapsed combo box"
"Ping when Claude asks a question"
"Off Ping when Claude asks a question list box pop up collapsed combo box"
"Send to"
"someone@example.com Insertion at beginning of text. Send to edit text"
"save button"

A pane with a column of on/off settings becomes a long run of doubled stops, which makes it tiring to use with a screen reader.

What Should Happen?

The label should be announced once, as the name of the control. The visible label text should be hidden from the accessibility tree (for example aria-hidden="true" on the visible copy, with the control keeping aria-label or aria-labelledby), or the label and control should be exposed as one element. A plugin has no prop to do this itself: dropping label leaves the control unnamed, which is worse.

Error Messages/Logs

None. The tree validates and draws.

Steps to Reproduce

  1. Load a function-hooks plugin whose ui.render hook on a Pane returns, on the desktop surface:

``tsx
const { Box, Select, Input } = $.ui.resolve(e)
return (
<Box flexDirection="column">
<Select key="s" label="Ping when a turn finishes" value="off"
options={[{ value: 'on', label: 'On' }, { value: 'off', label: 'Off' }]}
onSelect={() => {}} />
<Input key="i" label="Send to" value="someone@example.com" onSubmit={() => {}} />
</Box>
)
``

  1. Open the pane with $.ui.open({ id, title, focus: true }).
  2. Turn on VoiceOver, interact into the pane (VO-Shift-Down) and move through it with VO-Right.
  3. Each label is read on its own, then again as part of the control.

The Input also exposes its submitLabel as a separate button ("save button"). That one may be intended, but it adds a third stop per field.

Is this a regression?

I don't know

Claude Code Version

2.1.286 (Claude Code), Claude desktop app 2.19675.0

Platform

Anthropic API

Operating System

macOS 27.0.1

Terminal/Shell

Other (Claude desktop app, Code tab)

Additional Information

Found while building an accessibility-focused plugin: https://github.com/CamdenBopp/claude-afk. The plugin's own tests pass on every surface because the test kit checks the element description, not the accessibility tree the desktop builds from it, so this only shows up under a real screen reader.

View original on GitHub ↗