fix(app): preserve thinking trigger visibility invariant (#47256)

Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-09-04 09:34:18 +00:00
committed by GitHub
Unverified
parent b09a74591c
commit 3a08d95907
5 changed files with 124 additions and 27 deletions
+71 -10
View File
@@ -88,27 +88,88 @@ story("renders a draft once and supports editing, caret restoration, and failure
await expect(input).toHaveText("Preserve this draft on failure")
})
// Moved from packages/app/e2e/regression/prompt-thinking-level.spec.ts
story("shows the thinking level control while relevant", async ({ mount, page }) => {
story("shows thinking on composer hover or when a non-default variant is selected", async ({ mount, page }) => {
const component = await mount("opencode-composer-flow--model-and-variant")
const composer = component.locator('[data-component="composer"]')
const input = composer.locator('[data-component="composer-editor"]')
const input = composer.getByRole("textbox", { name: "Prompt", exact: true })
const control = composer.getByRole("button", { name: "Choose model variant" })
await component.getByRole("status").click()
await page.mouse.move(0, 0)
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur())
await expect(control).toBeVisible()
await expect(control).toHaveText("balanced")
await expect(control).toHaveCSS("opacity", "1")
await control.click()
await page.getByRole("menuitemradio", { name: "default", exact: true }).click()
await component.getByRole("status").click()
await expect(control).toHaveText("default")
await expect(control).toHaveCSS("opacity", "0")
await expect(control).toHaveCSS("pointer-events", "none")
await input.hover()
await expect(control).toHaveCSS("opacity", "1")
await expect(control).toHaveCSS("pointer-events", "auto")
await input.click()
await page.mouse.move(0, 0)
await expect(input).toBeFocused()
await expect(control).toHaveCSS("opacity", "0")
await input.hover()
await control.click()
const high = page.getByRole("menuitemradio", { name: "high" })
await expect(high).toBeVisible()
await page.mouse.move(0, 0)
await expect(control).toBeVisible()
await expect(control).toHaveAttribute("aria-expanded", "true")
await expect(control).toHaveCSS("opacity", "1")
await expect(high).toBeVisible()
await high.click()
await component.getByRole("status").click()
await expect(control).toHaveText("high")
await expect(control).toHaveCSS("opacity", "1")
await input.focus()
await expect(control).toBeVisible()
await input.blur()
await expect(control).toBeVisible()
await control.click()
await page.getByRole("menuitemradio", { name: "default", exact: true }).click()
await component.getByRole("status").click()
await expect(control).toHaveCSS("opacity", "0")
})
story("keeps default thinking accessible by keyboard without composer hover", async ({ mount, page }) => {
const component = await mount("opencode-composer-flow--model-and-variant")
const input = component.getByRole("textbox", { name: "Prompt", exact: true })
const control = component.getByRole("button", { name: "Choose model variant" })
await control.click()
await page.getByRole("menuitemradio", { name: "default", exact: true }).click()
await input.click()
await page.mouse.move(0, 0)
await expect(control).toHaveCSS("opacity", "0")
// Tab through Add, Agent, and Model to the visually hidden thinking trigger.
for (let count = 0; count < 4; count++) await page.keyboard.press("Tab")
await expect(control).toBeFocused()
await expect(control).toHaveCSS("opacity", "1")
await page.keyboard.press("Enter")
await expect(control).toHaveAttribute("aria-expanded", "true")
await expect(page.getByRole("menuitemradio", { name: "default", exact: true })).toBeFocused()
await expect(control).toHaveCSS("opacity", "1")
await page.keyboard.press("Escape")
await expect(control).toHaveAttribute("aria-expanded", "false")
await expect(control).toBeFocused()
await expect(control).toHaveCSS("opacity", "1")
await page.keyboard.press("Enter")
await expect(page.getByRole("menuitemradio", { name: "default", exact: true })).toBeFocused()
await page.keyboard.press("End")
await expect(page.getByRole("menuitemradio", { name: "high", exact: true })).toBeFocused()
await page.keyboard.press("Enter")
await expect(control).toHaveText("high")
await expect(control).toBeFocused()
await page.keyboard.press("Enter")
await expect(page.getByRole("menuitemradio", { name: "default", exact: true })).toBeFocused()
await page.keyboard.press("Enter")
await expect(control).toHaveText("default")
await expect(control).toBeFocused()
await expect(control).toHaveCSS("opacity", "1")
await page.keyboard.press("Tab")
await expect(control).not.toBeFocused()
await expect(control).toHaveCSS("opacity", "0")
})
@@ -1,4 +1,4 @@
import { expect, test, type Page } from "@playwright/test"
import { expect, test } from "@playwright/test"
import { base64Encode } from "@opencode-ai/util/encode"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible } from "../utils/waits"
@@ -8,7 +8,7 @@ const projectID = "proj_prompt_thinking_level_regression"
const sessionID = "ses_prompt_thinking_level_regression"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test("shows the thinking level control while relevant", async ({ page }) => {
test("shows thinking on hover or a non-default selection while preserving keyboard access", async ({ page }) => {
await mockOpenCodeServer(page, {
directory,
project: {
@@ -52,30 +52,57 @@ test("shows the thinking level control while relevant", async ({ page }) => {
})
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
const composer = page.locator('[data-component="composer"]')
const input = composer.locator('[data-component="composer-editor"]')
const input = composer.getByRole("textbox", { name: "Prompt", exact: true })
const control = composer.getByRole("button", { name: "Choose model variant" })
await expectAppVisible(composer)
await idleComposer(page)
await expect(control).toBeVisible()
await page.mouse.move(0, 0)
await expect(control).toHaveText("default")
await expect(control).toHaveCSS("opacity", "0")
await expect(control).toHaveCSS("pointer-events", "none")
await input.hover()
await expect(control).toHaveCSS("opacity", "1")
await input.click()
await page.mouse.move(0, 0)
await expect(input).toBeFocused()
await expect(control).toHaveCSS("opacity", "0")
await input.hover()
await control.click()
const high = page.getByRole("menuitemradio", { name: "high" })
await expect(high).toBeVisible()
await page.mouse.move(0, 0)
await expect(control).toBeVisible()
await expect(control).toHaveAttribute("aria-expanded", "true")
await expect(control).toHaveCSS("opacity", "1")
await expect(high).toBeVisible()
await high.click()
await idleComposer(page)
await input.focus()
await expect(control).toBeVisible()
await idleComposer(page)
await expect(control).toBeVisible()
})
async function idleComposer(page: Page) {
await input.click()
await page.mouse.move(0, 0)
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur())
}
await expect(control).toHaveText("high")
await expect(control).toHaveCSS("opacity", "1")
await control.click()
await page.getByRole("menuitemradio", { name: "default", exact: true }).click()
await input.click()
await page.mouse.move(0, 0)
await expect(control).toHaveText("default")
await expect(control).toHaveCSS("opacity", "0")
// The single-agent fixture has only Add and Model before the thinking trigger.
await page.keyboard.press("Tab")
await expect(composer.getByRole("button", { name: "Add images and files" })).toBeFocused()
await page.keyboard.press("Tab")
await expect(composer.getByRole("button", { name: "Thinking Model" })).toBeFocused()
await page.keyboard.press("Tab")
await expect(control).toBeFocused()
await expect(control).toHaveCSS("opacity", "1")
await page.keyboard.press("Enter")
await expect(page.getByRole("menuitemradio", { name: "default", exact: true })).toBeFocused()
await expect(control).toHaveCSS("opacity", "1")
await page.keyboard.press("Escape")
await expect(control).toBeFocused()
await page.keyboard.press("Tab")
await expect(control).toHaveCSS("opacity", "0")
})
@@ -134,6 +134,7 @@ function ComposerStory(props: {
},
variant: {
options: () => [
{ id: "default", label: "default" },
{ id: "balanced", label: "balanced" },
{ id: "high", label: "high" },
],
@@ -2,6 +2,11 @@
content: "\200B";
}
[data-component="composer"]:not(:hover) .composer-variant-default:not(:focus-visible, [data-expanded]) {
opacity: 0;
pointer-events: none;
}
[data-color-scheme="dark"] [data-component="composer"][data-dock-border-underlay="true"],
[data-color-scheme="dark"] [data-component="new-session"] [data-component="composer"] {
background: var(--v2-background-bg-layer-01);
@@ -263,6 +263,7 @@ export function ComposerEditor(props: ComposerEditorProps) {
title={i18n.t("ui.promptInput.chooseVariant")}
keybind={["Shift", "Mod", "D"]}
control={control}
class={control.current() === "default" ? "composer-variant-default" : undefined}
/>
</Show>
)}
@@ -596,12 +597,14 @@ function ComposerEditorConfiguredSelect(props: {
keybind?: string[]
control: ComposerSelectControl
model?: boolean
class?: string
}) {
const current = () => props.control.current()
const providerID = () => props.control.options().find((option) => option.id === current())?.providerID
return (
<ComposerEditorSelect
title={props.title}
class={props.class}
keybind={props.control.keybind?.() ?? props.keybind}
options={props.control.options()}
current={current()}