diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 97c910a47d..52efd126f3 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -1,7 +1,7 @@
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 { createCliRenderer, MouseButton, TextAttributes, type CliRendererConfig } 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"
@@ -103,6 +103,43 @@ async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
import type { EventSource } from "./context/sdk"
+function rendererConfig(config: TuiConfig.Info): CliRendererConfig {
+ const input = config.tui?.renderer
+ const kitty = input?.use_kitty_keyboard
+
+ return {
+ targetFps: input?.target_fps ?? 60,
+ maxFps: input?.max_fps,
+ gatherStats: input?.gather_stats ?? false,
+ exitOnCtrlC: false,
+ useMouse: input?.use_mouse,
+ enableMouseMovement: input?.enable_mouse_movement,
+ useAlternateScreen: input?.use_alternate_screen,
+ autoFocus: input?.auto_focus ?? false,
+ useKittyKeyboard:
+ kitty === undefined || kitty === true
+ ? {}
+ : kitty === false
+ ? null
+ : {
+ disambiguate: kitty.disambiguate,
+ alternateKeys: kitty.alternate_keys,
+ events: kitty.events,
+ allKeysAsEscapes: kitty.all_keys_as_escapes,
+ reportText: kitty.report_text,
+ },
+ openConsoleOnError: input?.open_console_on_error ?? false,
+ consoleOptions: {
+ keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
+ onCopySelection: (text) => {
+ Clipboard.copy(text).catch((error) => {
+ console.error(`Failed to copy console selection to clipboard: ${error}`)
+ })
+ },
+ },
+ }
+}
+
export function tui(input: {
url: string
args: Args
@@ -130,73 +167,58 @@ export function tui(input: {
resolve()
}
- render(
- () => {
- return (
- }
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
- },
- {
- targetFps: 60,
- gatherStats: false,
- exitOnCtrlC: false,
- useKittyKeyboard: {},
- autoFocus: false,
- openConsoleOnError: false,
- consoleOptions: {
- keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
- onCopySelection: (text) => {
- Clipboard.copy(text).catch((error) => {
- console.error(`Failed to copy console selection to clipboard: ${error}`)
- })
- },
- },
- },
- )
+ const renderer = await createCliRenderer(rendererConfig(input.config))
+
+ await render(() => {
+ return (
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }, renderer)
})
}
diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx
index 9d74adfebd..46838821fe 100644
--- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx
@@ -3,6 +3,7 @@ import { createSimpleContext } from "./helper"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { batch, onCleanup, onMount } from "solid-js"
import { TuiPlugin } from "../plugin"
+import type { CliRenderer } from "@opentui/core"
export type EventSource = {
on: (handler: (event: Event) => void) => () => void
@@ -12,6 +13,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
name: "SDK",
init: (props: {
url: string
+ renderer: CliRenderer
directory?: string
fetch?: typeof fetch
headers?: RequestInit["headers"]
@@ -35,6 +37,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
event: emitter,
url: props.url,
directory: props.directory,
+ renderer: props.renderer,
}).catch((error) => {
console.error("Failed to load TUI plugins", error)
})
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 31903831b5..242834f207 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -920,6 +920,34 @@ export namespace Config {
ref: "KeybindsConfig",
})
+ const TUIRenderer = z
+ .object({
+ target_fps: z.number().int().positive().optional().describe("Target FPS for the renderer"),
+ max_fps: z.number().int().positive().optional().describe("Maximum FPS for immediate rerenders"),
+ gather_stats: z.boolean().optional().describe("Enable renderer frame statistics collection"),
+ use_mouse: z.boolean().optional().describe("Enable mouse tracking"),
+ enable_mouse_movement: z.boolean().optional().describe("Track mouse movement events"),
+ auto_focus: z.boolean().optional().describe("Auto focus nearest focusable item on click"),
+ use_alternate_screen: z.boolean().optional().describe("Use alternate screen buffer"),
+ open_console_on_error: z.boolean().optional().describe("Open renderer console on uncaught errors"),
+ use_kitty_keyboard: z
+ .union([
+ z.boolean(),
+ z
+ .object({
+ disambiguate: z.boolean().optional(),
+ alternate_keys: z.boolean().optional(),
+ events: z.boolean().optional(),
+ all_keys_as_escapes: z.boolean().optional(),
+ report_text: z.boolean().optional(),
+ })
+ .strict(),
+ ])
+ .optional()
+ .describe("Kitty keyboard protocol settings. true enables defaults, false disables it."),
+ })
+ .strict()
+
export const TUI = z.object({
scroll_speed: z.number().min(0.001).optional().describe("TUI scroll speed"),
scroll_acceleration: z
@@ -932,6 +960,7 @@ export namespace Config {
.enum(["auto", "stacked"])
.optional()
.describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
+ renderer: TUIRenderer.optional().describe("Renderer options for the terminal UI"),
})
export const Server = z
diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts
index f1696d38ad..5cd2339e20 100644
--- a/packages/opencode/test/config/tui.test.ts
+++ b/packages/opencode/test/config/tui.test.ts
@@ -81,3 +81,42 @@ test("only reads plugin list from tui.json", async () => {
},
})
})
+
+test("parses renderer options from tui config", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(
+ path.join(dir, "tui.json"),
+ JSON.stringify(
+ {
+ tui: {
+ renderer: {
+ target_fps: 75,
+ auto_focus: true,
+ use_kitty_keyboard: {
+ events: true,
+ report_text: true,
+ },
+ },
+ },
+ },
+ null,
+ 2,
+ ),
+ )
+ },
+ })
+
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const config = await TuiConfig.get()
+ expect(config.tui?.renderer?.target_fps).toBe(75)
+ expect(config.tui?.renderer?.auto_focus).toBe(true)
+ expect(config.tui?.renderer?.use_kitty_keyboard).toEqual({
+ events: true,
+ report_text: true,
+ })
+ },
+ })
+})
diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts
index 12e26979f4..39ec3ad400 100644
--- a/packages/plugin/src/index.ts
+++ b/packages/plugin/src/index.ts
@@ -66,16 +66,19 @@ export type TuiEventBus = {
) => () => void
}
-export type TuiPluginInput = {
+export type TuiPluginInput = {
client: ReturnType
event: TuiEventBus
url: string
directory?: string
+ renderer: Renderer
}
-export type TuiPlugin = (input: TuiPluginInput, options?: PluginOptions) => Promise
+export type TuiPlugin = (input: TuiPluginInput, options?: PluginOptions) => Promise
-export type PluginModule = Plugin | { server?: Plugin; tui?: TuiPlugin; themes?: Record }
+export type PluginModule =
+ | Plugin
+ | { server?: Plugin; tui?: TuiPlugin; themes?: Record }
export type AuthHook = {
provider: string
diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx
index 8e6c1603e2..9d57e121fd 100644
--- a/packages/web/src/content/docs/config.mdx
+++ b/packages/web/src/content/docs/config.mdx
@@ -164,7 +164,12 @@ You can configure TUI-specific settings through the `tui` option.
"scroll_acceleration": {
"enabled": true
},
- "diff_style": "auto"
+ "diff_style": "auto",
+ "renderer": {
+ "target_fps": 60,
+ "auto_focus": false,
+ "use_kitty_keyboard": true
+ }
}
}
```
@@ -174,6 +179,7 @@ Available options:
- `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.
+- `renderer` - Renderer startup options such as `target_fps`, `max_fps`, `gather_stats`, `use_mouse`, `enable_mouse_movement`, `use_alternate_screen`, `auto_focus`, `open_console_on_error`, and `use_kitty_keyboard`.
[Learn more about using the TUI here](/docs/tui).
diff --git a/packages/web/src/content/docs/plugins.mdx b/packages/web/src/content/docs/plugins.mdx
index 7b857ca365..5874cab8bf 100644
--- a/packages/web/src/content/docs/plugins.mdx
+++ b/packages/web/src/content/docs/plugins.mdx
@@ -179,6 +179,7 @@ TUI input includes:
- `client`: the SDK client for the connected server
- `event`: an event bus for server events
+- `renderer`: the active OpenTUI renderer instance
- `url`: server URL
- `directory`: optional working directory