From 5d438eaba9a43accda46126e14b93fa261955044 Mon Sep 17 00:00:00 2001 From: Pjotr van der Horst Date: Fri, 17 Apr 2026 15:44:35 +0200 Subject: [PATCH] feat(tui): add logo_animation toggle for home and Go logos --- .../cmd/tui/component/dialog-go-upsell.tsx | 4 +- .../src/cli/cmd/tui/component/logo.tsx | 12 +++--- .../src/cli/cmd/tui/config/tui-migrate.ts | 4 +- .../src/cli/cmd/tui/config/tui-schema.ts | 1 + .../opencode/src/cli/cmd/tui/routes/home.tsx | 4 +- packages/opencode/test/config/tui.test.ts | 37 +++++++++++++++++++ packages/web/src/content/docs/tui.mdx | 4 +- 7 files changed, 57 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-go-upsell.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-go-upsell.tsx index ace4b090bca5..c5a9ef05bc00 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-go-upsell.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-go-upsell.tsx @@ -3,6 +3,7 @@ import { useKeyboard } from "@opentui/solid" import open from "open" import { createSignal, onCleanup, onMount } from "solid-js" import { selectedForeground, useTheme } from "@tui/context/theme" +import { useTuiConfig } from "@tui/context/tui-config" import { useDialog, type DialogContext } from "@tui/ui/dialog" import { Link } from "@tui/ui/link" import { GoLogo } from "./logo" @@ -30,6 +31,7 @@ function dismiss(props: DialogGoUpsellProps, dialog: ReturnType("subscribe") const [center, setCenter] = createSignal<{ x: number; y: number } | undefined>() @@ -108,7 +110,7 @@ export function DialogGoUpsell(props: DialogGoUpsellProps) { (logoBox = item)}> - + diff --git a/packages/opencode/src/cli/cmd/tui/component/logo.tsx b/packages/opencode/src/cli/cmd/tui/component/logo.tsx index bee104a35d3e..1439682a5350 100644 --- a/packages/opencode/src/cli/cmd/tui/component/logo.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/logo.tsx @@ -551,9 +551,10 @@ function buildIdleState(t: number, ctx: LogoContext): IdleState { return { cfg, reach, rings, active } } -export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } = {}) { +export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean; animate?: boolean } = {}) { const ctx = props.shape ? build(props.shape) : DEFAULT const { theme } = useTheme() + const animationEnabled = props.animate ?? true const [rings, setRings] = createSignal([]) const [hold, setHold] = createSignal() const [release, setRelease] = createSignal() @@ -608,7 +609,7 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } = }) onMount(() => { - if (!props.idle) return + if (!props.idle || !animationEnabled) return setNow(performance.now()) start() }) @@ -683,7 +684,7 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } = } }) - const idleState = createMemo(() => (props.idle ? buildIdleState(frame().t, ctx) : undefined)) + const idleState = createMemo(() => (props.idle && animationEnabled ? buildIdleState(frame().t, ctx) : undefined)) const renderLine = ( line: string, @@ -829,6 +830,7 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } = } const mouse = (evt: MouseEvent) => { + if (!animationEnabled) return if (!box) return if ((evt.type === "down" || evt.type === "drag") && evt.button === MouseButton.LEFT) { const x = evt.x - box.x @@ -886,8 +888,8 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } = ) } -export function GoLogo() { +export function GoLogo(props: { animate?: boolean } = {}) { const { theme } = useTheme() const base = tint(theme.background, theme.text, 0.62) - return + return } diff --git a/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts b/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts index a7f50ddf9dc6..fe5537b76693 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts @@ -20,6 +20,7 @@ const TuiLegacy = z scroll_speed: TuiOptions.shape.scroll_speed.catch(undefined), scroll_acceleration: TuiOptions.shape.scroll_acceleration.catch(undefined), diff_style: TuiOptions.shape.diff_style.catch(undefined), + logo_animation: TuiOptions.shape.logo_animation.catch(undefined), }) .strip() @@ -89,7 +90,8 @@ function normalizeTui(data: Record) { if ( parsed.scroll_speed === undefined && parsed.diff_style === undefined && - parsed.scroll_acceleration === undefined + parsed.scroll_acceleration === undefined && + parsed.logo_animation === undefined ) { return } diff --git a/packages/opencode/src/cli/cmd/tui/config/tui-schema.ts b/packages/opencode/src/cli/cmd/tui/config/tui-schema.ts index 66569efea5b3..056c081f17ca 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui-schema.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui-schema.ts @@ -24,6 +24,7 @@ export const TuiOptions = z.object({ .optional() .describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"), mouse: z.boolean().optional().describe("Enable or disable mouse capture (default: true)"), + logo_animation: z.boolean().optional().describe("Enable or disable logo animation and interaction (default: true)"), }) export const TuiInfo = z diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index 2f0ff07e9a9c..645b2cad7b5d 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -8,6 +8,7 @@ import { useArgs } from "../context/args" import { useRouteData } from "@tui/context/route" import { usePromptRef } from "../context/prompt" import { useLocal } from "../context/local" +import { useTuiConfig } from "../context/tui-config" import { TuiPluginRuntime } from "../plugin" let once = false @@ -24,6 +25,7 @@ export function Home() { const [ref, setRef] = createSignal() const args = useArgs() const local = useLocal() + const tuiConfig = useTuiConfig() let sent = false const bind = (r: PromptRef | undefined) => { @@ -59,7 +61,7 @@ export function Home() { - + diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts index c7b6d4a50494..e44315c1c4e8 100644 --- a/packages/opencode/test/config/tui.test.ts +++ b/packages/opencode/test/config/tui.test.ts @@ -126,6 +126,17 @@ test("loads tui config with the same precedence order as server config paths", a expect(config.diff_style).toBe("stacked") }) +test("loads logo animation toggle from tui.json", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ logo_animation: false }, null, 2)) + }, + }) + + const config = await getTuiConfig(tmp.path) + expect(config.logo_animation).toBe(false) +}) + test("migrates tui-specific keys from opencode.json when tui.json does not exist", async () => { await using tmp = await tmpdir({ init: async (dir) => { @@ -161,6 +172,32 @@ test("migrates tui-specific keys from opencode.json when tui.json does not exist expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true) }) +test("migrates legacy logo_animation key from opencode.json", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify( + { + tui: { logo_animation: false }, + }, + null, + 2, + ), + ) + }, + }) + + const config = await getTuiConfig(tmp.path) + expect(config.logo_animation).toBe(false) + const text = await Filesystem.readText(path.join(tmp.path, "tui.json")) + expect(JSON.parse(text)).toMatchObject({ + logo_animation: false, + }) + const server = JSON.parse(await Filesystem.readText(path.join(tmp.path, "opencode.json"))) + expect(server.tui).toBeUndefined() +}) + test("migrates project legacy tui keys even when global tui.json already exists", async () => { await using tmp = await tmpdir({ init: async (dir) => { diff --git a/packages/web/src/content/docs/tui.mdx b/packages/web/src/content/docs/tui.mdx index e89fb4af39ef..7d2d3b443cc2 100644 --- a/packages/web/src/content/docs/tui.mdx +++ b/packages/web/src/content/docs/tui.mdx @@ -369,7 +369,8 @@ You can customize TUI behavior through `tui.json` (or `tui.jsonc`). "enabled": true }, "diff_style": "auto", - "mouse": true + "mouse": true, + "logo_animation": false } ``` @@ -383,6 +384,7 @@ This is separate from `opencode.json`, which configures server/runtime behavior. - `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. - `mouse` - Enable or disable mouse capture in the TUI (default: `true`). When disabled, the terminal's native mouse selection/scrolling behavior is preserved. +- `logo_animation` - Enable or disable logo animation/interaction globally (default: `true`). Set to `false` to keep logos static and disable burst/sound effects. Use `OPENCODE_TUI_CONFIG` to load a custom TUI config path.