From cc0e9e02548293a5093cc321abb54d73a631288a Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:33:53 +1000 Subject: [PATCH 1/2] yeah --- packages/opencode/src/cli/cmd/tui/app.tsx | 72 +++++++++++++++---- .../opencode/src/cli/cmd/tui/ui/dialog.tsx | 43 ++++++++--- packages/opencode/src/flag/flag.ts | 4 +- 3 files changed, 96 insertions(+), 23 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index dbad3f699fc6..bddb7df4c08a 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -1,6 +1,6 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import { Clipboard } from "@tui/util/clipboard" -import { TextAttributes } from "@opentui/core" +import { MouseButton, TextAttributes } from "@opentui/core" import { RouteProvider, useRoute } from "@tui/context/route" import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js" import { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "./win32" @@ -209,6 +209,38 @@ function App() { const exit = useExit() const promptRef = usePromptRef() + useKeyboard((evt) => { + const selection = renderer.getSelection() + if (!selection) return + + // Windows Terminal-like behavior: + // - Ctrl+C copies and dismisses selection + // - Esc dismisses selection + // - Most other key input dismisses selection and is passed through + if (evt.ctrl && evt.name === "c") { + const text = selection.getSelectedText() + if (text) { + Clipboard.copy(text) + .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) + .catch(toast.error) + } + + renderer.clearSelection() + evt.preventDefault() + evt.stopPropagation() + return + } + + if (evt.name === "escape") { + renderer.clearSelection() + evt.preventDefault() + evt.stopPropagation() + return + } + + renderer.clearSelection() + }) + // Wire up console copy-to-clipboard via opentui's onCopySelection callback renderer.console.onCopySelection = async (text: string) => { if (!text || text.length === 0) return @@ -216,6 +248,7 @@ function App() { await Clipboard.copy(text) .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) .catch(toast.error) + renderer.clearSelection() } const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true)) @@ -702,19 +735,34 @@ function App() { width={dimensions().width} height={dimensions().height} backgroundColor={theme.background} - onMouseUp={async () => { - if (Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) { - renderer.clearSelection() - return - } + onMouseDown={(evt) => { + if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return + if (evt.button !== MouseButton.RIGHT) return + const text = renderer.getSelection()?.getSelectedText() - if (text && text.length > 0) { - await Clipboard.copy(text) - .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) - .catch(toast.error) - renderer.clearSelection() - } + if (!text) return + + Clipboard.copy(text) + .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) + .catch(toast.error) + + renderer.clearSelection() + evt.preventDefault() + evt.stopPropagation() }} + onMouseUp={ + Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT + ? undefined + : async () => { + const text = renderer.getSelection()?.getSelectedText() + if (text && text.length > 0) { + await Clipboard.copy(text) + .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) + .catch(toast.error) + renderer.clearSelection() + } + } + } > diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx index 0b57ad29cf41..b9e97e215f9b 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx @@ -1,10 +1,11 @@ import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import { batch, createContext, Show, useContext, type JSX, type ParentProps } from "solid-js" import { useTheme } from "@tui/context/theme" -import { Renderable, RGBA } from "@opentui/core" +import { MouseButton, Renderable, RGBA } from "@opentui/core" import { createStore } from "solid-js/store" import { Clipboard } from "@tui/util/clipboard" import { useToast } from "./toast" +import { Flag } from "@/flag/flag" export function Dialog( props: ParentProps<{ @@ -56,8 +57,12 @@ function init() { size: "medium" as "medium" | "large", }) + const renderer = useRenderer() + useKeyboard((evt) => { - if ((evt.name === "escape" || (evt.ctrl && evt.name === "c")) && store.stack.length > 0) { + if (store.stack.length === 0) return + if ((evt.name === "escape" || (evt.ctrl && evt.name === "c")) && renderer.getSelection()) return + if (evt.name === "escape" || (evt.ctrl && evt.name === "c")) { const current = store.stack.at(-1)! current.onClose?.() setStore("stack", store.stack.slice(0, -1)) @@ -67,7 +72,6 @@ function init() { } }) - const renderer = useRenderer() let focus: Renderable | null function refocus() { setTimeout(() => { @@ -138,15 +142,34 @@ export function DialogProvider(props: ParentProps) { {props.children} { + onMouseDown={(evt) => { + if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return + if (evt.button !== MouseButton.RIGHT) return + const text = renderer.getSelection()?.getSelectedText() - if (text && text.length > 0) { - await Clipboard.copy(text) - .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) - .catch(toast.error) - renderer.clearSelection() - } + if (!text) return + + Clipboard.copy(text) + .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) + .catch(toast.error) + + renderer.clearSelection() + evt.preventDefault() + evt.stopPropagation() }} + onMouseUp={ + !Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT + ? async () => { + const text = renderer.getSelection()?.getSelectedText() + if (text && text.length > 0) { + await Clipboard.copy(text) + .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) + .catch(toast.error) + renderer.clearSelection() + } + } + : undefined + } > value.clear()} size={value.size}> diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index b11058b34058..0270637f6685 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -37,7 +37,9 @@ export namespace Flag { export const OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER = truthy("OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER") export const OPENCODE_EXPERIMENTAL_ICON_DISCOVERY = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY") - export const OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT = truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT") + const copy = process.env["OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"] + export const OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT = + copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT") export const OPENCODE_ENABLE_EXA = truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA") export const OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS = number("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS") From 892fb6d3a886766f051f812798026d8454c8e1e3 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Fri, 13 Feb 2026 05:33:49 +1000 Subject: [PATCH 2/2] clean --- packages/opencode/src/cli/cmd/tui/app.tsx | 37 ++++------------- .../opencode/src/cli/cmd/tui/ui/dialog.tsx | 40 ++++++++----------- .../src/cli/cmd/tui/util/selection.ts | 25 ++++++++++++ packages/opencode/src/flag/flag.ts | 10 +++-- 4 files changed, 56 insertions(+), 56 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/util/selection.ts diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index bddb7df4c08a..53339082ae21 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -1,5 +1,6 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import { Clipboard } from "@tui/util/clipboard" +import { Selection } from "@tui/util/selection" import { MouseButton, TextAttributes } from "@opentui/core" import { RouteProvider, useRoute } from "@tui/context/route" import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js" @@ -210,22 +211,19 @@ function App() { const promptRef = usePromptRef() useKeyboard((evt) => { - const selection = renderer.getSelection() - if (!selection) return + if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return + if (!renderer.getSelection()) return // Windows Terminal-like behavior: // - Ctrl+C copies and dismisses selection // - Esc dismisses selection // - Most other key input dismisses selection and is passed through if (evt.ctrl && evt.name === "c") { - const text = selection.getSelectedText() - if (text) { - Clipboard.copy(text) - .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) - .catch(toast.error) + if (!Selection.copy(renderer, toast)) { + renderer.clearSelection() + return } - renderer.clearSelection() evt.preventDefault() evt.stopPropagation() return @@ -739,30 +737,11 @@ function App() { if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return if (evt.button !== MouseButton.RIGHT) return - const text = renderer.getSelection()?.getSelectedText() - if (!text) return - - Clipboard.copy(text) - .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) - .catch(toast.error) - - renderer.clearSelection() + if (!Selection.copy(renderer, toast)) return evt.preventDefault() evt.stopPropagation() }} - onMouseUp={ - Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT - ? undefined - : async () => { - const text = renderer.getSelection()?.getSelectedText() - if (text && text.length > 0) { - await Clipboard.copy(text) - .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) - .catch(toast.error) - renderer.clearSelection() - } - } - } + onMouseUp={Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT ? undefined : () => Selection.copy(renderer, toast)} > diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx index b9e97e215f9b..8cebd9cba54d 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx @@ -3,9 +3,9 @@ import { batch, createContext, Show, useContext, type JSX, type ParentProps } fr import { useTheme } from "@tui/context/theme" import { MouseButton, Renderable, RGBA } from "@opentui/core" import { createStore } from "solid-js/store" -import { Clipboard } from "@tui/util/clipboard" import { useToast } from "./toast" import { Flag } from "@/flag/flag" +import { Selection } from "@tui/util/selection" export function Dialog( props: ParentProps<{ @@ -17,10 +17,18 @@ export function Dialog( const { theme } = useTheme() const renderer = useRenderer() + let dismiss = false + return ( { - if (renderer.getSelection()) return + onMouseDown={() => { + dismiss = !!renderer.getSelection() + }} + onMouseUp={() => { + if (dismiss) { + dismiss = false + return + } props.onClose?.() }} width={dimensions().width} @@ -33,8 +41,8 @@ export function Dialog( backgroundColor={RGBA.fromInts(0, 0, 0, 150)} > { - if (renderer.getSelection()) return + onMouseUp={(e) => { + dismiss = false e.stopPropagation() }} width={props.size === "large" ? 80 : 60} @@ -61,6 +69,7 @@ function init() { useKeyboard((evt) => { if (store.stack.length === 0) return + if (evt.defaultPrevented) return if ((evt.name === "escape" || (evt.ctrl && evt.name === "c")) && renderer.getSelection()) return if (evt.name === "escape" || (evt.ctrl && evt.name === "c")) { const current = store.stack.at(-1)! @@ -146,29 +155,12 @@ export function DialogProvider(props: ParentProps) { if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return if (evt.button !== MouseButton.RIGHT) return - const text = renderer.getSelection()?.getSelectedText() - if (!text) return - - Clipboard.copy(text) - .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) - .catch(toast.error) - - renderer.clearSelection() + if (!Selection.copy(renderer, toast)) return evt.preventDefault() evt.stopPropagation() }} onMouseUp={ - !Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT - ? async () => { - const text = renderer.getSelection()?.getSelectedText() - if (text && text.length > 0) { - await Clipboard.copy(text) - .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) - .catch(toast.error) - renderer.clearSelection() - } - } - : undefined + !Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT ? () => Selection.copy(renderer, toast) : undefined } > diff --git a/packages/opencode/src/cli/cmd/tui/util/selection.ts b/packages/opencode/src/cli/cmd/tui/util/selection.ts new file mode 100644 index 000000000000..1230852dcc07 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/util/selection.ts @@ -0,0 +1,25 @@ +import { Clipboard } from "./clipboard" + +type Toast = { + show: (input: { message: string; variant: "info" | "success" | "warning" | "error" }) => void + error: (err: unknown) => void +} + +type Renderer = { + getSelection: () => { getSelectedText: () => string } | null + clearSelection: () => void +} + +export namespace Selection { + export function copy(renderer: Renderer, toast: Toast): boolean { + const text = renderer.getSelection()?.getSelectedText() + if (!text) return false + + Clipboard.copy(text) + .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) + .catch(toast.error) + + renderer.clearSelection() + return true + } +} diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 0270637f6685..8c999a1c0106 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -1,6 +1,10 @@ +function truthyValue(value: string | undefined) { + const v = value?.toLowerCase() + return v === "true" || v === "1" +} + function truthy(key: string) { - const value = process.env[key]?.toLowerCase() - return value === "true" || value === "1" + return truthyValue(process.env[key]) } export namespace Flag { @@ -39,7 +43,7 @@ export namespace Flag { OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY") const copy = process.env["OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"] export const OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT = - copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT") + copy === undefined ? process.platform === "win32" : truthyValue(copy) export const OPENCODE_ENABLE_EXA = truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA") export const OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS = number("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS")