You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
web ui: composer agent and model-variant selectors announce a static label instead of the selected value #57464
The composer "Choose agent" and "Choose model variant" buttons expose a static accessible name, so a screen reader never hears which value is selected.
NVDA on Windows announces:
Choose agent menu button collapsed subMenu
Choose model variant menu button collapsed subMenu
The model button next to them announces its value:
MiniMax M3.1 Flash Free menu button collapsed subMenu
The value is rendered visibly inside the button (packages/app/src/composer/editor/editor.tsx:870), but the trigger also sets aria-label="Choose agent" / aria-label="Choose model variant" (editor.tsx:902 and editor.tsx:929). aria-label takes precedence over the element's text content when the accessible name is computed, so the value is dropped.
That also fails WCAG 2.2 SC 2.5.3 Label in Name, which requires the accessible name to contain the visible text.
The model button works because it sets no aria-label at all. Its name comes from the text content (packages/app/src/composer/composer.tsx:82).
Steps to reproduce
Open the web UI with a screen reader (NVDA on Windows, Chrome or Edge).
Focus the "Choose agent" button in the composer.
Listen to the accessible name.
Repeat for "Choose model variant".
Scope
This issue covers the two composer triggers. The provider account menu (packages/app/src/settings/providers/providers.tsx:328) and the home project dropdown (packages/app/src/home/projects/view.tsx:100) hide their value the same way, but each needs its own copy, so they are tracked separately.
Thanks for the detailed report. I can confirm this on 2.1.1, and it still happens on the latest dev (772e2ae).
Steps I used:
Ran opencode serve (2.1.1) in a project. I configured a dummy provider whose model has variants, so the variant selector appears.
Opened the web UI in Chromium, turned on Settings › General › "Show agent", and opened a new session.
Read the buttons' accessible names from Chrome's accessibility tree.
Result:
Button
Accessible name
Visible text
Agent
Choose agent
Build
Model
Mock Model
Mock Model
Variant
Choose model variant
Default
The agent and variant triggers in packages/app/src/composer/editor/editor.tsx still set aria-label={props.title}, so the selected value isn't part of the name. I only checked the provider-accounts and Projects buttons you mentioned in the code, where they have the same pattern; I didn't test them in the browser.
The script below reproduces it. It prints a table of each popup button's accessible name next to its visible text.
Reproduction script (a11y.mjs)
// Reproduces #57464: composer agent / model-variant triggers expose a static accessible// name instead of the selected value shown in the button.// Setup: a git project at /tmp/opencode/proj with this opencode.json (a dummy provider whose model has// variants, so the variant selector appears; no request is ever sent):// { "providers": { "mock": { "name": "Mock", "env": ["MOCK_API_KEY"], "package": "@ai-sdk/openai-compatible",// "settings": { "baseURL": "http://127.0.0.1:9/v1" },// "models": { "mock-model": { "name": "Mock Model", "variants": [{ "id": "low" }, { "id": "high" }] } } }},// "model": "mock/mock-model" }// Needs: npm i playwright && npx playwright install --with-deps chromium// Usage: MOCK_API_KEY=x opencode serve --hostname 127.0.0.1 --port 4096 (from the project), then// OC_URL=http://127.0.0.1:4096 OC_PASSWORD=<server password> node a11y.mjsimport{chromium}from"playwright"consturl=process.env.OC_URL??"http://127.0.0.1:4096"constpassword=process.env.OC_PASSWORDconstproject=process.env.OC_PROJECT??"/tmp/opencode/proj"// Create a session in the project so it shows up on the home screen.awaitfetch(`${url}/api/session?directory=${encodeURIComponent(project)}`,{method: "POST",headers: {"content-type": "application/json",authorization: "Basic "+Buffer.from(`opencode:${password}`).toString("base64"),},body: JSON.stringify({directory: project}),})constbrowser=awaitchromium.launch()constcontext=awaitbrowser.newContext({viewport: {width: 1400,height: 900},
...(password ? {httpCredentials: {username: "opencode", password }} : {}),})constpage=awaitcontext.newPage()awaitpage.goto(`${url}/?auth_token=${Buffer.from(`opencode:${password}`).toString("base64")}`)awaitpage.waitForTimeout(4000)awaitpage.screenshot({path: "/workspace/media/home.png"})// Enable Settings > General > "Show agent" so the composer shows the agent selector.awaitpage.getByText("Settings",{exact: true}).first().click()awaitpage.waitForTimeout(1500)constsw=page.locator('[data-action="settings-show-custom-agents"] input, [data-action="settings-show-custom-agents"] [role=switch]').first()if(!(awaitsw.isChecked().catch(()=>false)))awaitpage.locator('[data-action="settings-show-custom-agents"]').click()awaitpage.waitForTimeout(500)awaitpage.keyboard.press("Escape")awaitpage.waitForTimeout(1000)awaitpage.screenshot({path: "/workspace/media/settings.png"})// Try to reach a composer: open the project from home if needed.if((awaitpage.locator('[aria-label="Choose agent"]').count())===0){// Open the project from the home screen, then start a new session in it (Ctrl+T).awaitpage.getByText(project.split("/").pop(),{exact: true}).first().click().catch(()=>{})awaitpage.waitForTimeout(1500)awaitpage.keyboard.press("Control+t")awaitpage.waitForTimeout(3000)}constcdp=awaitcontext.newCDPSession(page)awaitcdp.send("Accessibility.enable")const{ nodes }=awaitcdp.send("Accessibility.getFullAXTree")constbuttons=[]for(constnofnodes){constrole=n.role?.valueif(role!=="button"&&role!=="combobox")continueif(!n.properties?.some((p)=>p.name==="hasPopup"))continueif(n.ignored)continueletvisible=""if(n.backendDOMNodeId){const{ object }=awaitcdp.send("DOM.resolveNode",{backendNodeId: n.backendDOMNodeId})constr=awaitcdp.send("Runtime.callFunctionOn",{objectId: object.objectId,functionDeclaration: "function(){return this.innerText.trim()}",returnByValue: true,})visible=r.result.value}buttons.push({accessibleName: n.name?.value,visibleText: visible})}console.table(buttons)awaitpage.screenshot({path: "/workspace/media/composer.png"})awaitbrowser.close()
The trigger now names itself ": ", so the agent button announces "build: Choose agent" and the variant button "default: Choose model variant" instead of the static label. The value leads because that is the text on screen, and WCAG 2.2 SC 2.5.3 asks for the visible text to be in the accessible name.
Activity
opencode-agent commentedon Oct 20, 2026
Thanks for the detailed report. I can confirm this on 2.1.1, and it still happens on the latest
dev(772e2ae).Steps I used:
opencode serve(2.1.1) in a project. I configured a dummy provider whose model has variants, so the variant selector appears.Result:
The agent and variant triggers in
packages/app/src/composer/editor/editor.tsxstill setaria-label={props.title}, so the selected value isn't part of the name. I only checked the provider-accounts and Projects buttons you mentioned in the code, where they have the same pattern; I didn't test them in the browser.The script below reproduces it. It prints a table of each popup button's accessible name next to its visible text.
Reproduction script (a11y.mjs)
Run it:
bun a11y.mjspranam-s commentedon Oct 21, 2026
Fixed in #57488.
The trigger now names itself ": ", so the agent button announces "build: Choose agent" and the variant button "default: Choose model variant" instead of the static label. The value leads because that is the text on screen, and WCAG 2.2 SC 2.5.3 asks for the visible text to be in the accessible name.