diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index cca2de24da..d15f0d8cf3 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -46,6 +46,8 @@ import { EditorContextProvider } from "./context/editor" import { useEvent } from "./context/event" import { ClientProvider, useClient } from "./context/client" import { StartupLoading } from "./component/startup-loading" +import { DevToolsSidebar } from "./component/devtools-sidebar" +import { DevTools } from "./devtools" import { Reconnecting } from "./component/reconnecting" import { DataProvider, useData } from "./context/data" import { LocationProvider, useLocation } from "./context/location" @@ -86,6 +88,8 @@ import { win32DisableProcessedInput, win32FlushInputBuffer } from "./terminal-wi import { destroyRenderer } from "./util/renderer" import { cliErrorMessage, errorFormat } from "./util/error" +const themePerformance = DevTools.register({ id: "theme-performance", title: "Theme performance" }) + registerOpencodeSpinner() const appGlobalBindingCommands = [ @@ -252,9 +256,12 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { const pluginRuntime = createPluginRuntime() yield* Effect.tryPromise(async () => { + const appStarted = performance.now() // Prewarm palette before ThemeProvider mounts so `system` theme avoids a first-paint fallback flash. void renderer.getPalette({ size: 16 }).catch(() => undefined) + const modeStarted = performance.now() const mode = handoff?.mode ?? (await renderer.waitForThemeMode(1000)) ?? "dark" + themePerformance.set("Detect light/dark mode", `${(performance.now() - modeStarted).toFixed(2)} ms`) if (renderer.isDestroyed) return await render(() => { @@ -342,6 +349,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { config.data.debug?.devtools ?? false) const route = useRoute() const dimensions = useTerminalDimensions() const renderer = useRenderer() @@ -421,6 +430,11 @@ function App(props: { pair?: DialogPairCredentials }) { const plugins = usePlugin() const clipboard = useClipboard() + createEffect(() => { + if (!themeState.ready) return + themePerformance.set("Total", `${(performance.now() - props.started).toFixed(2)} ms`) + }) + // Toast once when an MCP server enters a failed or needs-auth state so the user knows to act, // without having to open the status panel. Tracking the last alerted status avoids re-toasting // the same problem on every refresh while still re-alerting if the state changes. @@ -1086,31 +1100,38 @@ function App(props: { pair?: DialogPairCredentials }) { - - - - - - - - - {(_) => } - - - - ( - route.navigate({ type: "home" })} /> - )} - /> - - + + + + + + + + + + + {(_) => } + + + + ( + route.navigate({ type: "home" })} /> + )} + /> + + + + + + + + - - - - - + + + + diff --git a/packages/tui/src/component/devtools-sidebar.tsx b/packages/tui/src/component/devtools-sidebar.tsx new file mode 100644 index 0000000000..e47edb9a36 --- /dev/null +++ b/packages/tui/src/component/devtools-sidebar.tsx @@ -0,0 +1,41 @@ +import { TextAttributes } from "@opentui/core" +import { For } from "solid-js" +import { useTheme } from "../context/theme" +import { DevTools } from "../devtools" + +export function DevToolsSidebar() { + const { themeV2 } = useTheme().contextual("elevated") + + return ( + + + {(group) => ( + + + + {group.title} + + + + {(entry) => ( + + {entry.key} + + {String(entry.value)} + + )} + + + )} + + + ) +} diff --git a/packages/tui/src/component/dialog-config.tsx b/packages/tui/src/component/dialog-config.tsx index c63e6e5d87..01cdb950dc 100644 --- a/packages/tui/src/component/dialog-config.tsx +++ b/packages/tui/src/component/dialog-config.tsx @@ -206,6 +206,14 @@ const settings: Setting[] = [ values: [false, true], labels: ["off", "on"], }, + { + title: "DevTools", + category: "Debug", + path: ["debug", "devtools"], + default: false, + values: [false, true], + labels: ["off", "on"], + }, ] export function DialogConfig() { diff --git a/packages/tui/src/component/logo.tsx b/packages/tui/src/component/logo.tsx index bc24b3c553..0acaa44882 100644 --- a/packages/tui/src/component/logo.tsx +++ b/packages/tui/src/component/logo.tsx @@ -5,10 +5,10 @@ import { tint } from "../theme/color" import { logo } from "../logo" export function Logo() { - const { theme } = useTheme() + const { themeV2 } = useTheme() const renderLine = (line: string, fg: RGBA, bold: boolean): JSX.Element[] => { - const shadow = tint(theme.background, fg, 0.25) + const shadow = tint(themeV2.background(), fg, 0.25) const attrs = bold ? TextAttributes.BOLD : undefined return Array.from(line).map((char) => { if (char === "_") { @@ -52,8 +52,8 @@ export function Logo() { {(line, index) => ( - {renderLine(line, theme.textMuted, false)} - {renderLine(logo.right[index()], theme.text, true)} + {renderLine(line, themeV2.text.subdued(), false)} + {renderLine(logo.right[index()], themeV2.text(), true)} )} diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index fb0a6e2fca..3688ef3ef7 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -190,7 +190,7 @@ export function Prompt(props: PromptProps) { const renderer = useRenderer() const exit = useExit() const dimensions = useTerminalDimensions() - const { theme, syntax } = useTheme() + const { themeV2, syntax } = useTheme() const animationsEnabled = createMemo(() => config.animations ?? true) const list = createMemo(() => props.placeholders?.normal ?? []) const shell = createMemo(() => props.placeholders?.shell ?? []) @@ -297,8 +297,8 @@ export function Prompt(props: PromptProps) { createEffect(() => { if (!input || input.isDestroyed) return - if (props.disabled) input.cursorColor = theme.backgroundElement - if (!props.disabled) input.cursorColor = theme.text + if (props.disabled) input.cursorColor = themeV2.background.surface.offset() + if (!props.disabled) input.cursorColor = themeV2.text() }) const usage = createMemo(() => { @@ -1306,10 +1306,10 @@ export function Prompt(props: PromptProps) { } const highlight = createMemo(() => { - if (leader()) return theme.border - if (store.mode === "shell") return theme.primary + if (leader()) return themeV2.border() + if (store.mode === "shell") return themeV2.background.action.primary() const agent = local.agent.current() - if (!agent) return theme.border + if (!agent) return themeV2.border() return local.agent.color(agent.id) }) @@ -1326,7 +1326,7 @@ export function Prompt(props: PromptProps) { () => !!local.agent.current() && store.mode === "normal" && showVariant(), animationsEnabled, ) - const borderHighlight = createMemo(() => tint(theme.border, highlight(), agentMetaAlpha())) + const borderHighlight = createMemo(() => tint(themeV2.border(), highlight(), agentMetaAlpha())) const placeholderText = createMemo(() => { if (props.showPlaceholder === false) return undefined @@ -1346,7 +1346,7 @@ export function Prompt(props: PromptProps) { const spinnerDef = createMemo(() => { const agent = status() === "running" ? local.agent.current() : local.agent.current() - const color = agent ? local.agent.color(agent.id) : theme.border + const color = agent ? local.agent.color(agent.id) : themeV2.border() return { frames: createFrames({ color, @@ -1383,16 +1383,16 @@ export function Prompt(props: PromptProps) { paddingRight={2} paddingTop={1} flexShrink={0} - backgroundColor={theme.backgroundElement} + backgroundColor={themeV2.background.action.secondary("focused")} flexGrow={1} width="100%" >