Skip to content

Commit da3756a

Browse files
Apply PR #15697: tweak(ui): make questions popup collapsible
2 parents 17b0214 + 611e616 commit da3756a

File tree

2 files changed

+189
-91
lines changed

2 files changed

+189
-91
lines changed

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

Lines changed: 167 additions & 87 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()
@@ -247,7 +252,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
247252
return store.customOn[i] === true && (store.custom[i] ?? "").trim().length > 0
248253
}
249254

250-
const picked = (answer: string) => store.answers[store.tab]?.includes(answer) ?? false
255+
const isPicked = (answer: string) => store.answers[store.tab]?.includes(answer) ?? false
251256

252257
const pick = (answer: string, custom: boolean = false) => {
253258
setStore("answers", store.tab, [answer])
@@ -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,99 +511,137 @@ 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={isPicked(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>
538-
<textarea
539-
ref={focusCustom}
540-
data-slot="question-custom-input"
541-
placeholder={customPlaceholder()}
542-
value={input()}
543-
rows={1}
544-
disabled={sending()}
545-
onKeyDown={(e) => {
546-
if (e.key === "Escape") {
617+
<textarea
618+
ref={focusCustom}
619+
data-slot="question-custom-input"
620+
placeholder={customPlaceholder()}
621+
value={input()}
622+
rows={1}
623+
disabled={sending()}
624+
onKeyDown={(e) => {
625+
if (e.key === "Escape") {
626+
e.preventDefault()
627+
setStore("editing", false)
628+
focus(options().length)
629+
return
630+
}
631+
if ((e.metaKey || e.ctrlKey) && !e.altKey) return
632+
if (e.key !== "Enter" || e.shiftKey) return
547633
e.preventDefault()
548-
setStore("editing", false)
549-
focus(options().length)
550-
return
551-
}
552-
if ((e.metaKey || e.ctrlKey) && !e.altKey) return
553-
if (e.key !== "Enter" || e.shiftKey) return
554-
e.preventDefault()
555-
commitCustom()
556-
}}
557-
onInput={(e) => {
558-
customUpdate(e.currentTarget.value)
559-
resizeInput(e.currentTarget)
560-
}}
561-
/>
562-
</span>
563-
</form>
564-
</Show>
634+
commitCustom()
635+
}}
636+
onInput={(e) => {
637+
customUpdate(e.currentTarget.value)
638+
resizeInput(e.currentTarget)
639+
}}
640+
/>
641+
</span>
642+
</form>
643+
</Show>
644+
</div>
565645
</div>
566646
</DockPrompt>
567647
)

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)