From 89a7e47d37d1d1b82b5edde3871d43cae0e31013 Mon Sep 17 00:00:00 2001 From: Fan Bot Date: Tue, 10 Mar 2026 15:37:06 -0700 Subject: [PATCH 1/8] feat(tui): add configurable cursor style/blink/color --- .../cli/cmd/tui/component/prompt/index.tsx | 13 +++++++--- .../cli/cmd/tui/routes/session/permission.tsx | 6 ++++- .../cli/cmd/tui/routes/session/question.tsx | 7 ++++- .../cli/cmd/tui/ui/dialog-export-options.tsx | 7 ++++- .../src/cli/cmd/tui/ui/dialog-prompt.tsx | 7 ++++- .../src/cli/cmd/tui/util/textarea-cursor.ts | 26 +++++++++++++++++++ packages/opencode/src/config/config.ts | 23 ++++++++++++++++ packages/web/src/content/docs/config.mdx | 12 ++++++++- packages/web/src/content/docs/tui.mdx | 8 +++++- 9 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/util/textarea-cursor.ts 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 2d99051fb976..7e150f9d3e6a 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 a50cd96fc843..201b1d6968ab 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 1565a3008185..938654cf265f 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 d29fe05ee90c..307f533e1916 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 b1b05a0f1a29..12c7341d4cbf 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 000000000000..b0041a443028 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/util/textarea-cursor.ts @@ -0,0 +1,26 @@ +import { RGBA, type CursorStyle, type CursorStyleOptions } from "@opentui/core" +import type { Config } from "@opencode-ai/sdk/v2" +import type { Theme } from "../context/theme" + +type CursorConfig = Config["tui"] + +export function resolveTextareaCursor(theme: Theme, 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: Theme, color?: string): RGBA | undefined { + if (!color) return + if (color.startsWith("#")) return RGBA.fromHex(color) + const maybeThemeColor = theme[color as keyof Theme] + if (maybeThemeColor instanceof RGBA) return maybeThemeColor +} diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 2b8aa9e03010..04d2bf029183 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/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index d2770ee20948..913c23d00e6d 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 010e8328f419..dc0d6677dd79 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. From a03a6ce33a57357dfda11bb065bcaf2fb2cc79ac Mon Sep 17 00:00:00 2001 From: Fan Bot Date: Tue, 10 Mar 2026 16:10:54 -0700 Subject: [PATCH 2/8] chore: rerun CI From 855191992fdd5111efc7f2cdb453fff51c3234a7 Mon Sep 17 00:00:00 2001 From: Fan Bot Date: Tue, 10 Mar 2026 16:54:37 -0700 Subject: [PATCH 3/8] fix(tui): align cursor util types with tui config --- .../src/cli/cmd/tui/util/textarea-cursor.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/util/textarea-cursor.ts b/packages/opencode/src/cli/cmd/tui/util/textarea-cursor.ts index b0041a443028..38dab2c88113 100644 --- a/packages/opencode/src/cli/cmd/tui/util/textarea-cursor.ts +++ b/packages/opencode/src/cli/cmd/tui/util/textarea-cursor.ts @@ -1,10 +1,14 @@ import { RGBA, type CursorStyle, type CursorStyleOptions } from "@opentui/core" -import type { Config } from "@opencode-ai/sdk/v2" -import type { Theme } from "../context/theme" +import type { TuiConfig } from "@/config/tui" -type CursorConfig = Config["tui"] +type CursorConfig = TuiConfig.Info -export function resolveTextareaCursor(theme: Theme, tui?: CursorConfig, fallbackColor: RGBA = theme.text): { +type CursorTheme = { + text: RGBA + [key: string]: unknown +} + +export function resolveTextareaCursor(theme: CursorTheme, tui?: CursorConfig, fallbackColor: RGBA = theme.text): { cursorColor: RGBA cursorStyle: CursorStyleOptions } { @@ -18,7 +22,7 @@ export function resolveTextareaCursor(theme: Theme, tui?: CursorConfig, fallback } } -function resolveCursorColor(theme: Theme, color?: string): RGBA | undefined { +function resolveCursorColor(theme: CursorTheme, color?: string): RGBA | undefined { if (!color) return if (color.startsWith("#")) return RGBA.fromHex(color) const maybeThemeColor = theme[color as keyof Theme] From ac1e7d966e41557122927bcb5dda6a8c72078f23 Mon Sep 17 00:00:00 2001 From: Fan Bot Date: Tue, 10 Mar 2026 16:57:47 -0700 Subject: [PATCH 4/8] fix(tui): add cursor options to tui schema --- packages/opencode/src/cli/cmd/tui/util/textarea-cursor.ts | 2 +- packages/opencode/src/config/tui-schema.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/util/textarea-cursor.ts b/packages/opencode/src/cli/cmd/tui/util/textarea-cursor.ts index 38dab2c88113..59baa89d1624 100644 --- a/packages/opencode/src/cli/cmd/tui/util/textarea-cursor.ts +++ b/packages/opencode/src/cli/cmd/tui/util/textarea-cursor.ts @@ -25,6 +25,6 @@ export function resolveTextareaCursor(theme: CursorTheme, tui?: CursorConfig, fa function resolveCursorColor(theme: CursorTheme, color?: string): RGBA | undefined { if (!color) return if (color.startsWith("#")) return RGBA.fromHex(color) - const maybeThemeColor = theme[color as keyof Theme] + const maybeThemeColor = theme[color] if (maybeThemeColor instanceof RGBA) return maybeThemeColor } diff --git a/packages/opencode/src/config/tui-schema.ts b/packages/opencode/src/config/tui-schema.ts index f9068e3f01d3..b44801ddfdde 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 From 46229eb03e415d1d70e9102018a90c887dcbe3be Mon Sep 17 00:00:00 2001 From: Fan Bot Date: Tue, 10 Mar 2026 17:49:42 -0700 Subject: [PATCH 5/8] fix(app): let terminal toggle keybind bubble from xterm --- packages/app/src/components/terminal.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 120af0a17269..4148b40f4b6f 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() From 50f2e6448683cd296270eaa8cdc5b0af2bacb9b0 Mon Sep 17 00:00:00 2001 From: Fan Bot Date: Tue, 10 Mar 2026 18:32:15 -0700 Subject: [PATCH 6/8] fix(app): trigger terminal toggle command from terminal key handler --- packages/app/src/components/terminal.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 4148b40f4b6f..0620aa2c259c 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -3,7 +3,7 @@ import { showToast } from "@opencode-ai/ui/toast" import type { FitAddon, Ghostty, Terminal as Term } from "ghostty-web" import { type ComponentProps, createEffect, createMemo, onCleanup, onMount, splitProps } from "solid-js" import { SerializeAddon } from "@/addons/serialize" -import { matchKeybind, parseKeybind } from "@/context/command" +import { matchKeybind, parseKeybind, useCommand } from "@/context/command" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { useSDK } from "@/context/sdk" @@ -156,6 +156,7 @@ export const Terminal = (props: TerminalProps) => { const settings = useSettings() const theme = useTheme() const language = useLanguage() + const command = useCommand() const server = useServer() let container!: HTMLDivElement const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"]) @@ -363,13 +364,13 @@ export const Terminal = (props: TerminalProps) => { return true } - // 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 + if (!matchKeybind(keybinds, event)) return true - return true + // Handle terminal toggle directly so it works even when terminal input has focus. + command.trigger(TOGGLE_TERMINAL_ID, "keybind") + return false }) const fit = new mod.FitAddon() From 22351211519e13eae0b3ff75e6b697875e808668 Mon Sep 17 00:00:00 2001 From: Fan Bot Date: Tue, 10 Mar 2026 18:45:28 -0700 Subject: [PATCH 7/8] fix(app): preserve terminal input while handling toggle keybind --- packages/app/src/components/terminal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 0620aa2c259c..f24c9a1b6616 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -366,11 +366,11 @@ export const Terminal = (props: TerminalProps) => { const config = settings.keybinds.get(TOGGLE_TERMINAL_ID) ?? DEFAULT_TOGGLE_TERMINAL_KEYBIND const keybinds = parseKeybind(config) - if (!matchKeybind(keybinds, event)) return true + if (!matchKeybind(keybinds, event)) return false // Handle terminal toggle directly so it works even when terminal input has focus. command.trigger(TOGGLE_TERMINAL_ID, "keybind") - return false + return true }) const fit = new mod.FitAddon() From 262b7d916be6afb85b12fc1b2e0f936184434931 Mon Sep 17 00:00:00 2001 From: Fan Bot Date: Tue, 10 Mar 2026 20:27:28 -0700 Subject: [PATCH 8/8] fix(app): handle terminal toggle on textarea keydown capture --- packages/app/src/components/terminal.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index f24c9a1b6616..7479f3fd2a4b 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -364,13 +364,7 @@ export const Terminal = (props: TerminalProps) => { return true } - const config = settings.keybinds.get(TOGGLE_TERMINAL_ID) ?? DEFAULT_TOGGLE_TERMINAL_KEYBIND - const keybinds = parseKeybind(config) - if (!matchKeybind(keybinds, event)) return false - - // Handle terminal toggle directly so it works even when terminal input has focus. - command.trigger(TOGGLE_TERMINAL_ID, "keybind") - return true + return false }) const fit = new mod.FitAddon() @@ -390,6 +384,17 @@ export const Terminal = (props: TerminalProps) => { handleLinkClick, }) + const handleToggleKeybind = (event: KeyboardEvent) => { + const config = settings.keybinds.get(TOGGLE_TERMINAL_ID) ?? DEFAULT_TOGGLE_TERMINAL_KEYBIND + const keybinds = parseKeybind(config) + if (!matchKeybind(keybinds, event)) return + event.preventDefault() + event.stopPropagation() + command.trigger(TOGGLE_TERMINAL_ID, "keybind") + } + t.textarea?.addEventListener("keydown", handleToggleKeybind, true) + cleanups.push(() => t.textarea?.removeEventListener("keydown", handleToggleKeybind, true)) + if (local.autoFocus !== false) focusTerminal() if (typeof document !== "undefined" && document.fonts) {