Repository navigation
feat(desktop): refresh question composer - #54822
Conversation
There was a problem hiding this comment.
This works as described and I found nothing that breaks it.
What I checked: I ran the question dock component tests 3× in headless Chromium (12/12 passed), ran typecheck in packages/app and packages/session-ui (both pass), and captured the same multiselect story on the base and on this branch (attached below). I also read through the keyboard handling: number keys are ignored while typing a custom answer, Escape inside the input doesn't dismiss the dock, and custom answers in multiselect keep the predefined choices that were already checked.
Public API: unchanged. The PR only adds English i18n keys.
Worth a look:
- CSS layering: the question styles moved out of the
componentslayer into an unlayered side-effect import, so they now rank above Tailwind utilities (inline comment). - Tests: two of the new component tests are long and depend on exact pixel sizes (inline comments).
- Single-line answers: the custom answer is now a single-line input instead of a textarea, so Shift+Enter no longer adds a new line and pasted line breaks are removed. Worth confirming that's intended.
Related: #44246
| @@ -1,5 +1,6 @@ | |||
| import type { JSX } from "solid-js" | |||
| import { DockShell, DockTray } from "@opencode/ui/dock-surface" | |||
| import "./question-prompt.css" | |||
There was a problem hiding this comment.
These rules used to come in through styles/index.css as @import "../components/message-part.css" layer(components). As a side-effect import from the component, they're now unlayered. Unlayered CSS beats every @layer, including Tailwind utilities, so any utility class later put on these slots (e.g. the existing pointer-events-none on question-options) loses to these rules. Could question-prompt.css be registered in styles/index.css with layer(components), like the other session-ui component styles?
There was a problem hiding this comment.
Fixed in a60a0b7. Question styles now come through styles/index.css in layer(components). Also layered the shared TextInput, Button, IconButton, and submit defaults so the dock overrides still work without an unlayered exception. Utilities now take precedence; light/dark and narrow/wide screenshot comparisons show no visual changes.
There was a problem hiding this comment.
Confirmed: the fix in a60a0b792 works.
-
Question styles:
dock-prompt.tsxno longer imports./question-prompt.css. It now comes in throughpackages/session-ui/src/styles/index.css:8as@import "../components/question-prompt.css" layer(components), like the other session-ui component styles. Tailwind utilities on those slots, such aspointer-events-noneonquestion-options, now take precedence. -
Shared styles:
submit.css,button.css,icon-button.cssandtext-input.cssare each wrapped in@layer components { … }. The@import "../submit.css"lines still come first, which is required because@importmust precede the layer block. The cascade order is declared inui/styles/index.css:1(theme, base, components, utilities), and the base resets inbase.cssstay inlayer(base), so they don't override the components. -
Wider effect: this changes these components for every consumer, not just the dock. Any unlayered app CSS that targets them now overrides them regardless of specificity. I looked at the places this could matter:
- In
app/src/servers/connect/screen.css, thebutton { min-height; width }rules and the 44px height / 16px font-size overrides fortext-input-v2now win every time. Before, the result depended on specificity and source order. This looks like what those mobile overrides intended. app/src/settings/settings.cssimportstext-input.cssandbutton.cssdirectly. Those rules are now layered inside that file, which is fine.- The global
* { -webkit-font-smoothing … }rules inkeybind.cssandline-comment.cssare unlayered. They now beat the button's own font-smoothing, but the values are identical.
I found nothing that would visibly flip.
- In
I only read the diff on pr/head and grepped for consumers of these files. I didn't run a build, the tests or a visual check, so for appearance I'm relying on your screenshot comparison. I made no changes in the checkout.
| await expect(question.getByRole("radio", { name: /Focused/ })).toBeVisible() | ||
| }) | ||
|
|
||
| story("selects single answers and toggles multiple answers with number keys", async ({ mount, page }) => { |
There was a problem hiding this comment.
This test is a ~70-line script that checks many things in one pass: single select, Alt/9 no-ops, custom radio typing, Enter, then multiselect toggling, key-repeat, and custom toggling. When it fails, it's hard to tell which part broke. Could it be cut down to the core contract (a digit selects an option in single select and toggles it in multiselect, and the last number toggles the custom row)?
There was a problem hiding this comment.
Split in c7fba01. The original test now covers numbered selection/toggling, including the custom row. Modifier/out-of-range/repeat safeguards and typing digits into the custom input have separate tests, so those regressions remain covered. All 6 question component tests pass.
There was a problem hiding this comment.
I checked pr/head and the split in c7fba01 looks good to me, so this thread can be resolved.
- Main test (
selects single answers and toggles multiple answers with number keys): it now covers only the core contract. In single select, a digit selects that option and the last number selects the custom row and focuses the input. In multiselect, a digit toggles that option without clearing the others, and the custom row toggles the same way. The Enter step that returns focus to the custom radio stays, because the test needs it to press digits again. It's still about 55 lines because it walks through both question types, but now it's all one behaviour. ignores modified, out-of-range, and repeated number shortcuts: covers9,Alt+2, and a held-down key repeating, which the old test checked along the way.types digits into the custom answer without selecting numbered options: covers typing digits into the custom input.
Nothing that was covered before has been lost. I only read the test file at pr/head and didn't run the component tests. The local pr-54822 checkout is behind pr/head (it doesn't include a60a0b7), and I left it as it is. I made no changes.
| await expect(next).toBeVisible() | ||
| }) | ||
|
|
||
| story("keeps the custom answer input and dock geometry stable while editing", async ({ mount }) => { |
There was a problem hiding this comment.
This test asserts that the row is exactly 40px tall and that the row and dock bounding boxes stay identical after each step. Any small spacing or token change will break it even when behaviour is fine. Checking that the input element stays connected and keeps its draft (plus maybe one height comparison) would cover the layout-jump contract with less churn.
There was a problem hiding this comment.
Simplified in c7fba01. Removed the exact 40px assertion and repeated row/dock bounding-box comparisons. Kept input identity/connectedness, draft persistence, padding/gap click activation, and one before/after dock-height comparison while editing.
There was a problem hiding this comment.
Looks good. That resolves my concern. On pr/head, the geometry story (session-question-dock.spec.ts, from line 147) no longer asserts the fixed 40px height or the repeated boundingBox equality. It still covers what matters:
- the same input element stays in place and connected (
element === original && original.isConnected) - the draft survives Enter and row clicks
- clicks on the padding at (4,4) and the gap at (30,20) activate the custom option
- one dock-height check confirms a long draft doesn't wrap the editor
One small gap, not a blocker: the same commit moved the input.fill("123") / toHaveValue("123") steps out of the multiselect flow. Nothing now checks that a custom draft is kept after the custom checkbox is unchecked with 4 in multiselect. The PR description says drafts are retained, so to keep that covered, add these lines back to the end of the first story:
await input.fill("123") // after `terminal.press("4")`
...
await expect(input).toHaveValue("123") // after `customCheckbox.press("4")`I only read the diff and the file at pr/head. I didn't run the component tests, and I didn't change anything in repo/.
|
Addressed the CSS layering and test feedback in c7fba01 and a60a0b7, with details in the inline replies. Single-line custom answers are intentional: the compact editor stays at a fixed height without expanding or shifting the dock. Shift+Enter and multiline paste no longer preserve line breaks. Verification: 6 question component tests, 21 broader composer/Open in component tests (serial run), 4 production-build request-dock E2E tests, and bun run check pass. One composer subpixel assertion failed in the initial parallel run, then passed in isolation and in the serial suite. |
Vite links shared chunk CSS before the app entry CSS, so button.css named the components layer first and ranked it below Tailwind's base reset. Component borders collapsed to 0px across the app.


Issue for this PR
No linked issue — question composer UI update.
Type of change
What does this PR do?
How did you verify your code works?
bun run checkScreenshots / recordings
Captured from
origin/devand this branch using the same production-component fixtures.Checklist
Added by the opencode bot (AI-generated).
Related: #44246