Skip to content

Commit 96d731e

Browse files
Apply PR #15697: tweak(ui): make questions popup collapsible
2 parents 0cf29ba + 611e616 commit 96d731e

File tree

2 files changed

+148
-64
lines changed

2 files changed

+148
-64
lines changed

packages/app/src/pages/session/composer/session-question-dock.tsx

Lines changed: 126 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useMutation } from "@tanstack/solid-query"
44
import { Button } from "@opencode-ai/ui/button"
55
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
66
import { Icon } from "@opencode-ai/ui/icon"
7+
import { IconButton } from "@opencode-ai/ui/icon-button"
78
import { showToast } from "@opencode-ai/ui/toast"
89
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
910
import { useLanguage } from "@/context/language"
@@ -73,6 +74,8 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
7374
customOn: cached?.customOn ?? ([] as boolean[]),
7475
editing: false,
7576
focus: 0,
77+
sending: false,
78+
collapsed: false,
7679
})
7780

7881
let root: HTMLDivElement | undefined
@@ -87,6 +90,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
8790
const on = createMemo(() => store.customOn[store.tab] === true)
8891
const multi = createMemo(() => question()?.multiple === true)
8992
const count = createMemo(() => options().length + 1)
93+
const selectedCount = createMemo(() => store.answers[store.tab]?.length ?? 0)
9094

9195
const summary = createMemo(() => {
9296
const n = Math.min(store.tab + 1, total())
@@ -98,6 +102,8 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
98102

99103
const last = createMemo(() => store.tab >= total() - 1)
100104

105+
const fold = () => setStore("collapsed", (value) => !value)
106+
101107
const customUpdate = (value: string, selected: boolean = on()) => {
102108
const prev = input().trim()
103109
const next = value.trim()
@@ -426,9 +432,21 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
426432
ref={(el) => (root = el)}
427433
onKeyDown={nav}
428434
header={
429-
<>
435+
<div
436+
data-action="session-question-toggle"
437+
class="flex flex-1 min-w-0 items-center gap-2 cursor-default select-none"
438+
role="button"
439+
tabIndex={0}
440+
style={{ margin: "0 -10px", padding: "0 0 0 10px" }}
441+
onClick={fold}
442+
onKeyDown={(event) => {
443+
if (event.key !== "Enter" && event.key !== " ") return
444+
event.preventDefault()
445+
fold()
446+
}}
447+
>
430448
<div data-slot="question-header-title">{summary()}</div>
431-
<div data-slot="question-progress">
449+
<div data-slot="question-progress" class="ml-auto mr-1">
432450
<For each={questions()}>
433451
{(_, i) => (
434452
<button
@@ -437,13 +455,38 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
437455
data-active={i() === store.tab}
438456
data-answered={answered(i())}
439457
disabled={sending()}
440-
onClick={() => jump(i())}
458+
onMouseDown={(event) => {
459+
event.preventDefault()
460+
event.stopPropagation()
461+
}}
462+
onClick={(event) => {
463+
event.stopPropagation()
464+
jump(i())
465+
}}
441466
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
442467
/>
443468
)}
444469
</For>
445470
</div>
446-
</>
471+
<div>
472+
<IconButton
473+
data-action="session-question-toggle-button"
474+
icon="chevron-down"
475+
size="normal"
476+
variant="ghost"
477+
classList={{ "rotate-180": store.collapsed }}
478+
onMouseDown={(event) => {
479+
event.preventDefault()
480+
event.stopPropagation()
481+
}}
482+
onClick={(event) => {
483+
event.stopPropagation()
484+
fold()
485+
}}
486+
aria-label={store.collapsed ? language.t("session.todo.expand") : language.t("session.todo.collapse")}
487+
/>
488+
</div>
489+
</div>
447490
}
448491
footer={
449492
<>
@@ -469,72 +512,95 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
469512
</>
470513
}
471514
>
472-
<div data-slot="question-text">{question()?.question}</div>
473-
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
474-
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
515+
<div
516+
data-slot="question-text"
517+
class="cursor-default"
518+
classList={{
519+
"mb-6": store.collapsed && selectedCount() === 0,
520+
}}
521+
role={store.collapsed ? "button" : undefined}
522+
tabIndex={store.collapsed ? 0 : undefined}
523+
onClick={fold}
524+
onKeyDown={(event) => {
525+
if (!store.collapsed) return
526+
if (event.key !== "Enter" && event.key !== " ") return
527+
event.preventDefault()
528+
fold()
529+
}}
530+
>
531+
{question()?.question}
532+
</div>
533+
<Show when={store.collapsed && selectedCount() > 0}>
534+
<div data-slot="question-hint" class="cursor-default mb-6">
535+
{selectedCount()} answer{selectedCount() === 1 ? "" : "s"} selected
536+
</div>
475537
</Show>
476-
<div data-slot="question-options">
477-
<For each={options()}>
478-
{(opt, i) => (
479-
<Option
480-
multi={multi()}
481-
picked={picked(opt.label)}
482-
label={opt.label}
483-
description={opt.description}
484-
disabled={sending()}
485-
ref={(el) => (optsRef[i()] = el)}
486-
onFocus={() => setStore("focus", i())}
487-
onClick={() => selectOption(i())}
488-
/>
489-
)}
490-
</For>
491-
492-
<Show
493-
when={store.editing}
494-
fallback={
495-
<button
496-
type="button"
497-
ref={customRef}
538+
<div data-slot="question-answers" hidden={store.collapsed} aria-hidden={store.collapsed}>
539+
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
540+
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
541+
</Show>
542+
<div data-slot="question-options">
543+
<For each={options()}>
544+
{(opt, i) => (
545+
<Option
546+
multi={multi()}
547+
picked={picked(opt.label)}
548+
label={opt.label}
549+
description={opt.description}
550+
disabled={sending()}
551+
ref={(el) => (optsRef[i()] = el)}
552+
onFocus={() => setStore("focus", i())}
553+
onClick={() => selectOption(i())}
554+
/>
555+
)}
556+
</For>
557+
558+
<Show
559+
when={store.editing}
560+
fallback={
561+
<button
562+
type="button"
563+
ref={customRef}
564+
data-slot="question-option"
565+
data-custom="true"
566+
data-picked={on()}
567+
role={multi() ? "checkbox" : "radio"}
568+
aria-checked={on()}
569+
disabled={sending()}
570+
onFocus={() => setStore("focus", options().length)}
571+
onClick={customOpen}
572+
>
573+
<Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
574+
<span data-slot="question-option-main">
575+
<span data-slot="option-label">{customLabel()}</span>
576+
<span data-slot="option-description">{input() || customPlaceholder()}</span>
577+
</span>
578+
</button>
579+
}
580+
>
581+
<form
498582
data-slot="question-option"
499583
data-custom="true"
500584
data-picked={on()}
501585
role={multi() ? "checkbox" : "radio"}
502586
aria-checked={on()}
503-
disabled={sending()}
504-
onFocus={() => setStore("focus", options().length)}
505-
onClick={customOpen}
587+
onMouseDown={(e) => {
588+
if (sending()) {
589+
e.preventDefault()
590+
return
591+
}
592+
if (e.target instanceof HTMLTextAreaElement) return
593+
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
594+
if (input instanceof HTMLTextAreaElement) input.focus()
595+
}}
596+
onSubmit={(e) => {
597+
e.preventDefault()
598+
commitCustom()
599+
}}
506600
>
507601
<Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
508602
<span data-slot="question-option-main">
509603
<span data-slot="option-label">{customLabel()}</span>
510-
<span data-slot="option-description">{input() || customPlaceholder()}</span>
511-
</span>
512-
</button>
513-
}
514-
>
515-
<form
516-
data-slot="question-option"
517-
data-custom="true"
518-
data-picked={on()}
519-
role={multi() ? "checkbox" : "radio"}
520-
aria-checked={on()}
521-
onMouseDown={(e) => {
522-
if (sending()) {
523-
e.preventDefault()
524-
return
525-
}
526-
if (e.target instanceof HTMLTextAreaElement) return
527-
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
528-
if (input instanceof HTMLTextAreaElement) input.focus()
529-
}}
530-
onSubmit={(e) => {
531-
e.preventDefault()
532-
commitCustom()
533-
}}
534-
>
535-
<Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
536-
<span data-slot="question-option-main">
537-
<span data-slot="option-label">{customLabel()}</span>
538604
<textarea
539605
ref={focusCustom}
540606
data-slot="question-custom-input"

packages/ui/src/components/message-part.css

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,7 @@
837837
[data-slot="question-body"] {
838838
display: flex;
839839
flex-direction: column;
840-
gap: 16px;
840+
gap: 0;
841841
flex: 1;
842842
min-height: 0;
843843
padding: 8px 8px 0;
@@ -917,7 +917,7 @@
917917
font-weight: var(--font-weight-medium);
918918
line-height: var(--line-height-large);
919919
color: var(--text-strong);
920-
padding: 0 10px;
920+
padding: 16px 10px 0;
921921
}
922922

923923
[data-slot="question-hint"] {
@@ -1060,8 +1060,26 @@
10601060
line-height: var(--line-height-large);
10611061
color: var(--text-base);
10621062
min-width: 0;
1063-
overflow-wrap: anywhere;
1064-
white-space: normal;
1063+
overflow: hidden;
1064+
text-overflow: ellipsis;
1065+
white-space: nowrap;
1066+
}
1067+
1068+
[data-slot="question-option"][data-custom="true"] {
1069+
[data-slot="option-description"] {
1070+
overflow: visible;
1071+
text-overflow: clip;
1072+
white-space: normal;
1073+
overflow-wrap: anywhere;
1074+
}
1075+
1076+
&[data-picked="true"] {
1077+
[data-slot="question-custom-input"]:focus-visible {
1078+
outline: none;
1079+
outline-offset: 0;
1080+
border-radius: 0;
1081+
}
1082+
}
10651083
}
10661084

10671085
[data-slot="question-custom"] {

0 commit comments

Comments
 (0)