Skip to content

feat(desktop): refresh question composer - #54822

Merged
Hona merged 9 commits into
devfrom
question-composer
Oct 13, 2026
Merged

Hona merged 9 commits into
devfrom
question-composer

Conversation

@usrnk1

@usrnk1 usrnk1 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Issue for this PR

No linked issue — question composer UI update.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

  • Refreshes the question composer with V2 tokens, tighter spacing, medium labels, and 13px descriptions.
  • Keeps the custom-answer input mounted in a compact row, avoiding layout jumps and retaining drafts.
  • Preserves checked options alongside custom answers in multiselect, including full-row click activation.
  • Adds 1–9 answer shortcuts and keeps the Next/Submit shortcuts visible in the buttons.

How did you verify your code works?

  • bun run check
  • Question composer component tests: 4 passed.
  • Request dock E2E tests against a production build: 4 passed.
  • Checked light/dark, narrow layouts, RTL, and mixed-direction input.

Screenshots / recordings

Captured from origin/dev and this branch using the same production-component fixtures.

State Before After
Single select Single select before Single select after
Multiselect + custom answer Multiselect and custom answer before Multiselect and custom answer after
Dark mode Dark mode before Dark mode after

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #44246

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 components layer 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

Before

After

@@ -1,5 +1,6 @@
import type { JSX } from "solid-js"
import { DockShell, DockTray } from "@opencode/ui/dock-surface"
import "./question-prompt.css"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed: the fix in a60a0b792 works.

  • Question styles: dock-prompt.tsx no longer imports ./question-prompt.css. It now comes in through packages/session-ui/src/styles/index.css:8 as @import "../components/question-prompt.css" layer(components), like the other session-ui component styles. Tailwind utilities on those slots, such as pointer-events-none on question-options, now take precedence.

  • Shared styles: submit.css, button.css, icon-button.css and text-input.css are each wrapped in @layer components { … }. The @import "../submit.css" lines still come first, which is required because @import must precede the layer block. The cascade order is declared in ui/styles/index.css:1 (theme, base, components, utilities), and the base resets in base.css stay in layer(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, the button { min-height; width } rules and the 44px height / 16px font-size overrides for text-input-v2 now win every time. Before, the result depended on specificity and source order. This looks like what those mobile overrides intended.
    • app/src/settings/settings.css imports text-input.css and button.css directly. Those rules are now layered inside that file, which is fine.
    • The global * { -webkit-font-smoothing … } rules in keybind.css and line-comment.css are unlayered. They now beat the button's own font-smoothing, but the values are identical.

    I found nothing that would visibly flip.

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 }) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: covers 9, 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 }) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/.

@usrnk1

usrnk1 commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

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.

usrnk1 and others added 4 commits October 12, 2026 19:15
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.
@Hona
Hona enabled auto-merge (squash) October 13, 2026 21:38
@Hona
Hona disabled auto-merge October 13, 2026 22:02
@Hona
Hona merged commit 3ec856b into dev Oct 13, 2026
10 checks passed
@Hona
Hona deleted the question-composer branch October 13, 2026 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants