From 9f453cbf882d5e778ada6f6b42380291be0c3076 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Wed, 3 Jun 2026 10:47:04 +0800 Subject: [PATCH] refactor(app): extract prompt-input action bar to action-bar.tsx Move the composer's bottom action bar (attach button, model control, workspace chip, context-usage, send/stop button) out of prompt-input.tsx into a new PromptActionBar component in prompt-input/action-bar.tsx. The button-reveal spring, its motion style helper, the buttons memo, and the send/stop tooltip move with it since they serve only the action bar. Pure extraction: no behavior, DOM, aria, copy, or storage-key change. prompt-input.tsx drops from 572 to 496 lines. --- packages/app/src/components/prompt-input.tsx | 106 +++------------ .../components/prompt-input/action-bar.tsx | 125 ++++++++++++++++++ 2 files changed, 140 insertions(+), 91 deletions(-) create mode 100644 packages/app/src/components/prompt-input/action-bar.tsx diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index cb9867b07..060352424 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1,4 +1,3 @@ -import { useSpring } from "@opencode-ai/ui/motion-spring" import { useNavigate, useParams } from "@solidjs/router" import { createEffect, on, Component, For, Show, createMemo, createSignal } from "solid-js" import { createStore } from "solid-js/store" @@ -9,14 +8,8 @@ import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" import { useComments } from "@/context/comments" import { DockSegmentForm } from "@opencode-ai/ui/dock-card" -import { Icon } from "@opencode-ai/ui/icon" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { useDialog } from "@opencode-ai/ui/context/dialog" import { openModelPicker } from "@/components/prompt-input/model-picker" -import { WorkspaceChip } from "@/components/prompt-input/workspace-chip" -import { SessionContextUsage } from "@/components/session-context-usage" -import { SendButton } from "./prompt-input/send-button" import { useCommand } from "@/context/command" import { usePermission } from "@/context/permission" import { useLanguage } from "@/context/language" @@ -32,7 +25,7 @@ import { type PopoverControllers, } from "./prompt-input/popover-controllers" import { createPromptKeydownHandler } from "./prompt-input/keydown" -import { PromptModelControl } from "./prompt-input/model-controls" +import { PromptActionBar } from "./prompt-input/action-bar" import { createPromptAttachments } from "./prompt-input/attachments" import { ACCEPTED_FILE_TYPES } from "./prompt-input/files" import { promptLength } from "./prompt-input/history" @@ -46,7 +39,6 @@ import { PromptPopover } from "./prompt-input/slash-popover" import { PromptContextItems } from "./prompt-input/context-items" import { PromptImageAttachments } from "./prompt-input/image-attachments" import { PromptDragOverlay } from "./prompt-input/drag-overlay" -import { promptSendDisabled } from "./prompt-input/readiness" import { ImagePreview } from "@opencode-ai/ui/image-preview" interface PromptInputProps { @@ -143,33 +135,6 @@ export const PromptInput: Component = (props) => { abortReadyProp: () => props.abortReady?.(), }) - const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 }) - const motion = (value: number) => ({ - opacity: value, - transform: `scale(${0.95 + value * 0.05})`, - filter: `blur(${(1 - value) * 2}px)`, - "pointer-events": value > 0.5 ? ("auto" as const) : ("none" as const), - }) - const buttons = createMemo(() => motion(buttonsSpring())) - - const tip = () => { - if (stopping() && abortReady()) { - return ( -
- {language.t("prompt.action.stop")} - {language.t("common.key.esc")} -
- ) - } - - return ( -
- {language.t("prompt.action.send")} - -
- ) - } - const { addToHistory, navigateHistory } = createHistoryNavigation({ store, setStore, @@ -510,61 +475,20 @@ export const PromptInput: Component = (props) => { }} /> -
-
0.5 ? "auto" : "none", - }} - > - - - - - - - - - -
- -
- - - - -
-
+ diff --git a/packages/app/src/components/prompt-input/action-bar.tsx b/packages/app/src/components/prompt-input/action-bar.tsx new file mode 100644 index 000000000..9a46a24f7 --- /dev/null +++ b/packages/app/src/components/prompt-input/action-bar.tsx @@ -0,0 +1,125 @@ +// Bottom action bar of the composer: attach / model / workspace chips on the +// left, context-usage + send-or-stop on the right. Owns the button-reveal +// spring (driven by mode) and the send/stop tooltip, since both exist only to +// serve these controls. + +import { createMemo, Show, type Accessor, type JSX } from "solid-js" +import { useSpring } from "@opencode-ai/ui/motion-spring" +import { Icon } from "@opencode-ai/ui/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { SessionContextUsage } from "@/components/session-context-usage" +import type { useCommand } from "@/context/command" +import type { useLanguage } from "@/context/language" +import type { useLocal } from "@/context/local" +import { PromptModelControl } from "./model-controls" +import { WorkspaceChip } from "./workspace-chip" +import { SendButton } from "./send-button" +import { promptSendDisabled } from "./readiness" +import type { PromptStore } from "./store-types" + +export interface PromptActionBarProps { + mode: PromptStore["mode"] + homeMode?: boolean + language: ReturnType + command: ReturnType + model: ReturnType["model"] + actionReady: Accessor + working: Accessor + abortReady: Accessor + blank: Accessor + stopping: Accessor + pick: () => void + restoreFocus: () => void +} + +export function PromptActionBar(props: PromptActionBarProps): JSX.Element { + const buttonsSpring = useSpring(() => (props.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 }) + const motion = (value: number) => ({ + opacity: value, + transform: `scale(${0.95 + value * 0.05})`, + filter: `blur(${(1 - value) * 2}px)`, + "pointer-events": value > 0.5 ? ("auto" as const) : ("none" as const), + }) + const buttons = createMemo(() => motion(buttonsSpring())) + + const tip = () => { + if (props.stopping() && props.abortReady()) { + return ( +
+ {props.language.t("prompt.action.stop")} + {props.language.t("common.key.esc")} +
+ ) + } + + return ( +
+ {props.language.t("prompt.action.send")} + +
+ ) + } + + return ( +
+
0.5 ? "auto" : "none", + }} + > + + + + + + + + + +
+ +
+ + + + +
+
+ ) +}