From 9102d6f98f452c7f7d919088196f3ccb46c44555 Mon Sep 17 00:00:00 2001 From: Julian Coy Date: Fri, 26 Jun 2026 17:03:48 -0400 Subject: [PATCH 1/2] fix(web ui): questions can now collapse, scroll through answers, and properly display questions even on smaller viewports --- .../regression/session-request-docks.spec.ts | 21 ++ packages/app/src/i18n/en.ts | 4 + .../composer/session-question-dock.tsx | 319 ++++++++++-------- .../src/components/message-part.css | 49 ++- 4 files changed, 254 insertions(+), 139 deletions(-) diff --git a/packages/app/e2e/regression/session-request-docks.spec.ts b/packages/app/e2e/regression/session-request-docks.spec.ts index e66cf81504ee..4b0e06a4f999 100644 --- a/packages/app/e2e/regression/session-request-docks.spec.ts +++ b/packages/app/e2e/regression/session-request-docks.spec.ts @@ -38,6 +38,27 @@ test("shows a pending question dock", async ({ page }) => { await expect(question.getByRole("radio", { name: /Extended/ })).toBeVisible() await expect(page.locator('[data-component="session-composer"]')).toHaveCount(0) + const rejectRequests: string[] = [] + page.on("request", (request) => { + if (request.method() !== "POST") return + if (new URL(request.url()).pathname === "/question/question-request/reject") rejectRequests.push(request.url()) + }) + + await question.getByRole("button", { name: "Minimize question" }).click() + await expect(question).toHaveCount(0) + await expect(page.getByText("Which implementation should be used?")).toHaveCount(0) + + const minimized = page.locator('[data-component="question-minimized-dock"]') + await expect(minimized).toBeVisible() + await expect(minimized.getByText("1 pending question")).toBeVisible() + expect(rejectRequests).toEqual([]) + + await minimized.getByRole("button", { name: "Restore question" }).click() + await expect(question).toBeVisible() + await expect(question.getByText("Which implementation should be used?")).toBeVisible() + await expect(question.getByRole("radio", { name: /Minimal/ })).toBeVisible() + expect(rejectRequests).toEqual([]) + await question.getByRole("radio", { name: /Minimal/ }).click() const reply = page.waitForRequest( (request) => request.method() === "POST" && new URL(request.url()).pathname === "/question/question-request/reply", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 3872d01ae78b..4d08f52bcb66 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -637,6 +637,10 @@ export const dict = { "session.todo.expand": "Expand", "session.todo.progress": "{{done}} of {{total}} todos completed", "session.question.progress": "{{current}} of {{total}} questions", + "session.question.minimize": "Minimize question", + "session.question.restore": "Restore question", + "session.question.pending.one": "{{count}} pending question", + "session.question.pending.other": "{{count}} pending questions", "session.followupDock.summary.one": "{{count}} queued message", "session.followupDock.summary.other": "{{count}} queued messages", "session.followupDock.sendNow": "Send now", diff --git a/packages/app/src/pages/session/composer/session-question-dock.tsx b/packages/app/src/pages/session/composer/session-question-dock.tsx index 93ab8818c60c..268bcfddbca9 100644 --- a/packages/app/src/pages/session/composer/session-question-dock.tsx +++ b/packages/app/src/pages/session/composer/session-question-dock.tsx @@ -3,7 +3,9 @@ import { createStore } from "solid-js/store" import { useMutation } from "@tanstack/solid-query" import { Button } from "@opencode-ai/ui/button" import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt" +import { DockTray } from "@opencode-ai/ui/dock-surface" import { Icon } from "@opencode-ai/ui/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" import { showToast } from "@/utils/toast" import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2" import { useLanguage } from "@/context/language" @@ -77,6 +79,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit customOn: cached?.customOn ?? ([] as boolean[]), editing: false, focus: 0, + minimized: false, }) let root: HTMLDivElement | undefined @@ -96,6 +99,9 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit const n = Math.min(store.tab + 1, total()) return language.t("session.question.progress", { current: n, total: total() }) }) + const minimizedSummary = createMemo(() => + language.t(total() === 1 ? "session.question.pending.one" : "session.question.pending.other", { count: total() }), + ) const customLabel = () => language.t("ui.messagePart.option.typeOwnAnswer") const customPlaceholder = () => language.t("ui.question.custom.placeholder") @@ -424,153 +430,192 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit focus(pickFocus(tab)) } + const minimize = () => { + if (sending()) return + setStore("minimized", true) + } + + const restore = () => { + if (sending()) return + setStore("minimized", false) + focus(pickFocus()) + } + return ( - (root = el)} - onKeyDown={nav} - header={ - <> -
{summary()}
- 1}> -
- - {(_, i) => ( -
-
- - } - footer={ - <> - -
- 0}> -
+ + + + + } + footer={ + <> + - - - - - } - > -
- {question()?.question} -
- {language.t("ui.question.singleHint")}}> -
{language.t("ui.question.multiHint")}
-
-
- - {(opt, i) => ( - - - setStore("focus", options().length)} - onClick={customOpen} - > - - - {customLabel()} - {input() || customPlaceholder()} - - +
+ 0}> + + + +
+ } > -
{ - if (sending()) { - e.preventDefault() - return +
+ {question()?.question} +
+ {language.t("ui.question.singleHint")}
}> +
{language.t("ui.question.multiHint")}
+ +
+ + {(opt, i) => ( + + + setStore("focus", options().length)} + onClick={customOpen} + > + + + {customLabel()} + {input() || customPlaceholder()} + + } - if (e.target instanceof HTMLTextAreaElement) return - const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]') - if (input instanceof HTMLTextAreaElement) input.focus() - }} - onSubmit={(e) => { - e.preventDefault() - commitCustom() - }} - > - - - {customLabel()} -