Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -30,6 +31,7 @@ function dismiss(props: DialogGoUpsellProps, dialog: ReturnType<typeof useDialog
export function DialogGoUpsell(props: DialogGoUpsellProps) {
const dialog = useDialog()
const { theme } = useTheme()
const tuiConfig = useTuiConfig()
const fg = selectedForeground(theme)
const [selected, setSelected] = createSignal<"dismiss" | "subscribe">("subscribe")
const [center, setCenter] = createSignal<{ x: number; y: number } | undefined>()
Expand Down Expand Up @@ -108,7 +110,7 @@ export function DialogGoUpsell(props: DialogGoUpsellProps) {
</box>
<box alignItems="center" gap={1} paddingBottom={1}>
<box ref={(item: BoxRenderable) => (logoBox = item)}>
<GoLogo />
<GoLogo animate={tuiConfig.logo_animation ?? true} />
</box>
<Link href={GO_URL} fg={theme.primary} />
</box>
Expand Down
12 changes: 7 additions & 5 deletions packages/opencode/src/cli/cmd/tui/component/logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Ring[]>([])
const [hold, setHold] = createSignal<Hold>()
const [release, setRelease] = createSignal<Release>()
Expand Down Expand Up @@ -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()
})
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 <Logo shape={go} ink={base} idle />
return <Logo shape={go} ink={base} idle animate={props.animate} />
}
4 changes: 3 additions & 1 deletion packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -89,7 +90,8 @@ function normalizeTui(data: Record<string, unknown>) {
if (
parsed.scroll_speed === undefined &&
parsed.diff_style === undefined &&
parsed.scroll_acceleration === undefined
parsed.scroll_acceleration === undefined &&
parsed.logo_animation === undefined
) {
return
}
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/cli/cmd/tui/config/tui-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion packages/opencode/src/cli/cmd/tui/routes/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,6 +25,7 @@ export function Home() {
const [ref, setRef] = createSignal<PromptRef | undefined>()
const args = useArgs()
const local = useLocal()
const tuiConfig = useTuiConfig()
let sent = false

const bind = (r: PromptRef | undefined) => {
Expand Down Expand Up @@ -59,7 +61,7 @@ export function Home() {
<box height={4} minHeight={0} flexShrink={1} />
<box flexShrink={0}>
<TuiPluginRuntime.Slot name="home_logo" mode="replace">
<Logo />
<Logo animate={tuiConfig.logo_animation ?? true} />
</TuiPluginRuntime.Slot>
</box>
<box height={1} minHeight={0} flexShrink={1} />
Expand Down
37 changes: 37 additions & 0 deletions packages/opencode/test/config/tui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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) => {
Expand Down
4 changes: 3 additions & 1 deletion packages/web/src/content/docs/tui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
```

Expand All @@ -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.

Expand Down
Loading