Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 35 additions & 6 deletions packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { DialogAgent } from "@tui/component/dialog-agent"
import { DialogSessionList } from "@tui/component/dialog-session-list"
import { DialogWorkspaceList } from "@tui/component/dialog-workspace-list"
import { KeybindProvider } from "@tui/context/keybind"
import { ThemeProvider, useTheme } from "@tui/context/theme"
import { ThemeProvider, selectedForeground, useTheme } from "@tui/context/theme"
import { Home } from "@tui/routes/home"
import { Session } from "@tui/routes/session"
import { PromptHistoryProvider } from "./component/prompt/history"
Expand Down Expand Up @@ -217,10 +217,10 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
const sync = useSync()
const exit = useExit()
const promptRef = usePromptRef()
const click = Selection.click()

useKeyboard((evt) => {
if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return
if (!renderer.getSelection()) return
if (!Selection.active(renderer)) return

// Windows Terminal-like behavior:
// - Ctrl+C copies and dismisses selection
Expand All @@ -238,12 +238,14 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
}

if (evt.name === "escape") {
Selection.dismiss(renderer)
renderer.clearSelection()
evt.preventDefault()
evt.stopPropagation()
return
}

Selection.dismiss(renderer)
renderer.clearSelection()
})

Expand All @@ -259,6 +261,13 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
}
const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true))

createEffect(() => {
Selection.configure(renderer, {
bg: theme.primary,
fg: selectedForeground(theme, theme.primary),
})
})

createEffect(() => {
console.log(JSON.stringify(route.data))
})
Expand Down Expand Up @@ -795,14 +804,19 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
height={dimensions().height}
backgroundColor={theme.background}
onMouseDown={(evt) => {
if (Selection.press(click, renderer, evt)) return
if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return
if (evt.button !== MouseButton.RIGHT) return

if (!Selection.copy(renderer, toast)) return
evt.preventDefault()
evt.stopPropagation()
}}
onMouseUp={Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT ? undefined : () => Selection.copy(renderer, toast)}
onMouseUp={(evt) => {
if (Selection.release(click, renderer, toast, evt)) return
if (Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return
Selection.copy(renderer, toast)
}}
>
<Switch>
<Match when={route.data.type === "home"}>
Expand Down Expand Up @@ -864,6 +878,7 @@ function ErrorComponent(props: {
issueURL.searchParams.set("opencode-version", Installation.VERSION)

const copyIssueURL = () => {
if (Selection.active(renderer)) return
Clipboard.copy(issueURL.toString()).then(() => {
setCopied(true)
})
Expand All @@ -884,10 +899,24 @@ function ErrorComponent(props: {
</box>
<box flexDirection="row" gap={2} alignItems="center">
<text fg={colors.text}>A fatal error occurred!</text>
<box onMouseUp={props.reset} backgroundColor={colors.primary} padding={1}>
<box
onMouseUp={() => {
if (Selection.active(renderer)) return
props.reset()
}}
backgroundColor={colors.primary}
padding={1}
>
<text fg={colors.bg}>Reset TUI</text>
</box>
<box onMouseUp={handleExit} backgroundColor={colors.primary} padding={1}>
<box
onMouseUp={() => {
if (Selection.active(renderer)) return
handleExit()
}}
backgroundColor={colors.primary}
padding={1}
>
<text fg={colors.bg}>Exit</text>
</box>
</box>
Expand Down
12 changes: 10 additions & 2 deletions packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import { useTheme } from "../context/theme"
import { TextAttributes } from "@opentui/core"
import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2"
import { DialogModel } from "./dialog-model"
import { useKeyboard } from "@opentui/solid"
import { useKeyboard, useRenderer } from "@opentui/solid"
import { Clipboard } from "@tui/util/clipboard"
import { useToast } from "../ui/toast"
import { Selection } from "../util/selection"

const PROVIDER_PRIORITY: Record<string, number> = {
opencode: 0,
Expand Down Expand Up @@ -131,6 +132,7 @@ function AutoMethod(props: AutoMethodProps) {
const dialog = useDialog()
const sync = useSync()
const toast = useToast()
const renderer = useRenderer()

useKeyboard((evt) => {
if (evt.name === "c" && !evt.ctrl && !evt.meta) {
Expand Down Expand Up @@ -161,7 +163,13 @@ function AutoMethod(props: AutoMethodProps) {
<text attributes={TextAttributes.BOLD} fg={theme.text}>
{props.title}
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
<text
fg={theme.textMuted}
onMouseUp={() => {
if (Selection.active(renderer)) return
dialog.clear()
}}
>
esc
</text>
</box>
Expand Down
11 changes: 10 additions & 1 deletion packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import { useTheme } from "../context/theme"
import { useDialog } from "@tui/ui/dialog"
import { useSync } from "@tui/context/sync"
import { For, Match, Switch, Show, createMemo } from "solid-js"
import { useRenderer } from "@opentui/solid"
import { Selection } from "../util/selection"

export type DialogStatusProps = {}

export function DialogStatus() {
const sync = useSync()
const { theme } = useTheme()
const dialog = useDialog()
const renderer = useRenderer()

const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled))

Expand Down Expand Up @@ -45,7 +48,13 @@ export function DialogStatus() {
<text fg={theme.text} attributes={TextAttributes.BOLD}>
Status
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
<text
fg={theme.textMuted}
onMouseUp={() => {
if (Selection.active(renderer)) return
dialog.clear()
}}
>
esc
</text>
</box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { useSync } from "@tui/context/sync"
import { useTheme, selectedForeground } from "@tui/context/theme"
import { SplitBorder } from "@tui/component/border"
import { useCommandDialog } from "@tui/component/dialog-command"
import { useTerminalDimensions } from "@opentui/solid"
import { useRenderer, useTerminalDimensions } from "@opentui/solid"
import { Selection } from "@tui/util/selection"
import { Locale } from "@/util/locale"
import type { PromptInfo } from "./history"
import { useFrecency } from "./frecency"
Expand Down Expand Up @@ -80,6 +81,7 @@ export function Autocomplete(props: {
const command = useCommandDialog()
const { theme } = useTheme()
const dimensions = useTerminalDimensions()
const renderer = useRenderer()
const frecency = useFrecency()

const [store, setStore] = createStore({
Expand Down Expand Up @@ -648,7 +650,10 @@ export function Autocomplete(props: {
setStore("input", "mouse")
moveTo(index)
}}
onMouseUp={() => select()}
onMouseUp={() => {
if (Selection.active(renderer)) return
select()
}}
>
<text fg={index === store.selected ? selectedForeground(theme) : theme.text} flexShrink={0}>
{option().display}
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { useExit } from "../../context/exit"
import { Clipboard } from "../../util/clipboard"
import type { FilePart } from "@opencode-ai/sdk/v2"
import { TuiEvent } from "../../event"
import { Selection } from "../../util/selection"
import { iife } from "@/util/iife"
import { Locale } from "@/util/locale"
import { formatDuration } from "@/util/format"
Expand Down Expand Up @@ -1119,6 +1120,7 @@ export function Prompt(props: PromptProps) {
})
})
const handleMessageClick = () => {
if (Selection.active(renderer)) return
const r = retry()
if (!r) return
if (isTruncated()) {
Expand Down
19 changes: 15 additions & 4 deletions packages/opencode/src/cli/cmd/tui/routes/session/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { useTheme } from "@tui/context/theme"
import { SplitBorder } from "@tui/component/border"
import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2"
import { useCommandDialog } from "@tui/component/dialog-command"
import { Selection } from "@tui/util/selection"
import { useKeybind } from "../../context/keybind"
import { Flag } from "@/flag/flag"
import { useTerminalDimensions } from "@opentui/solid"
import { useRenderer, useTerminalDimensions } from "@opentui/solid"

const Title = (props: { session: Accessor<Session> }) => {
const { theme } = useTheme()
Expand Down Expand Up @@ -82,6 +83,7 @@ export function Header() {
const { theme } = useTheme()
const keybind = useKeybind()
const command = useCommandDialog()
const renderer = useRenderer()
const [hover, setHover] = createSignal<"parent" | "prev" | "next" | null>(null)
const dimensions = useTerminalDimensions()
const narrow = createMemo(() => dimensions().width < 80)
Expand Down Expand Up @@ -122,7 +124,10 @@ export function Header() {
<box
onMouseOver={() => setHover("parent")}
onMouseOut={() => setHover(null)}
onMouseUp={() => command.trigger("session.parent")}
onMouseUp={() => {
if (Selection.active(renderer)) return
command.trigger("session.parent")
}}
backgroundColor={hover() === "parent" ? theme.backgroundElement : theme.backgroundPanel}
>
<text fg={theme.text}>
Expand All @@ -132,7 +137,10 @@ export function Header() {
<box
onMouseOver={() => setHover("prev")}
onMouseOut={() => setHover(null)}
onMouseUp={() => command.trigger("session.child.previous")}
onMouseUp={() => {
if (Selection.active(renderer)) return
command.trigger("session.child.previous")
}}
backgroundColor={hover() === "prev" ? theme.backgroundElement : theme.backgroundPanel}
>
<text fg={theme.text}>
Expand All @@ -142,7 +150,10 @@ export function Header() {
<box
onMouseOver={() => setHover("next")}
onMouseOut={() => setHover(null)}
onMouseUp={() => command.trigger("session.child.next")}
onMouseUp={() => {
if (Selection.active(renderer)) return
command.trigger("session.child.next")
}}
backgroundColor={hover() === "next" ? theme.backgroundElement : theme.backgroundPanel}
>
<text fg={theme.text}>
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import { DialogExportOptions } from "../../ui/dialog-export-options"
import { formatTranscript } from "../../util/transcript"
import { UI } from "@/cli/ui.ts"
import { useTuiConfig } from "../../context/tui-config"
import { Selection } from "../../util/selection"

addDefaultParsers(parsers.parsers)

Expand Down Expand Up @@ -1079,6 +1080,7 @@ export function Session() {
const dialog = useDialog()

const handleUnrevert = async () => {
if (Selection.active(renderer)) return
const confirmed = await DialogConfirm.show(
dialog,
"Confirm Redo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Locale } from "@/util/locale"
import { Global } from "@/global"
import { useDialog } from "../../ui/dialog"
import { useTuiConfig } from "../../context/tui-config"
import { Selection } from "../../util/selection"

type PermissionStage = "permission" | "always" | "reject"

Expand Down Expand Up @@ -649,6 +650,7 @@ function Prompt<const T extends Record<string, string>>(props: {
backgroundColor={option === store.selected ? theme.warning : theme.backgroundMenu}
onMouseOver={() => setStore("selected", option)}
onMouseUp={() => {
if (Selection.active(renderer)) return
setStore("selected", option)
props.onSelect(option)
}}
Expand Down
24 changes: 19 additions & 5 deletions packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createStore } from "solid-js/store"
import { createMemo, createSignal, For, Show } from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { useKeyboard, useRenderer } from "@opentui/solid"
import type { TextareaRenderable } from "@opentui/core"
import { useKeybind } from "../../context/keybind"
import { selectedForeground, tint, useTheme } from "../../context/theme"
Expand All @@ -9,12 +9,14 @@ import { useSDK } from "../../context/sdk"
import { SplitBorder } from "../../component/border"
import { useTextareaKeybindings } from "../../component/textarea-keybindings"
import { useDialog } from "../../ui/dialog"
import { Selection } from "../../util/selection"

export function QuestionPrompt(props: { request: QuestionRequest }) {
const sdk = useSDK()
const { theme } = useTheme()
const keybind = useKeybind()
const bindings = useTextareaKeybindings()
const renderer = useRenderer()

const questions = createMemo(() => props.request.questions)
const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
Expand Down Expand Up @@ -279,7 +281,10 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
}
onMouseOver={() => setTabHover(index())}
onMouseOut={() => setTabHover(null)}
onMouseUp={() => selectTab(index())}
onMouseUp={() => {
if (Selection.active(renderer)) return
selectTab(index())
}}
>
<text
fg={
Expand All @@ -304,7 +309,10 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
}
onMouseOver={() => setTabHover("confirm")}
onMouseOut={() => setTabHover(null)}
onMouseUp={() => selectTab(questions().length)}
onMouseUp={() => {
if (Selection.active(renderer)) return
selectTab(questions().length)
}}
>
<text fg={confirm() ? selectedForeground(theme, theme.accent) : theme.textMuted}>Confirm</text>
</box>
Expand All @@ -328,7 +336,10 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
<box
onMouseOver={() => moveTo(i())}
onMouseDown={() => moveTo(i())}
onMouseUp={() => selectOption()}
onMouseUp={() => {
if (Selection.active(renderer)) return
selectOption()
}}
>
<box flexDirection="row">
<box backgroundColor={active() ? theme.backgroundElement : undefined} paddingRight={1}>
Expand Down Expand Up @@ -357,7 +368,10 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
<box
onMouseOver={() => moveTo(options().length)}
onMouseDown={() => moveTo(options().length)}
onMouseUp={() => selectOption()}
onMouseUp={() => {
if (Selection.active(renderer)) return
selectOption()
}}
>
<box flexDirection="row">
<box backgroundColor={other() ? theme.backgroundElement : undefined} paddingRight={1}>
Expand Down
Loading
Loading