From 3b4a7fa8e9e9ce1d7f10b835223cc172ad2a6b1a Mon Sep 17 00:00:00 2001 From: scarf Date: Tue, 6 Jan 2026 17:27:58 +0900 Subject: [PATCH 1/2] feat(tui): add mouse support to menus --- .../cli/cmd/tui/component/prompt/index.tsx | 80 +++++++++++++++---- 1 file changed, 65 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 4558914cb7e..8e5680dec8b 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1,8 +1,8 @@ -import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg } from "@opentui/core" +import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg, RGBA } from "@opentui/core" import { createEffect, createMemo, type JSX, onMount, createSignal, onCleanup, Show, Switch, Match } from "solid-js" import "opentui-spinner/solid" import { useLocal } from "@tui/context/local" -import { useTheme } from "@tui/context/theme" +import { useTheme, selectedForeground } from "@tui/context/theme" import { EmptyBorder } from "@tui/component/border" import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" @@ -30,6 +30,8 @@ import { DialogAlert } from "../../ui/dialog-alert" import { useToast } from "../../ui/toast" import { useKV } from "../../context/kv" import { useTextareaKeybindings } from "../textarea-keybindings" +import { DialogAgent } from "../dialog-agent" +import { DialogModel } from "../dialog-model" export type PromptProps = { sessionID?: string @@ -73,6 +75,13 @@ export function Prompt(props: PromptProps) { const { theme, syntax } = useTheme() const kv = useKV() + const hoverFg = createMemo(() => selectedForeground(theme)) + + const [agentHover, setAgentHover] = createSignal(false) + const [modelNameHover, setModelNameHover] = createSignal(false) + const [agentCycleHover, setAgentCycleHover] = createSignal(false) + const [commandListHover, setCommandListHover] = createSignal(false) + function promptModelWarning() { toast.show({ variant: "warning", @@ -932,17 +941,42 @@ export function Prompt(props: PromptProps) { syntaxStyle={syntax()} /> - - {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "} - + { + if (store.mode === "shell") return + setAgentHover(true) + }} + onMouseOut={() => setAgentHover(false)} + onMouseUp={() => { + if (store.mode === "shell") return + dialog.replace(() => ) + }} + > + + {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "} + + - - + setModelNameHover(true)} + onMouseOut={() => setModelNameHover(false)} + onMouseUp={() => { + dialog.replace(() => ) + }} + > + {local.model.parsed().model} - {local.model.parsed().provider} + {local.model.parsed().provider} - · + · {local.model.variant.current()} @@ -1062,12 +1096,28 @@ export function Prompt(props: PromptProps) { - - {keybind.print("agent_cycle")} switch agent - - - {keybind.print("command_list")} commands - + setAgentCycleHover(true)} + onMouseOut={() => setAgentCycleHover(false)} + onMouseUp={() => dialog.replace(() => )} + > + + {keybind.print("agent_cycle")} {" "} + switch agent + + + setCommandListHover(true)} + onMouseOut={() => setCommandListHover(false)} + onMouseUp={() => command.show()} + > + + {keybind.print("command_list")} {" "} + commands + + From ddc8f9e00571d2a4e97a677375067d480b3d1bdf Mon Sep 17 00:00:00 2001 From: scarf Date: Tue, 6 Jan 2026 17:31:31 +0900 Subject: [PATCH 2/2] refactor(tui): create and use `HoverableLabel` --- .../cli/cmd/tui/component/hoverable-label.tsx | 51 ++++++++ .../cli/cmd/tui/component/prompt/index.tsx | 113 +++++++----------- 2 files changed, 93 insertions(+), 71 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/component/hoverable-label.tsx diff --git a/packages/opencode/src/cli/cmd/tui/component/hoverable-label.tsx b/packages/opencode/src/cli/cmd/tui/component/hoverable-label.tsx new file mode 100644 index 00000000000..73944c3adcf --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/hoverable-label.tsx @@ -0,0 +1,51 @@ +import { RGBA } from "@opentui/core" +import { createSignal, createUniqueId, onCleanup, type JSX } from "solid-js" +import { useTheme, selectedForeground } from "@tui/context/theme" + +const [activeId, setActiveId] = createSignal(null) + +type HoverableLabelProps = { + children: (hover: boolean, hoverFg: () => RGBA) => JSX.Element + onClick?: () => void + disabled?: boolean +} + +export function HoverableLabel(props: HoverableLabelProps) { + const id = createUniqueId() + const { theme } = useTheme() + const hoverFg = () => selectedForeground(theme) + + const isHovered = () => activeId() === id && !props.disabled + + onCleanup(() => { + if (activeId() !== id) return + setActiveId(null) + }) + + const handleMouseOver = () => { + if (props.disabled) return + setActiveId(id) + } + + const handleMouseOut = () => { + if (activeId() !== id) return + setActiveId(null) + } + + const handleClick = () => { + if (props.disabled) return + setActiveId(null) + props.onClick?.() + } + + return ( + + {props.children(isHovered(), hoverFg)} + + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 8e5680dec8b..39fc3e80144 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1,8 +1,8 @@ -import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg, RGBA } from "@opentui/core" +import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg } from "@opentui/core" import { createEffect, createMemo, type JSX, onMount, createSignal, onCleanup, Show, Switch, Match } from "solid-js" import "opentui-spinner/solid" import { useLocal } from "@tui/context/local" -import { useTheme, selectedForeground } from "@tui/context/theme" +import { useTheme } from "@tui/context/theme" import { EmptyBorder } from "@tui/component/border" import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" @@ -32,6 +32,7 @@ import { useKV } from "../../context/kv" import { useTextareaKeybindings } from "../textarea-keybindings" import { DialogAgent } from "../dialog-agent" import { DialogModel } from "../dialog-model" +import { HoverableLabel } from "../hoverable-label" export type PromptProps = { sessionID?: string @@ -75,13 +76,6 @@ export function Prompt(props: PromptProps) { const { theme, syntax } = useTheme() const kv = useKV() - const hoverFg = createMemo(() => selectedForeground(theme)) - - const [agentHover, setAgentHover] = createSignal(false) - const [modelNameHover, setModelNameHover] = createSignal(false) - const [agentCycleHover, setAgentCycleHover] = createSignal(false) - const [commandListHover, setCommandListHover] = createSignal(false) - function promptModelWarning() { toast.show({ variant: "warning", @@ -941,47 +935,30 @@ export function Prompt(props: PromptProps) { syntaxStyle={syntax()} /> - { - if (store.mode === "shell") return - setAgentHover(true) - }} - onMouseOut={() => setAgentHover(false)} - onMouseUp={() => { - if (store.mode === "shell") return - dialog.replace(() => ) - }} - > - - {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "} - - - - setModelNameHover(true)} - onMouseOut={() => setModelNameHover(false)} - onMouseUp={() => { - dialog.replace(() => ) - }} - > - - {local.model.parsed().model} + dialog.replace(() => )}> + {(hover, hoverFg) => ( + + {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "} - {local.model.parsed().provider} - - · - - {local.model.variant.current()} - - - + )} + + + dialog.replace(() => )}> + {(hover, hoverFg) => ( + + + {local.model.parsed().model} + + {local.model.parsed().provider} + + · + + {local.model.variant.current()} + + + + )} + @@ -1096,28 +1073,22 @@ export function Prompt(props: PromptProps) { - setAgentCycleHover(true)} - onMouseOut={() => setAgentCycleHover(false)} - onMouseUp={() => dialog.replace(() => )} - > - - {keybind.print("agent_cycle")} {" "} - switch agent - - - setCommandListHover(true)} - onMouseOut={() => setCommandListHover(false)} - onMouseUp={() => command.show()} - > - - {keybind.print("command_list")} {" "} - commands - - + dialog.replace(() => )}> + {(hover, hoverFg) => ( + + {keybind.print("agent_cycle")} {" "} + switch agent + + )} + + command.show()}> + {(hover, hoverFg) => ( + + {keybind.print("command_list")} {" "} + commands + + )} +