From 2c17a980ffdc019d46b9e48a22bf719c009075e0 Mon Sep 17 00:00:00 2001
From: David Hill <iamdavidhill@gmail.com>
Date: Tue, 17 Feb 2026 17:06:21 +0000
Subject: refactor(ui): extract dock prompt shell

---
 packages/app/src/components/question-dock.tsx | 316 +++++++++++++-------------
 1 file changed, 158 insertions(+), 158 deletions(-)

(limited to 'packages/app/src/components/question-dock.tsx')

diff --git a/packages/app/src/components/question-dock.tsx b/packages/app/src/components/question-dock.tsx
index 1a0bbbe97..cd2e495b1 100644
--- a/packages/app/src/components/question-dock.tsx
+++ b/packages/app/src/components/question-dock.tsx
@@ -1,6 +1,7 @@
 import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid-js"
 import { createStore } from "solid-js/store"
 import { Button } from "@opencode-ai/ui/button"
+import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
 import { Icon } from "@opencode-ai/ui/icon"
 import { showToast } from "@opencode-ai/ui/toast"
 import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
@@ -232,9 +233,11 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
   }
 
   return (
-    <div data-component="question-prompt" ref={(el) => (root = el)}>
-      <div data-slot="question-body">
-        <div data-slot="question-header">
+    <DockPrompt
+      kind="question"
+      ref={(el) => (root = el)}
+      header={
+        <>
           <div data-slot="question-header-title">{summary()}</div>
           <div data-slot="question-progress">
             <For each={questions()}>
@@ -254,172 +257,169 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
               )}
             </For>
           </div>
-        </div>
-
-        <div data-slot="question-content">
-          <div data-slot="question-text">{question()?.question}</div>
-          <Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
-            <div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
-          </Show>
-          <div data-slot="question-options">
-            <For each={options()}>
-              {(opt, i) => {
-                const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
-                return (
-                  <button
-                    data-slot="question-option"
-                    data-picked={picked()}
-                    role={multi() ? "checkbox" : "radio"}
-                    aria-checked={picked()}
-                    disabled={store.sending}
-                    onClick={() => selectOption(i())}
-                  >
-                    <span data-slot="question-option-check" aria-hidden="true">
-                      <span
-                        data-slot="question-option-box"
-                        data-type={multi() ? "checkbox" : "radio"}
-                        data-picked={picked()}
-                      >
-                        <Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
-                          <Icon name="check-small" size="small" />
-                        </Show>
-                      </span>
-                    </span>
-                    <span data-slot="question-option-main">
-                      <span data-slot="option-label">{opt.label}</span>
-                      <Show when={opt.description}>
-                        <span data-slot="option-description">{opt.description}</span>
-                      </Show>
-                    </span>
-                  </button>
-                )
-              }}
-            </For>
-
-            <Show
-              when={store.editing}
-              fallback={
-                <button
-                  data-slot="question-option"
-                  data-custom="true"
-                  data-picked={on()}
-                  role={multi() ? "checkbox" : "radio"}
-                  aria-checked={on()}
-                  disabled={store.sending}
-                  onClick={customOpen}
-                >
+        </>
+      }
+      footer={
+        <>
+          <Button variant="ghost" size="large" disabled={store.sending} onClick={reject}>
+            {language.t("ui.common.dismiss")}
+          </Button>
+          <div data-slot="question-footer-actions">
+            <Show when={store.tab > 0}>
+              <Button variant="secondary" size="large" disabled={store.sending} onClick={back}>
+                {language.t("ui.common.back")}
+              </Button>
+            </Show>
+            <Button variant={last() ? "primary" : "secondary"} size="large" disabled={store.sending} onClick={next}>
+              {last() ? language.t("ui.common.submit") : language.t("ui.common.next")}
+            </Button>
+          </div>
+        </>
+      }
+    >
+      <div data-slot="question-text">{question()?.question}</div>
+      <Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
+        <div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
+      </Show>
+      <div data-slot="question-options">
+        <For each={options()}>
+          {(opt, i) => {
+            const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
+            return (
+              <button
+                data-slot="question-option"
+                data-picked={picked()}
+                role={multi() ? "checkbox" : "radio"}
+                aria-checked={picked()}
+                disabled={store.sending}
+                onClick={() => selectOption(i())}
+              >
+                <span data-slot="question-option-check" aria-hidden="true">
                   <span
-                    data-slot="question-option-check"
-                    aria-hidden="true"
-                    onClick={(e) => {
-                      e.preventDefault()
-                      e.stopPropagation()
-                      customToggle()
-                    }}
+                    data-slot="question-option-box"
+                    data-type={multi() ? "checkbox" : "radio"}
+                    data-picked={picked()}
                   >
-                    <span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
-                      <Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
-                        <Icon name="check-small" size="small" />
-                      </Show>
-                    </span>
-                  </span>
-                  <span data-slot="question-option-main">
-                    <span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
-                    <span data-slot="option-description">
-                      {input() || language.t("ui.question.custom.placeholder")}
-                    </span>
+                    <Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
+                      <Icon name="check-small" size="small" />
+                    </Show>
                   </span>
-                </button>
+                </span>
+                <span data-slot="question-option-main">
+                  <span data-slot="option-label">{opt.label}</span>
+                  <Show when={opt.description}>
+                    <span data-slot="option-description">{opt.description}</span>
+                  </Show>
+                </span>
+              </button>
+            )
+          }}
+        </For>
+
+        <Show
+          when={store.editing}
+          fallback={
+            <button
+              data-slot="question-option"
+              data-custom="true"
+              data-picked={on()}
+              role={multi() ? "checkbox" : "radio"}
+              aria-checked={on()}
+              disabled={store.sending}
+              onClick={customOpen}
+            >
+              <span
+                data-slot="question-option-check"
+                aria-hidden="true"
+                onClick={(e) => {
+                  e.preventDefault()
+                  e.stopPropagation()
+                  customToggle()
+                }}
+              >
+                <span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
+                  <Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
+                    <Icon name="check-small" size="small" />
+                  </Show>
+                </span>
+              </span>
+              <span data-slot="question-option-main">
+                <span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
+                <span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
+              </span>
+            </button>
+          }
+        >
+          <form
+            data-slot="question-option"
+            data-custom="true"
+            data-picked={on()}
+            role={multi() ? "checkbox" : "radio"}
+            aria-checked={on()}
+            onMouseDown={(e) => {
+              if (store.sending) {
+                e.preventDefault()
+                return
               }
+              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()
+            }}
+          >
+            <span
+              data-slot="question-option-check"
+              aria-hidden="true"
+              onClick={(e) => {
+                e.preventDefault()
+                e.stopPropagation()
+                customToggle()
+              }}
             >
-              <form
-                data-slot="question-option"
-                data-custom="true"
-                data-picked={on()}
-                role={multi() ? "checkbox" : "radio"}
-                aria-checked={on()}
-                onMouseDown={(e) => {
-                  if (store.sending) {
+              <span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
+                <Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
+                  <Icon name="check-small" size="small" />
+                </Show>
+              </span>
+            </span>
+            <span data-slot="question-option-main">
+              <span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
+              <textarea
+                ref={(el) =>
+                  setTimeout(() => {
+                    el.focus()
+                    el.style.height = "0px"
+                    el.style.height = `${el.scrollHeight}px`
+                  }, 0)
+                }
+                data-slot="question-custom-input"
+                placeholder={language.t("ui.question.custom.placeholder")}
+                value={input()}
+                rows={1}
+                disabled={store.sending}
+                onKeyDown={(e) => {
+                  if (e.key === "Escape") {
                     e.preventDefault()
+                    setStore("editing", false)
                     return
                   }
-                  if (e.target instanceof HTMLTextAreaElement) return
-                  const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
-                  if (input instanceof HTMLTextAreaElement) input.focus()
-                }}
-                onSubmit={(e) => {
+                  if (e.key !== "Enter" || e.shiftKey) return
                   e.preventDefault()
                   commitCustom()
                 }}
-              >
-                <span
-                  data-slot="question-option-check"
-                  aria-hidden="true"
-                  onClick={(e) => {
-                    e.preventDefault()
-                    e.stopPropagation()
-                    customToggle()
-                  }}
-                >
-                  <span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
-                    <Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
-                      <Icon name="check-small" size="small" />
-                    </Show>
-                  </span>
-                </span>
-                <span data-slot="question-option-main">
-                  <span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
-                  <textarea
-                    ref={(el) =>
-                      setTimeout(() => {
-                        el.focus()
-                        el.style.height = "0px"
-                        el.style.height = `${el.scrollHeight}px`
-                      }, 0)
-                    }
-                    data-slot="question-custom-input"
-                    placeholder={language.t("ui.question.custom.placeholder")}
-                    value={input()}
-                    rows={1}
-                    disabled={store.sending}
-                    onKeyDown={(e) => {
-                      if (e.key === "Escape") {
-                        e.preventDefault()
-                        setStore("editing", false)
-                        return
-                      }
-                      if (e.key !== "Enter" || e.shiftKey) return
-                      e.preventDefault()
-                      commitCustom()
-                    }}
-                    onInput={(e) => {
-                      customUpdate(e.currentTarget.value)
-                      e.currentTarget.style.height = "0px"
-                      e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
-                    }}
-                  />
-                </span>
-              </form>
-            </Show>
-          </div>
-        </div>
-      </div>
-
-      <div data-slot="question-footer">
-        <Button variant="ghost" size="large" disabled={store.sending} onClick={reject}>
-          {language.t("ui.common.dismiss")}
-        </Button>
-        <div data-slot="question-footer-actions">
-          <Show when={store.tab > 0}>
-            <Button variant="secondary" size="large" disabled={store.sending} onClick={back}>
-              {language.t("ui.common.back")}
-            </Button>
-          </Show>
-          <Button variant={last() ? "primary" : "secondary"} size="large" disabled={store.sending} onClick={next}>
-            {last() ? language.t("ui.common.submit") : language.t("ui.common.next")}
-          </Button>
-        </div>
+                onInput={(e) => {
+                  customUpdate(e.currentTarget.value)
+                  e.currentTarget.style.height = "0px"
+                  e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
+                }}
+              />
+            </span>
+          </form>
+        </Show>
       </div>
-    </div>
+    </DockPrompt>
   )
 }
-- 
cgit v1.2.3

