diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx
index 120af0a1726..4148b40f4b6 100644
--- a/packages/app/src/components/terminal.tsx
+++ b/packages/app/src/components/terminal.tsx
@@ -363,11 +363,13 @@ export const Terminal = (props: TerminalProps) => {
return true
}
- // allow for toggle terminal keybinds in parent
+ // Let terminal-toggle keybind bubble to parent command handlers.
+ // Returning false prevents xterm from consuming the key event.
const config = settings.keybinds.get(TOGGLE_TERMINAL_ID) ?? DEFAULT_TOGGLE_TERMINAL_KEYBIND
const keybinds = parseKeybind(config)
+ if (matchKeybind(keybinds, event)) return false
- return matchKeybind(keybinds, event)
+ return true
})
const fit = new mod.FitAddon()
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 2d99051fb97..7e150f9d3e6 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -9,6 +9,7 @@ import { EmptyBorder } from "@tui/component/border"
import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
+import { useTuiConfig } from "@tui/context/tui-config"
import { Identifier } from "@/id/id"
import { createStore, produce } from "solid-js/store"
import { useKeybind } from "@tui/context/keybind"
@@ -34,6 +35,7 @@ import { useToast } from "../../ui/toast"
import { useKV } from "../../context/kv"
import { useTextareaKeybindings } from "../textarea-keybindings"
import { DialogSkill } from "../dialog-skill"
+import { resolveTextareaCursor } from "../../util/textarea-cursor"
export type PromptProps = {
sessionID?: string
@@ -78,6 +80,8 @@ export function Prompt(props: PromptProps) {
const renderer = useRenderer()
const { theme, syntax } = useTheme()
const kv = useKV()
+ const tuiConfig = useTuiConfig()
+ const textareaCursor = createMemo(() => resolveTextareaCursor(theme, tuiConfig))
function promptModelWarning() {
toast.show({
@@ -110,8 +114,9 @@ export function Prompt(props: PromptProps) {
})
createEffect(() => {
+ input.cursorStyle = textareaCursor().cursorStyle
if (props.disabled) input.cursorColor = theme.backgroundElement
- if (!props.disabled) input.cursorColor = theme.text
+ if (!props.disabled) input.cursorColor = textareaCursor().cursorColor
})
const lastUserMessage = createMemo(() => {
@@ -1004,12 +1009,14 @@ export function Prompt(props: PromptProps) {
setTimeout(() => {
// setTimeout is a workaround and needs to be addressed properly
if (!input || input.isDestroyed) return
- input.cursorColor = theme.text
+ input.cursorColor = textareaCursor().cursorColor
+ input.cursorStyle = textareaCursor().cursorStyle
}, 0)
}}
onMouseDown={(r: MouseEvent) => r.target?.focus()}
focusedBackgroundColor={theme.backgroundElement}
- cursorColor={theme.text}
+ cursorColor={textareaCursor().cursorColor}
+ cursorStyle={textareaCursor().cursorStyle}
syntaxStyle={syntax()}
/>
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx
index a50cd96fc84..201b1d6968a 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx
@@ -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 { resolveTextareaCursor } from "../../util/textarea-cursor"
type PermissionStage = "permission" | "always" | "reject"
@@ -473,6 +474,8 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
const dimensions = useTerminalDimensions()
const narrow = createMemo(() => dimensions().width < 80)
const dialog = useDialog()
+ const tuiConfig = useTuiConfig()
+ const cursor = createMemo(() => resolveTextareaCursor(theme, tuiConfig, theme.primary))
useKeyboard((evt) => {
if (dialog.stack.length > 0) return
@@ -521,7 +524,8 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
focused
textColor={theme.text}
focusedTextColor={theme.text}
- cursorColor={theme.primary}
+ cursorColor={cursor().cursorColor}
+ cursorStyle={cursor().cursorStyle}
keyBindings={textareaKeybindings()}
/>
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
index 1565a300818..938654cf265 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
@@ -9,12 +9,16 @@ import { useSDK } from "../../context/sdk"
import { SplitBorder } from "../../component/border"
import { useTextareaKeybindings } from "../../component/textarea-keybindings"
import { useDialog } from "../../ui/dialog"
+import { useTuiConfig } from "../../context/tui-config"
+import { resolveTextareaCursor } from "../../util/textarea-cursor"
export function QuestionPrompt(props: { request: QuestionRequest }) {
const sdk = useSDK()
const { theme } = useTheme()
const keybind = useKeybind()
const bindings = useTextareaKeybindings()
+ const tuiConfig = useTuiConfig()
+ const cursor = createMemo(() => resolveTextareaCursor(theme, tuiConfig, theme.primary))
const questions = createMemo(() => props.request.questions)
const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
@@ -391,7 +395,8 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
maxHeight={6}
textColor={theme.text}
focusedTextColor={theme.text}
- cursorColor={theme.primary}
+ cursorColor={cursor().cursorColor}
+ cursorStyle={cursor().cursorStyle}
keyBindings={bindings()}
/>
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx
index d29fe05ee90..307f533e191 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx
@@ -4,6 +4,8 @@ import { useDialog, type DialogContext } from "./dialog"
import { createStore } from "solid-js/store"
import { onMount, Show, type JSX } from "solid-js"
import { useKeyboard } from "@opentui/solid"
+import { useTuiConfig } from "../context/tui-config"
+import { resolveTextareaCursor } from "../util/textarea-cursor"
export type DialogExportOptionsProps = {
defaultFilename: string
@@ -24,6 +26,8 @@ export type DialogExportOptionsProps = {
export function DialogExportOptions(props: DialogExportOptionsProps) {
const dialog = useDialog()
const { theme } = useTheme()
+ const tuiConfig = useTuiConfig()
+ const cursor = () => resolveTextareaCursor(theme, tuiConfig)
let textarea: TextareaRenderable
const [store, setStore] = createStore({
thinking: props.defaultThinking,
@@ -105,7 +109,8 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
placeholder="Enter filename"
textColor={theme.text}
focusedTextColor={theme.text}
- cursorColor={theme.text}
+ cursorColor={cursor().cursorColor}
+ cursorStyle={cursor().cursorStyle}
/>
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx
index b1b05a0f1a2..12c7341d4cb 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx
@@ -3,6 +3,8 @@ import { useTheme } from "../context/theme"
import { useDialog, type DialogContext } from "./dialog"
import { onMount, type JSX } from "solid-js"
import { useKeyboard } from "@opentui/solid"
+import { useTuiConfig } from "../context/tui-config"
+import { resolveTextareaCursor } from "../util/textarea-cursor"
export type DialogPromptProps = {
title: string
@@ -16,6 +18,8 @@ export type DialogPromptProps = {
export function DialogPrompt(props: DialogPromptProps) {
const dialog = useDialog()
const { theme } = useTheme()
+ const tuiConfig = useTuiConfig()
+ const cursor = () => resolveTextareaCursor(theme, tuiConfig)
let textarea: TextareaRenderable
useKeyboard((evt) => {
@@ -56,7 +60,8 @@ export function DialogPrompt(props: DialogPromptProps) {
placeholder={props.placeholder ?? "Enter text"}
textColor={theme.text}
focusedTextColor={theme.text}
- cursorColor={theme.text}
+ cursorColor={cursor().cursorColor}
+ cursorStyle={cursor().cursorStyle}
/>
diff --git a/packages/opencode/src/cli/cmd/tui/util/textarea-cursor.ts b/packages/opencode/src/cli/cmd/tui/util/textarea-cursor.ts
new file mode 100644
index 00000000000..59baa89d162
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/util/textarea-cursor.ts
@@ -0,0 +1,30 @@
+import { RGBA, type CursorStyle, type CursorStyleOptions } from "@opentui/core"
+import type { TuiConfig } from "@/config/tui"
+
+type CursorConfig = TuiConfig.Info
+
+type CursorTheme = {
+ text: RGBA
+ [key: string]: unknown
+}
+
+export function resolveTextareaCursor(theme: CursorTheme, tui?: CursorConfig, fallbackColor: RGBA = theme.text): {
+ cursorColor: RGBA
+ cursorStyle: CursorStyleOptions
+} {
+ const cursorColor = resolveCursorColor(theme, tui?.cursor_color) ?? fallbackColor
+ return {
+ cursorColor,
+ cursorStyle: {
+ style: (tui?.cursor_style ?? "block") as CursorStyle,
+ blinking: tui?.cursor_blink ?? true,
+ },
+ }
+}
+
+function resolveCursorColor(theme: CursorTheme, color?: string): RGBA | undefined {
+ if (!color) return
+ if (color.startsWith("#")) return RGBA.fromHex(color)
+ const maybeThemeColor = theme[color]
+ if (maybeThemeColor instanceof RGBA) return maybeThemeColor
+}
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 2b8aa9e0301..04d2bf02918 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -934,6 +934,29 @@ export namespace Config {
ref: "KeybindsConfig",
})
+ export const TUI = z.object({
+ scroll_speed: z.number().min(0.001).optional().describe("TUI scroll speed"),
+ scroll_acceleration: z
+ .object({
+ enabled: z.boolean().describe("Enable scroll acceleration"),
+ })
+ .optional()
+ .describe("Scroll acceleration settings"),
+ diff_style: z
+ .enum(["auto", "stacked"])
+ .optional()
+ .describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
+ cursor_style: z
+ .enum(["block", "line", "underline"])
+ .optional()
+ .describe("Cursor style for TUI textareas"),
+ cursor_blink: z.boolean().optional().describe("Whether TUI textarea cursor blinks"),
+ cursor_color: z
+ .string()
+ .regex(/^#[0-9a-fA-F]{6}$|^[a-zA-Z][a-zA-Z0-9_]*$/, "Invalid cursor color format")
+ .optional()
+ .describe("Cursor color for TUI textareas. Supports hex (#RRGGBB) or theme color name."),
+ })
export const Server = z
.object({
port: z.number().int().positive().optional().describe("Port to listen on"),
diff --git a/packages/opencode/src/config/tui-schema.ts b/packages/opencode/src/config/tui-schema.ts
index f9068e3f01d..b44801ddfdd 100644
--- a/packages/opencode/src/config/tui-schema.ts
+++ b/packages/opencode/src/config/tui-schema.ts
@@ -22,6 +22,13 @@ export const TuiOptions = z.object({
.enum(["auto", "stacked"])
.optional()
.describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
+ cursor_style: z.enum(["block", "line", "underline"]).optional().describe("Cursor style for TUI textareas"),
+ cursor_blink: z.boolean().optional().describe("Whether TUI textarea cursor blinks"),
+ cursor_color: z
+ .string()
+ .regex(/^#[0-9a-fA-F]{6}$|^[a-zA-Z][a-zA-Z0-9_]*$/, "Invalid cursor color format")
+ .optional()
+ .describe("Cursor color for TUI textareas. Supports hex (#RRGGBB) or theme color name."),
})
export const TuiInfo = z
diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx
index d2770ee2094..913c23d00e6 100644
--- a/packages/web/src/content/docs/config.mdx
+++ b/packages/web/src/content/docs/config.mdx
@@ -170,7 +170,10 @@ Use a dedicated `tui.json` (or `tui.jsonc`) file for TUI-specific settings.
"scroll_acceleration": {
"enabled": true
},
- "diff_style": "auto"
+ "diff_style": "auto",
+ "cursor_style": "line",
+ "cursor_blink": false,
+ "cursor_color": "primary"
}
```
@@ -178,6 +181,13 @@ Use `OPENCODE_TUI_CONFIG` to point to a custom TUI config file.
Legacy `theme`, `keybinds`, and `tui` keys in `opencode.json` are deprecated and automatically migrated when possible.
+- `scroll_acceleration.enabled` - Enable macOS-style scroll acceleration. **Takes precedence over `scroll_speed`.**
+- `scroll_speed` - Custom scroll speed multiplier (default: `3`, minimum: `1`). Ignored if `scroll_acceleration.enabled` is `true`.
+- `diff_style` - Control diff rendering. `"auto"` adapts to terminal width, `"stacked"` always shows single column.
+- `cursor_style` - Cursor shape for TUI textareas. One of `"block"`, `"line"`, or `"underline"` (default: `"block"`).
+- `cursor_blink` - Whether the TUI textarea cursor blinks (default: `true`).
+- `cursor_color` - Cursor color for TUI textareas. Accepts a hex color like `"#FF5733"` or a theme color name like `"primary"`.
+
[Learn more about TUI configuration here](/docs/tui#configure).
---
diff --git a/packages/web/src/content/docs/tui.mdx b/packages/web/src/content/docs/tui.mdx
index 010e8328f41..dc0d6677dd7 100644
--- a/packages/web/src/content/docs/tui.mdx
+++ b/packages/web/src/content/docs/tui.mdx
@@ -368,7 +368,10 @@ You can customize TUI behavior through `tui.json` (or `tui.jsonc`).
"scroll_acceleration": {
"enabled": true
},
- "diff_style": "auto"
+ "diff_style": "auto",
+ "cursor_style": "line",
+ "cursor_blink": false,
+ "cursor_color": "primary"
}
```
@@ -381,6 +384,9 @@ This is separate from `opencode.json`, which configures server/runtime behavior.
- `scroll_acceleration.enabled` - Enable macOS-style scroll acceleration for smooth, natural scrolling. When enabled, scroll speed increases with rapid scrolling gestures and stays precise for slower movements. **This setting takes precedence over `scroll_speed` and overrides it when enabled.**
- `scroll_speed` - Controls how fast the TUI scrolls when using scroll commands (minimum: `0.001`, supports decimal values). Defaults to `3`. **Note: This is ignored if `scroll_acceleration.enabled` is set to `true`.**
- `diff_style` - Controls diff rendering. `"auto"` adapts to terminal width, `"stacked"` always shows a single-column layout.
+- `cursor_style` - Cursor shape for TUI textareas. One of `"block"`, `"line"`, or `"underline"`.
+- `cursor_blink` - Toggle cursor blinking for TUI textareas.
+- `cursor_color` - Cursor color for TUI textareas. Supports hex (for example `"#FF5733"`) or theme color names (for example `"primary"`).
Use `OPENCODE_TUI_CONFIG` to load a custom TUI config path.