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