From 49cc872c4415f081b4208d16fd0d85e425a75eed Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:02:45 -0600 Subject: chore: refactor composer/dock components (#14328) --- packages/app/src/components/prompt-input.tsx | 12 +- packages/app/src/components/question-dock.tsx | 425 --------------------- packages/app/src/components/session-todo-dock.tsx | 208 ---------- packages/app/src/pages/session.tsx | 13 +- packages/app/src/pages/session/composer/index.ts | 3 + .../session/composer/session-composer-region.tsx | 124 ++++++ .../session/composer/session-composer-state.ts | 158 ++++++++ .../session/composer/session-permission-dock.tsx | 74 ++++ .../session/composer/session-question-dock.tsx | 425 +++++++++++++++++++++ .../pages/session/composer/session-todo-dock.tsx | 211 ++++++++++ .../app/src/pages/session/session-prompt-dock.tsx | 318 --------------- 11 files changed, 1006 insertions(+), 965 deletions(-) delete mode 100644 packages/app/src/components/question-dock.tsx delete mode 100644 packages/app/src/components/session-todo-dock.tsx create mode 100644 packages/app/src/pages/session/composer/index.ts create mode 100644 packages/app/src/pages/session/composer/session-composer-region.tsx create mode 100644 packages/app/src/pages/session/composer/session-composer-state.ts create mode 100644 packages/app/src/pages/session/composer/session-permission-dock.tsx create mode 100644 packages/app/src/pages/session/composer/session-question-dock.tsx create mode 100644 packages/app/src/pages/session/composer/session-todo-dock.tsx delete mode 100644 packages/app/src/pages/session/session-prompt-dock.tsx (limited to 'packages/app/src') diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 8d97fccea..0777bacc7 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -20,6 +20,7 @@ import { useParams } from "@solidjs/router" import { useSync } from "@/context/sync" import { useComments } from "@/context/comments" import { Button } from "@opencode-ai/ui/button" +import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface" import { Icon } from "@opencode-ai/ui/icon" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import type { IconName } from "@opencode-ai/ui/icons/provider" @@ -1045,12 +1046,11 @@ export const PromptInput: Component = (props) => { commandKeybind={command.keybind} t={(key) => language.t(key as Parameters[0])} /> -
= (props) => { -
+ -
+
@@ -1385,7 +1385,7 @@ export const PromptInput: Component = (props) => { />
-
+
) diff --git a/packages/app/src/components/question-dock.tsx b/packages/app/src/components/question-dock.tsx deleted file mode 100644 index cd2e495b1..000000000 --- a/packages/app/src/components/question-dock.tsx +++ /dev/null @@ -1,425 +0,0 @@ -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" -import { useLanguage } from "@/context/language" -import { useSDK } from "@/context/sdk" - -export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => { - const sdk = useSDK() - const language = useLanguage() - - const questions = createMemo(() => props.request.questions) - const total = createMemo(() => questions().length) - - const [store, setStore] = createStore({ - tab: 0, - answers: [] as QuestionAnswer[], - custom: [] as string[], - customOn: [] as boolean[], - editing: false, - sending: false, - }) - - let root: HTMLDivElement | undefined - - const question = createMemo(() => questions()[store.tab]) - const options = createMemo(() => question()?.options ?? []) - const input = createMemo(() => store.custom[store.tab] ?? "") - const on = createMemo(() => store.customOn[store.tab] === true) - const multi = createMemo(() => question()?.multiple === true) - - const summary = createMemo(() => { - const n = Math.min(store.tab + 1, total()) - return `${n} of ${total()} questions` - }) - - const last = createMemo(() => store.tab >= total() - 1) - - const customUpdate = (value: string, selected: boolean = on()) => { - const prev = input().trim() - const next = value.trim() - - setStore("custom", store.tab, value) - if (!selected) return - - if (multi()) { - setStore("answers", store.tab, (current = []) => { - const removed = prev ? current.filter((item) => item.trim() !== prev) : current - if (!next) return removed - if (removed.some((item) => item.trim() === next)) return removed - return [...removed, next] - }) - return - } - - setStore("answers", store.tab, next ? [next] : []) - } - - const measure = () => { - if (!root) return - - const scroller = document.querySelector(".session-scroller") - const head = scroller instanceof HTMLElement ? scroller.firstElementChild : undefined - const top = - head instanceof HTMLElement && head.classList.contains("sticky") ? head.getBoundingClientRect().bottom : 0 - if (!top) { - root.style.removeProperty("--question-prompt-max-height") - return - } - - const dock = root.closest('[data-component="session-prompt-dock"]') - if (!(dock instanceof HTMLElement)) return - - const dockBottom = dock.getBoundingClientRect().bottom - const below = Math.max(0, dockBottom - root.getBoundingClientRect().bottom) - const gap = 8 - const max = Math.max(240, Math.floor(dockBottom - top - gap - below)) - root.style.setProperty("--question-prompt-max-height", `${max}px`) - } - - onMount(() => { - let raf: number | undefined - const update = () => { - if (raf !== undefined) cancelAnimationFrame(raf) - raf = requestAnimationFrame(() => { - raf = undefined - measure() - }) - } - - update() - window.addEventListener("resize", update) - - const dock = root?.closest('[data-component="session-prompt-dock"]') - const scroller = document.querySelector(".session-scroller") - const observer = new ResizeObserver(update) - if (dock instanceof HTMLElement) observer.observe(dock) - if (scroller instanceof HTMLElement) observer.observe(scroller) - - onCleanup(() => { - window.removeEventListener("resize", update) - observer.disconnect() - if (raf !== undefined) cancelAnimationFrame(raf) - }) - }) - - const fail = (err: unknown) => { - const message = err instanceof Error ? err.message : String(err) - showToast({ title: language.t("common.requestFailed"), description: message }) - } - - const reply = async (answers: QuestionAnswer[]) => { - if (store.sending) return - - setStore("sending", true) - try { - await sdk.client.question.reply({ requestID: props.request.id, answers }) - } catch (err) { - fail(err) - } finally { - setStore("sending", false) - } - } - - const reject = async () => { - if (store.sending) return - - setStore("sending", true) - try { - await sdk.client.question.reject({ requestID: props.request.id }) - } catch (err) { - fail(err) - } finally { - setStore("sending", false) - } - } - - const submit = () => void reply(questions().map((_, i) => store.answers[i] ?? [])) - - const pick = (answer: string, custom: boolean = false) => { - setStore("answers", store.tab, [answer]) - if (custom) setStore("custom", store.tab, answer) - if (!custom) setStore("customOn", store.tab, false) - setStore("editing", false) - } - - const toggle = (answer: string) => { - setStore("answers", store.tab, (current = []) => { - if (current.includes(answer)) return current.filter((item) => item !== answer) - return [...current, answer] - }) - } - - const customToggle = () => { - if (store.sending) return - - if (!multi()) { - setStore("customOn", store.tab, true) - setStore("editing", true) - customUpdate(input(), true) - return - } - - const next = !on() - setStore("customOn", store.tab, next) - if (next) { - setStore("editing", true) - customUpdate(input(), true) - return - } - - const value = input().trim() - if (value) setStore("answers", store.tab, (current = []) => current.filter((item) => item.trim() !== value)) - setStore("editing", false) - } - - const customOpen = () => { - if (store.sending) return - if (!on()) setStore("customOn", store.tab, true) - setStore("editing", true) - customUpdate(input(), true) - } - - const selectOption = (optIndex: number) => { - if (store.sending) return - - if (optIndex === options().length) { - customOpen() - return - } - - const opt = options()[optIndex] - if (!opt) return - if (multi()) { - toggle(opt.label) - return - } - pick(opt.label) - } - - const commitCustom = () => { - setStore("editing", false) - customUpdate(input()) - } - - const next = () => { - if (store.sending) return - if (store.editing) commitCustom() - - if (store.tab >= total() - 1) { - submit() - return - } - - setStore("tab", store.tab + 1) - setStore("editing", false) - } - - const back = () => { - if (store.sending) return - if (store.tab <= 0) return - setStore("tab", store.tab - 1) - setStore("editing", false) - } - - const jump = (tab: number) => { - if (store.sending) return - setStore("tab", tab) - setStore("editing", false) - } - - return ( - (root = el)} - header={ - <> -
{summary()}
-
- - {(_, i) => ( -
- - } - footer={ - <> - -
- 0}> - - - -
- - } - > -
{question()?.question}
- {language.t("ui.question.singleHint")}}> -
{language.t("ui.question.multiHint")}
-
-
- - {(opt, i) => { - const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false - return ( - - ) - }} - - - - - - {language.t("ui.messagePart.option.typeOwnAnswer")} - {input() || language.t("ui.question.custom.placeholder")} - - - } - > -
{ - 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() - }} - > - - - {language.t("ui.messagePart.option.typeOwnAnswer")} -