From a1d3be7510399519c097ed09da66cc9f65e14082 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 11 Sep 2026 19:32:13 -0500 Subject: [PATCH] fix(tui): render the home prompt before plugins settle Mount the production Home prompt as soon as the built-in theme is ready instead of waiting for plugin reconciliation. Reserve the responsive logo and footer geometry while plugins load, then fill that chrome without remounting or moving the focused prompt. Session and continue launches remain gated. Delay terminal-title output until after the first frame so it does not serialize ahead of useful terminal output. In compiled interleaved runs, prompt bytes improve from 215.5ms to 168ms and first accepted input from 124ms to 76ms. Full chrome settles roughly 15ms later. --- packages/tui/src/app.tsx | 24 +++- packages/tui/src/component/logo.tsx | 22 +++- packages/tui/src/context/theme.tsx | 3 +- .../tui/src/feature-plugins/home/footer.tsx | 16 +-- packages/tui/src/routes/home.tsx | 25 +++- packages/tui/src/ui/layout.ts | 13 ++ packages/tui/test/app-lifecycle.test.tsx | 111 +++++++++++++++++- packages/tui/test/cli/tui/theme-mode.test.tsx | 48 ++++++++ packages/tui/test/component/logo.test.ts | 10 ++ .../test/feature-plugins/home-footer.test.ts | 10 +- packages/tui/test/fixture/app.ts | 3 +- 11 files changed, 257 insertions(+), 28 deletions(-) create mode 100644 packages/tui/test/component/logo.test.ts diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 6cb331f7371f..9afc18eacc95 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -494,14 +494,22 @@ function App(props: { pair?: DialogPairCredentials }) { const clipboard = useClipboard() const terminalEnvironment = useTuiTerminalEnvironment() let systemThemeTimeout: ReturnType | undefined + let terminalTitleTimeout: ReturnType | undefined + const [terminalTitleReady, setTerminalTitleReady] = createSignal(false) const prepareSystemTheme = () => { // The native writer can still be flushing the frame when FRAME fires. Keep OSC probes behind visible app output. systemThemeTimeout = setTimeout(themes.prepareSystem, 50) } - onMount(() => renderer.once(CliRenderEvents.FRAME, prepareSystemTheme)) + const finishFirstFrame = () => { + // Native terminal updates serialize behind frame output, so keep them from forcing an empty frame ahead of Home. + prepareSystemTheme() + terminalTitleTimeout = setTimeout(() => setTerminalTitleReady(true), 50) + } + onMount(() => renderer.once(CliRenderEvents.FRAME, finishFirstFrame)) onCleanup(() => { - renderer.off(CliRenderEvents.FRAME, prepareSystemTheme) + renderer.off(CliRenderEvents.FRAME, finishFirstFrame) if (systemThemeTimeout) clearTimeout(systemThemeTimeout) + if (terminalTitleTimeout) clearTimeout(terminalTitleTimeout) }) createEffect(() => { if (client.connection.status() !== "connected") return @@ -616,6 +624,7 @@ function App(props: { pair?: DialogPairCredentials }) { const session = route.data.type === "session" ? data.session.get(route.data.sessionID) : undefined if (session) active = { id: session.id, title: session.title } if (!terminalTitleEnabled()) return + if (!terminalTitleReady()) return if (route.data.type === "home") { renderer.setTerminalTitle("OpenCode") @@ -639,6 +648,7 @@ function App(props: { pair?: DialogPairCredentials }) { }) const args = useArgs() + const promptFirstHome = () => route.data.type === "home" && !args.sessionID && !args.continue const startupPrompt = args.prompt ? { text: args.prompt, files: [], agents: [], pasted: [] } : undefined onMount(() => { batch(() => { @@ -1323,14 +1333,14 @@ function App(props: { pair?: DialogPairCredentials }) { - + - + @@ -1351,7 +1361,9 @@ function App(props: { pair?: DialogPairCredentials }) { - + + + @@ -1361,7 +1373,7 @@ function App(props: { pair?: DialogPairCredentials }) { - + diff --git a/packages/tui/src/component/logo.tsx b/packages/tui/src/component/logo.tsx index 32b5e96b1f1a..d1163d07a90d 100644 --- a/packages/tui/src/component/logo.tsx +++ b/packages/tui/src/component/logo.tsx @@ -4,6 +4,26 @@ import { useTerminalDimensions } from "@opentui/solid" import { useTheme } from "../context/theme" import { tint } from "../theme/color" import { go, logo } from "../logo" +import { stringWidth } from "../util/string-width" + +export function logoSize(width: number, height: number) { + if (height < 12) return { width: 0, height: 0 } + if (width < 22) + return { + width: Math.max(...go.right.slice(1).map((line) => stringWidth(line))), + height: go.right.length - 1, + } + if (width < 44) { + const lines = [...logo.left.slice(1), ...logo.right] + return { width: Math.max(...lines.map((line) => stringWidth(line))), height: lines.length } + } + return { + width: Math.max( + ...logo.left.map((line, index) => stringWidth(line) + 1 + stringWidth(logo.right[index] ?? "")), + ), + height: logo.left.length, + } +} export function Logo() { const theme = useTheme() @@ -50,7 +70,7 @@ export function Logo() { } return ( - +