refactor(ui): extract dock prompt shell

This commit is contained in:
David Hill
2026-02-17 17:06:21 +00:00
parent b784c923a8
commit 2c17a980ff
4 changed files with 336 additions and 230 deletions

View File

@@ -1,6 +1,7 @@
import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid-js" import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2" import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
@@ -232,9 +233,11 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
} }
return ( return (
<div data-component="question-prompt" ref={(el) => (root = el)}> <DockPrompt
<div data-slot="question-body"> kind="question"
<div data-slot="question-header"> ref={(el) => (root = el)}
header={
<>
<div data-slot="question-header-title">{summary()}</div> <div data-slot="question-header-title">{summary()}</div>
<div data-slot="question-progress"> <div data-slot="question-progress">
<For each={questions()}> <For each={questions()}>
@@ -254,9 +257,26 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
)} )}
</For> </For>
</div> </div>
</>
}
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>
</>
<div data-slot="question-content"> }
>
<div data-slot="question-text">{question()?.question}</div> <div data-slot="question-text">{question()?.question}</div>
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</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> <div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
@@ -325,9 +345,7 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
</span> </span>
<span data-slot="question-option-main"> <span data-slot="question-option-main">
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span> <span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
<span data-slot="option-description"> <span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
{input() || language.t("ui.question.custom.placeholder")}
</span>
</span> </span>
</button> </button>
} }
@@ -402,24 +420,6 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
</form> </form>
</Show> </Show>
</div> </div>
</div> </DockPrompt>
</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>
</div>
</div>
) )
} }

View File

@@ -1,6 +1,7 @@
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js" import { For, Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
import type { QuestionRequest, Todo } from "@opencode-ai/sdk/v2" import type { QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
import { PromptInput } from "@/components/prompt-input" import { PromptInput } from "@/components/prompt-input"
import { QuestionDock } from "@/components/question-dock" import { QuestionDock } from "@/components/question-dock"
import { SessionTodoDock } from "@/components/session-todo-dock" import { SessionTodoDock } from "@/components/session-todo-dock"
@@ -139,37 +140,17 @@ export function SessionPromptDock(props: {
return ( return (
<div> <div>
<div data-component="question-prompt" data-permission="true"> <DockPrompt
<div data-slot="question-body"> kind="permission"
<div data-slot="question-header"> header={
<div data-slot="question-header-title"> <>
{props.t("notification.permission.title")}{" "} <div data-slot="permission-header-title">{props.t("notification.permission.title")}</div>
<span class="text-13-regular text-text-weak">{toolTitle()}</span> </>
</div> }
</div> footer={
<>
<div data-slot="question-content">
<Show when={toolDescription()}>
<div data-slot="question-hint">{toolDescription()}</div>
</Show>
<Show when={perm.patterns.length > 0}>
<div data-slot="question-options">
<For each={perm.patterns}>
{(pattern) => (
<div class="px-[10px]">
<code class="text-12-regular text-text-base break-all">{pattern}</code>
</div>
)}
</For>
</div>
</Show>
</div>
</div>
<div data-slot="question-footer">
<div /> <div />
<div data-slot="question-footer-actions"> <div data-slot="permission-footer-actions">
<Button <Button
variant="ghost" variant="ghost"
size="normal" size="normal"
@@ -195,8 +176,21 @@ export function SessionPromptDock(props: {
{props.t("ui.permission.allowOnce")} {props.t("ui.permission.allowOnce")}
</Button> </Button>
</div> </div>
</>
}
>
<Show when={toolDescription()}>
<div data-slot="permission-hint">{toolDescription()}</div>
</Show>
<Show when={perm.patterns.length > 0}>
<div data-slot="permission-patterns">
<For each={perm.patterns}>
{(pattern) => <code class="text-12-regular text-text-base break-all">{pattern}</code>}
</For>
</div> </div>
</div> </Show>
</DockPrompt>
</div> </div>
) )
}} }}

View File

@@ -0,0 +1,21 @@
import type { JSX } from "solid-js"
export function DockPrompt(props: {
kind: "question" | "permission"
header: JSX.Element
children: JSX.Element
footer: JSX.Element
ref?: (el: HTMLDivElement) => void
}) {
const slot = (name: string) => `${props.kind}-${name}`
return (
<div data-component="dock-prompt" data-kind={props.kind} ref={props.ref}>
<div data-slot={slot("body")}>
<div data-slot={slot("header")}>{props.header}</div>
<div data-slot={slot("content")}>{props.children}</div>
</div>
<div data-slot={slot("footer")}>{props.footer}</div>
</div>
)
}

View File

@@ -753,23 +753,106 @@
} }
} }
[data-component="question-prompt"] { [data-component="dock-prompt"][data-kind="permission"] {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0; gap: 0;
min-height: 0; min-height: 0;
max-height: var(--question-prompt-max-height, 100dvh); max-height: 100dvh;
[data-slot="permission-body"] {
display: flex;
flex-direction: column;
gap: 16px;
flex: 1;
min-height: 0;
padding: 8px 8px 0;
background-color: var(--surface-raised-stronger-non-alpha);
border-radius: 12px;
box-shadow: var(--shadow-xs-border);
overflow: clip;
position: relative;
z-index: 10;
}
[data-slot="permission-header"] {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 0 10px;
}
[data-slot="permission-header-title"] {
font-family: var(--font-family-sans);
font-size: 14px;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
color: var(--text-strong);
min-width: 0;
}
[data-slot="permission-content"] {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
min-height: 0;
}
[data-slot="permission-hint"] {
font-family: var(--font-family-sans);
font-size: 14px;
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large);
color: var(--text-weak);
padding: 0 10px;
}
[data-slot="permission-patterns"] {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 8px;
margin-bottom: 16px;
padding: 1px 10px 8px;
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
&[data-permission="true"] {
[data-slot="question-options"] {
code { code {
font-size: 14px; font-size: 14px;
line-height: var(--line-height-large); line-height: var(--line-height-large);
} }
} }
[data-slot="question-footer-actions"] { [data-slot="permission-footer"] {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
padding: 32px 8px 8px;
background-color: var(--background-base);
border: 1px solid var(--border-weak-base);
border-radius: 12px;
overflow: clip;
margin-top: -24px;
position: relative;
z-index: 0;
}
[data-slot="permission-footer-actions"] {
display: flex;
align-items: center;
gap: 8px;
[data-component="button"] { [data-component="button"] {
padding-left: 12px; padding-left: 12px;
padding-right: 12px; padding-right: 12px;
@@ -777,6 +860,14 @@
} }
} }
:is([data-component="question-prompt"], [data-component="dock-prompt"][data-kind="question"]) {
position: relative;
display: flex;
flex-direction: column;
gap: 0;
min-height: 0;
max-height: var(--question-prompt-max-height, 100dvh);
[data-slot="question-body"] { [data-slot="question-body"] {
display: flex; display: flex;
flex-direction: column; flex-direction: column;