Skip to content

Commit 11cf4f0

Browse files
Apply PR #15697: tweak(ui): make questions popup collapsible
2 parents 6cce4b5 + 611e616 commit 11cf4f0

File tree

2 files changed

+152
-62
lines changed

2 files changed

+152
-62
lines changed

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

Lines changed: 140 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,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
7374
customOn: cached?.customOn ?? ([] as boolean[]),
7475
editing: false,
7576
focus: 0,
77+
collapsed: false,
7678
})
7779

7880
let root: HTMLDivElement | undefined
@@ -87,6 +89,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
8789
const on = createMemo(() => store.customOn[store.tab] === true)
8890
const multi = createMemo(() => question()?.multiple === true)
8991
const count = createMemo(() => options().length + 1)
92+
const pickedCount = createMemo(() => store.answers[store.tab]?.length ?? 0)
9093

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

99102
const last = createMemo(() => store.tab >= total() - 1)
100103

104+
const fold = () => setStore("collapsed", (value) => !value)
105+
101106
const customUpdate = (value: string, selected: boolean = on()) => {
102107
const prev = input().trim()
103108
const next = value.trim()
@@ -426,9 +431,21 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
426431
ref={(el) => (root = el)}
427432
onKeyDown={nav}
428433
header={
429-
<>
434+
<div
435+
data-action="session-question-toggle"
436+
class="flex flex-1 min-w-0 items-center gap-2 cursor-default select-none"
437+
role="button"
438+
tabIndex={0}
439+
style={{ margin: "0 -10px", padding: "0 0 0 10px" }}
440+
onClick={fold}
441+
onKeyDown={(event) => {
442+
if (event.key !== "Enter" && event.key !== " ") return
443+
event.preventDefault()
444+
fold()
445+
}}
446+
>
430447
<div data-slot="question-header-title">{summary()}</div>
431-
<div data-slot="question-progress">
448+
<div data-slot="question-progress" class="ml-auto mr-1">
432449
<For each={questions()}>
433450
{(_, i) => (
434451
<button
@@ -437,13 +454,38 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
437454
data-active={i() === store.tab}
438455
data-answered={answered(i())}
439456
disabled={sending()}
440-
onClick={() => jump(i())}
457+
onMouseDown={(event) => {
458+
event.preventDefault()
459+
event.stopPropagation()
460+
}}
461+
onClick={(event) => {
462+
event.stopPropagation()
463+
jump(i())
464+
}}
441465
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
442466
/>
443467
)}
444468
</For>
445469
</div>
446-
</>
470+
<div>
471+
<IconButton
472+
data-action="session-question-toggle-button"
473+
icon="chevron-down"
474+
size="normal"
475+
variant="ghost"
476+
classList={{ "rotate-180": store.collapsed }}
477+
onMouseDown={(event) => {
478+
event.preventDefault()
479+
event.stopPropagation()
480+
}}
481+
onClick={(event) => {
482+
event.stopPropagation()
483+
fold()
484+
}}
485+
aria-label={store.collapsed ? language.t("session.todo.expand") : language.t("session.todo.collapse")}
486+
/>
487+
</div>
488+
</div>
447489
}
448490
footer={
449491
<>
@@ -469,72 +511,109 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
469511
</>
470512
}
471513
>
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>
514+
<div
515+
data-slot="question-text"
516+
class="cursor-default"
517+
classList={{
518+
"mb-6": store.collapsed && pickedCount() === 0,
519+
}}
520+
role={store.collapsed ? "button" : undefined}
521+
tabIndex={store.collapsed ? 0 : undefined}
522+
onClick={fold}
523+
onKeyDown={(event) => {
524+
if (!store.collapsed) return
525+
if (event.key !== "Enter" && event.key !== " ") return
526+
event.preventDefault()
527+
fold()
528+
}}
529+
>
530+
{question()?.question}
531+
</div>
532+
<Show when={store.collapsed && pickedCount() > 0}>
533+
<div data-slot="question-hint" class="cursor-default mb-6">
534+
{pickedCount()} answer{pickedCount() === 1 ? "" : "s"} selected
535+
</div>
475536
</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}
537+
<div data-slot="question-answers" hidden={store.collapsed} aria-hidden={store.collapsed}>
538+
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
539+
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
540+
</Show>
541+
<div data-slot="question-options">
542+
<For each={options()}>
543+
{(opt, i) => (
544+
<Option
545+
multi={multi()}
546+
picked={picked(opt.label)}
547+
label={opt.label}
548+
description={opt.description}
549+
disabled={sending()}
550+
ref={(el) => (optsRef[i()] = el)}
551+
onFocus={() => setStore("focus", i())}
552+
onClick={() => selectOption(i())}
553+
/>
554+
)}
555+
</For>
556+
557+
<Show
558+
when={store.editing}
559+
fallback={
560+
<button
561+
type="button"
562+
ref={customRef}
563+
data-slot="question-option"
564+
data-custom="true"
565+
data-picked={on()}
566+
role={multi() ? "checkbox" : "radio"}
567+
aria-checked={on()}
568+
disabled={sending()}
569+
onFocus={() => setStore("focus", options().length)}
570+
onClick={customOpen}
571+
>
572+
<span
573+
data-slot="question-option-check"
574+
aria-hidden="true"
575+
onClick={(e) => {
576+
e.preventDefault()
577+
e.stopPropagation()
578+
customToggle()
579+
}}
580+
>
581+
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
582+
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
583+
<Icon name="check-small" size="small" />
584+
</Show>
585+
</span>
586+
</span>
587+
<span data-slot="question-option-main">
588+
<span data-slot="option-label">{customLabel()}</span>
589+
<span data-slot="option-description">{input() || customPlaceholder()}</span>
590+
</span>
591+
</button>
592+
}
593+
>
594+
<form
498595
data-slot="question-option"
499596
data-custom="true"
500597
data-picked={on()}
501598
role={multi() ? "checkbox" : "radio"}
502599
aria-checked={on()}
503-
disabled={sending()}
504-
onFocus={() => setStore("focus", options().length)}
505-
onClick={customOpen}
600+
onMouseDown={(e) => {
601+
if (sending()) {
602+
e.preventDefault()
603+
return
604+
}
605+
if (e.target instanceof HTMLTextAreaElement) return
606+
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
607+
if (input instanceof HTMLTextAreaElement) input.focus()
608+
}}
609+
onSubmit={(e) => {
610+
e.preventDefault()
611+
commitCustom()
612+
}}
506613
>
507614
<Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
508615
<span data-slot="question-option-main">
509616
<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>
538617
<textarea
539618
ref={focusCustom}
540619
data-slot="question-custom-input"
@@ -562,6 +641,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
562641
</span>
563642
</form>
564643
</Show>
644+
</div>
565645
</div>
566646
</DockPrompt>
567647
)

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

Lines changed: 12 additions & 2 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"] {
@@ -1064,6 +1064,16 @@
10641064
white-space: normal;
10651065
}
10661066

1067+
[data-slot="question-option"][data-custom="true"] {
1068+
&[data-picked="true"] {
1069+
[data-slot="question-custom-input"]:focus-visible {
1070+
outline: none;
1071+
outline-offset: 0;
1072+
border-radius: 0;
1073+
}
1074+
}
1075+
}
1076+
10671077
[data-slot="question-custom"] {
10681078
display: flex;
10691079
flex-direction: column;

0 commit comments

Comments
 (0)