Sebastian Herrlinger vor 2 Monaten
Ursprung
Commit
9a5cf7dfe5

+ 90 - 68
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 (
-          <ErrorBoundary
-            fallback={(error, reset) => <ErrorComponent error={error} reset={reset} onExit={onExit} mode={mode} />}
-          >
-            <ArgsProvider {...input.args}>
-              <ExitProvider onExit={onExit}>
-                <KVProvider>
-                  <ToastProvider>
-                    <RouteProvider>
-                      <TuiConfigProvider config={input.config}>
-                        <SDKProvider
-                          url={input.url}
-                          directory={input.directory}
-                          fetch={input.fetch}
-                          headers={input.headers}
-                          events={input.events}
-                        >
-                          <SyncProvider>
-                            <ThemeProvider mode={mode}>
-                              <LocalProvider>
-                                <KeybindProvider>
-                                  <PromptStashProvider>
-                                    <DialogProvider>
-                                      <CommandProvider>
-                                        <FrecencyProvider>
-                                          <PromptHistoryProvider>
-                                            <PromptRefProvider>
-                                              <App />
-                                            </PromptRefProvider>
-                                          </PromptHistoryProvider>
-                                        </FrecencyProvider>
-                                      </CommandProvider>
-                                    </DialogProvider>
-                                  </PromptStashProvider>
-                                </KeybindProvider>
-                              </LocalProvider>
-                            </ThemeProvider>
-                          </SyncProvider>
-                        </SDKProvider>
-                      </TuiConfigProvider>
-                    </RouteProvider>
-                  </ToastProvider>
-                </KVProvider>
-              </ExitProvider>
-            </ArgsProvider>
-          </ErrorBoundary>
-        )
-      },
-      {
-        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 (
+        <ErrorBoundary
+          fallback={(error, reset) => <ErrorComponent error={error} reset={reset} onExit={onExit} mode={mode} />}
+        >
+          <ArgsProvider {...input.args}>
+            <ExitProvider onExit={onExit}>
+              <KVProvider>
+                <ToastProvider>
+                  <RouteProvider>
+                    <TuiConfigProvider config={input.config}>
+                      <SDKProvider
+                        url={input.url}
+                        renderer={renderer}
+                        directory={input.directory}
+                        fetch={input.fetch}
+                        headers={input.headers}
+                        events={input.events}
+                      >
+                        <SyncProvider>
+                          <ThemeProvider mode={mode}>
+                            <LocalProvider>
+                              <KeybindProvider>
+                                <PromptStashProvider>
+                                  <DialogProvider>
+                                    <CommandProvider>
+                                      <FrecencyProvider>
+                                        <PromptHistoryProvider>
+                                          <PromptRefProvider>
+                                            <App />
+                                          </PromptRefProvider>
+                                        </PromptHistoryProvider>
+                                      </FrecencyProvider>
+                                    </CommandProvider>
+                                  </DialogProvider>
+                                </PromptStashProvider>
+                              </KeybindProvider>
+                            </LocalProvider>
+                          </ThemeProvider>
+                        </SyncProvider>
+                      </SDKProvider>
+                    </TuiConfigProvider>
+                  </RouteProvider>
+                </ToastProvider>
+              </KVProvider>
+            </ExitProvider>
+          </ArgsProvider>
+        </ErrorBoundary>
+      )
+    }, renderer)
   })
 }
 

+ 3 - 0
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)
     })

+ 29 - 0
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

+ 39 - 0
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,
+      })
+    },
+  })
+})

+ 6 - 3
packages/plugin/src/index.ts

@@ -66,16 +66,19 @@ export type TuiEventBus = {
   ) => () => void
 }
 
-export type TuiPluginInput = {
+export type TuiPluginInput<Renderer = unknown> = {
   client: ReturnType<typeof createOpencodeClientV2>
   event: TuiEventBus
   url: string
   directory?: string
+  renderer: Renderer
 }
 
-export type TuiPlugin = (input: TuiPluginInput, options?: PluginOptions) => Promise<void>
+export type TuiPlugin<Renderer = unknown> = (input: TuiPluginInput<Renderer>, options?: PluginOptions) => Promise<void>
 
-export type PluginModule = Plugin | { server?: Plugin; tui?: TuiPlugin; themes?: Record<string, ThemeJson> }
+export type PluginModule<Renderer = unknown> =
+  | Plugin
+  | { server?: Plugin; tui?: TuiPlugin<Renderer>; themes?: Record<string, ThemeJson> }
 
 export type AuthHook = {
   provider: string

+ 7 - 1
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).
 

+ 1 - 0
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