Light mode (#3709)

This commit is contained in:
Dax
2025-11-01 13:54:01 -04:00
committed by GitHub
parent f98e730405
commit 104a895a71
9 changed files with 677 additions and 580 deletions

View File

@@ -29,6 +29,63 @@ import { Session as SessionApi } from "@/session"
import { TuiEvent } from "./event" import { TuiEvent } from "./event"
import { KVProvider, useKV } from "./context/kv" import { KVProvider, useKV } from "./context/kv"
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
return new Promise((resolve) => {
let timeout: NodeJS.Timeout
const cleanup = () => {
process.stdin.setRawMode(false)
process.stdin.removeListener("data", handler)
clearTimeout(timeout)
}
const handler = (data: Buffer) => {
const str = data.toString()
const match = str.match(/\x1b]11;([^\x07\x1b]+)/)
if (match) {
cleanup()
const color = match[1]
// Parse RGB values from color string
// Formats: rgb:RR/GG/BB or #RRGGBB or rgb(R,G,B)
let r = 0,
g = 0,
b = 0
if (color.startsWith("rgb:")) {
const parts = color.substring(4).split("/")
r = parseInt(parts[0], 16) >> 8 // Convert 16-bit to 8-bit
g = parseInt(parts[1], 16) >> 8 // Convert 16-bit to 8-bit
b = parseInt(parts[2], 16) >> 8 // Convert 16-bit to 8-bit
} else if (color.startsWith("#")) {
r = parseInt(color.substring(1, 3), 16)
g = parseInt(color.substring(3, 5), 16)
b = parseInt(color.substring(5, 7), 16)
} else if (color.startsWith("rgb(")) {
const parts = color.substring(4, color.length - 1).split(",")
r = parseInt(parts[0])
g = parseInt(parts[1])
b = parseInt(parts[2])
}
// Calculate luminance using relative luminance formula
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
// Determine if dark or light based on luminance threshold
resolve(luminance > 0.5 ? "light" : "dark")
}
}
process.stdin.setRawMode(true)
process.stdin.on("data", handler)
process.stdout.write("\x1b]11;?\x07")
timeout = setTimeout(() => {
cleanup()
resolve("dark")
}, 1000)
})
}
export function tui(input: { export function tui(input: {
url: string url: string
sessionID?: string sessionID?: string
@@ -38,7 +95,9 @@ export function tui(input: {
onExit?: () => Promise<void> onExit?: () => Promise<void>
}) { }) {
// promise to prevent immediate exit // promise to prevent immediate exit
return new Promise<void>((resolve) => { return new Promise<void>(async (resolve) => {
const mode = await getTerminalBackgroundColor()
const routeData: Route | undefined = input.sessionID const routeData: Route | undefined = input.sessionID
? { ? {
type: "session", type: "session",
@@ -65,8 +124,12 @@ export function tui(input: {
<RouteProvider data={routeData}> <RouteProvider data={routeData}>
<SDKProvider url={input.url}> <SDKProvider url={input.url}>
<SyncProvider> <SyncProvider>
<ThemeProvider> <ThemeProvider mode={mode}>
<LocalProvider initialModel={input.model} initialAgent={input.agent} initialPrompt={input.prompt}> <LocalProvider
initialModel={input.model}
initialAgent={input.agent}
initialPrompt={input.prompt}
>
<KeybindProvider> <KeybindProvider>
<DialogProvider> <DialogProvider>
<CommandProvider> <CommandProvider>
@@ -109,7 +172,7 @@ function App() {
const sync = useSync() const sync = useSync()
const toast = useToast() const toast = useToast()
const [sessionExists, setSessionExists] = createSignal(false) const [sessionExists, setSessionExists] = createSignal(false)
const { theme } = useTheme() const { theme, mode, setMode } = useTheme()
const exit = useExit() const exit = useExit()
useKeyboard(async (evt) => { useKeyboard(async (evt) => {
@@ -238,6 +301,14 @@ function App() {
}, },
category: "System", category: "System",
}, },
{
title: `Switch to ${mode() === "dark" ? "light" : "dark"} mode`,
value: "theme.switch_mode",
onSelect: () => {
setMode(mode() === "dark" ? "light" : "dark")
},
category: "System",
},
{ {
title: "Help", title: "Help",
value: "help.show", value: "help.show",
@@ -251,7 +322,7 @@ function App() {
value: "app.exit", value: "app.exit",
onSelect: exit, onSelect: exit,
category: "System", category: "System",
} },
]) ])
createEffect(() => { createEffect(() => {
@@ -335,7 +406,9 @@ function App() {
paddingRight={1} paddingRight={1}
> >
<text fg={theme.textMuted}>open</text> <text fg={theme.textMuted}>open</text>
<text attributes={TextAttributes.BOLD}>code </text> <text fg={theme.text} attributes={TextAttributes.BOLD}>
code{" "}
</text>
<text fg={theme.textMuted}>v{Installation.VERSION}</text> <text fg={theme.textMuted}>v{Installation.VERSION}</text>
</box> </box>
<box paddingLeft={1} paddingRight={1}> <box paddingLeft={1} paddingRight={1}>

View File

@@ -14,12 +14,14 @@ export function DialogStatus() {
return ( return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}> <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD}>Status</text> <text fg={theme.text} attributes={TextAttributes.BOLD}>
Status
</text>
<text fg={theme.textMuted}>esc</text> <text fg={theme.textMuted}>esc</text>
</box> </box>
<Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text>No MCP Servers</text>}> <Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text>No MCP Servers</text>}>
<box> <box>
<text>{Object.keys(sync.data.mcp).length} MCP Servers</text> <text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
<For each={Object.entries(sync.data.mcp)}> <For each={Object.entries(sync.data.mcp)}>
{([key, item]) => ( {([key, item]) => (
<box flexDirection="row" gap={1}> <box flexDirection="row" gap={1}>
@@ -35,7 +37,7 @@ export function DialogStatus() {
> >
</text> </text>
<text wrapMode="word"> <text fg={theme.text} wrapMode="word">
<b>{key}</b>{" "} <b>{key}</b>{" "}
<span style={{ fg: theme.textMuted }}> <span style={{ fg: theme.textMuted }}>
<Switch> <Switch>
@@ -52,7 +54,7 @@ export function DialogStatus() {
</Show> </Show>
{sync.data.lsp.length > 0 && ( {sync.data.lsp.length > 0 && (
<box> <box>
<text>{sync.data.lsp.length} LSP Servers</text> <text fg={theme.text}>{sync.data.lsp.length} LSP Servers</text>
<For each={sync.data.lsp}> <For each={sync.data.lsp}>
{(item) => ( {(item) => (
<box flexDirection="row" gap={1}> <box flexDirection="row" gap={1}>
@@ -67,7 +69,7 @@ export function DialogStatus() {
> >
</text> </text>
<text wrapMode="word"> <text fg={theme.text} wrapMode="word">
<b>{item.id}</b> <span style={{ fg: theme.textMuted }}>{item.root}</span> <b>{item.id}</b> <span style={{ fg: theme.textMuted }}>{item.root}</span>
</text> </text>
</box> </box>
@@ -75,9 +77,12 @@ export function DialogStatus() {
</For> </For>
</box> </box>
)} )}
<Show when={enabledFormatters().length > 0} fallback={<text>No Formatters</text>}> <Show
when={enabledFormatters().length > 0}
fallback={<text fg={theme.text}>No Formatters</text>}
>
<box> <box>
<text>{enabledFormatters().length} Formatters</text> <text fg={theme.text}>{enabledFormatters().length} Formatters</text>
<For each={enabledFormatters()}> <For each={enabledFormatters()}>
{(item) => ( {(item) => (
<box flexDirection="row" gap={1}> <box flexDirection="row" gap={1}>
@@ -89,7 +94,7 @@ export function DialogStatus() {
> >
</text> </text>
<text wrapMode="word"> <text wrapMode="word" fg={theme.text}>
<b>{item.name}</b> <b>{item.name}</b>
</text> </text>
</box> </box>

View File

@@ -11,7 +11,7 @@ import {
} from "@opentui/core" } from "@opentui/core"
import { createEffect, createMemo, Match, Switch, type JSX, onMount, batch } from "solid-js" import { createEffect, createMemo, Match, Switch, type JSX, onMount, batch } from "solid-js"
import { useLocal } from "@tui/context/local" import { useLocal } from "@tui/context/local"
import { SyntaxTheme, useTheme } from "@tui/context/theme" import { useTheme } from "@tui/context/theme"
import { SplitBorder } from "@tui/component/border" import { SplitBorder } from "@tui/component/border"
import { useSDK } from "@tui/context/sdk" import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route" import { useRoute } from "@tui/context/route"
@@ -60,7 +60,7 @@ export function Prompt(props: PromptProps) {
const history = usePromptHistory() const history = usePromptHistory()
const command = useCommandDialog() const command = useCommandDialog()
const renderer = useRenderer() const renderer = useRenderer()
const { theme } = useTheme() const { theme, syntax } = useTheme()
const textareaKeybindings = createMemo(() => { const textareaKeybindings = createMemo(() => {
const newlineBindings = keybind.all.input_newline || [] const newlineBindings = keybind.all.input_newline || []
@@ -86,9 +86,9 @@ export function Prompt(props: PromptProps) {
] ]
}) })
const fileStyleId = SyntaxTheme.getStyleId("extmark.file")! const fileStyleId = syntax().getStyleId("extmark.file")!
const agentStyleId = SyntaxTheme.getStyleId("extmark.agent")! const agentStyleId = syntax().getStyleId("extmark.agent")!
const pasteStyleId = SyntaxTheme.getStyleId("extmark.paste")! const pasteStyleId = syntax().getStyleId("extmark.paste")!
let promptPartTypeId: number let promptPartTypeId: number
command.register(() => { command.register(() => {
@@ -315,9 +315,9 @@ export function Prompt(props: PromptProps) {
const sessionID = props.sessionID const sessionID = props.sessionID
? props.sessionID ? props.sessionID
: await (async () => { : await (async () => {
const sessionID = await sdk.client.session.create({}).then((x) => x.data!.id) const sessionID = await sdk.client.session.create({}).then((x) => x.data!.id)
return sessionID return sessionID
})() })()
const messageID = Identifier.ascending("message") const messageID = Identifier.ascending("message")
let inputText = store.prompt.input let inputText = store.prompt.input
@@ -680,7 +680,7 @@ export function Prompt(props: PromptProps) {
onMouseDown={(r: MouseEvent) => r.target?.focus()} onMouseDown={(r: MouseEvent) => r.target?.focus()}
focusedBackgroundColor={theme.backgroundElement} focusedBackgroundColor={theme.backgroundElement}
cursorColor={theme.primary} cursorColor={theme.primary}
syntaxStyle={SyntaxTheme} syntaxStyle={syntax()}
/> />
</box> </box>
<box <box
@@ -691,7 +691,7 @@ export function Prompt(props: PromptProps) {
></box> ></box>
</box> </box>
<box flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">
<text flexShrink={0} wrapMode="none"> <text flexShrink={0} wrapMode="none" fg={theme.text}>
<span style={{ fg: theme.textMuted }}>{local.model.parsed().provider}</span>{" "} <span style={{ fg: theme.textMuted }}>{local.model.parsed().provider}</span>{" "}
<span style={{ bold: true }}>{local.model.parsed().model}</span> <span style={{ bold: true }}>{local.model.parsed().model}</span>
</text> </text>
@@ -701,14 +701,14 @@ export function Prompt(props: PromptProps) {
</Match> </Match>
<Match when={status() === "working"}> <Match when={status() === "working"}>
<box flexDirection="row" gap={1}> <box flexDirection="row" gap={1}>
<text> <text fg={theme.text}>
esc <span style={{ fg: theme.textMuted }}>interrupt</span> esc <span style={{ fg: theme.textMuted }}>interrupt</span>
</text> </text>
</box> </box>
</Match> </Match>
<Match when={props.hint}>{props.hint!}</Match> <Match when={props.hint}>{props.hint!}</Match>
<Match when={true}> <Match when={true}>
<text> <text fg={theme.text}>
ctrl+p <span style={{ fg: theme.textMuted }}>commands</span> ctrl+p <span style={{ fg: theme.textMuted }}>commands</span>
</text> </text>
</Match> </Match>

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@ export function Home() {
const Hint = ( const Hint = (
<Show when={Object.keys(sync.data.mcp).length > 0}> <Show when={Object.keys(sync.data.mcp).length > 0}>
<box flexShrink={0} flexDirection="row" gap={1}> <box flexShrink={0} flexDirection="row" gap={1}>
<text> <text fg={theme.text}>
<Switch> <Switch>
<Match when={mcpError()}> <Match when={mcpError()}>
<span style={{ fg: theme.error }}></span> mcp errors{" "} <span style={{ fg: theme.error }}></span> mcp errors{" "}
@@ -76,7 +76,7 @@ function HelpRow(props: ParentProps<{ keybind: keyof KeybindsConfig }>) {
const { theme } = useTheme() const { theme } = useTheme()
return ( return (
<box flexDirection="row" justifyContent="space-between" width="100%"> <box flexDirection="row" justifyContent="space-between" width="100%">
<text>{props.children}</text> <text fg={theme.text}>{props.children}</text>
<text fg={theme.primary}>{keybind.print(props.keybind)}</text> <text fg={theme.primary}>{keybind.print(props.keybind)}</text>
</box> </box>
) )

View File

@@ -51,7 +51,7 @@ export function Header() {
borderColor={theme.backgroundElement} borderColor={theme.backgroundElement}
flexShrink={0} flexShrink={0}
> >
<text> <text fg={theme.text}>
<span style={{ bold: true, fg: theme.accent }}>#</span>{" "} <span style={{ bold: true, fg: theme.accent }}>#</span>{" "}
<span style={{ bold: true }}>{session().title}</span> <span style={{ bold: true }}>{session().title}</span>
</text> </text>
@@ -64,7 +64,7 @@ export function Header() {
</text> </text>
</Match> </Match>
<Match when={true}> <Match when={true}>
<text wrapMode="word"> <text fg={theme.text} wrapMode="word">
/share <span style={{ fg: theme.textMuted }}>to create a shareable link</span> /share <span style={{ fg: theme.textMuted }}>to create a shareable link</span>
</text> </text>
</Match> </Match>

View File

@@ -15,7 +15,7 @@ import path from "path"
import { useRouteData } from "@tui/context/route" import { useRouteData } from "@tui/context/route"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
import { SplitBorder } from "@tui/component/border" import { SplitBorder } from "@tui/component/border"
import { SyntaxTheme, useTheme } from "@tui/context/theme" import { useTheme } from "@tui/context/theme"
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers } from "@opentui/core" import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers } from "@opentui/core"
import { Prompt, type PromptRef } from "@tui/component/prompt" import { Prompt, type PromptRef } from "@tui/component/prompt"
import type { import type {
@@ -641,7 +641,7 @@ function UserMessage(props: {
borderColor={color()} borderColor={color()}
flexShrink={0} flexShrink={0}
> >
<text>{text()?.text}</text> <text fg={theme.text}>{text()?.text}</text>
<Show when={files().length}> <Show when={files().length}>
<box flexDirection="row" paddingBottom={1} paddingTop={1} gap={1} flexWrap="wrap"> <box flexDirection="row" paddingBottom={1} paddingTop={1} gap={1} flexWrap="wrap">
<For each={files()}> <For each={files()}>
@@ -652,7 +652,7 @@ function UserMessage(props: {
return theme.secondary return theme.secondary
}) })
return ( return (
<text> <text fg={theme.text}>
<span style={{ bg: bg(), fg: theme.background }}> <span style={{ bg: bg(), fg: theme.background }}>
{" "} {" "}
{MIME_BADGE[file.mime] ?? file.mime}{" "} {MIME_BADGE[file.mime] ?? file.mime}{" "}
@@ -667,7 +667,7 @@ function UserMessage(props: {
</For> </For>
</box> </box>
</Show> </Show>
<text> <text fg={theme.text}>
{sync.data.config.username ?? "You"}{" "} {sync.data.config.username ?? "You"}{" "}
<Show <Show
when={queued()} when={queued()}
@@ -782,7 +782,7 @@ function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }
paddingLeft={2} paddingLeft={2}
backgroundColor={theme.backgroundPanel} backgroundColor={theme.backgroundPanel}
> >
<text>{props.part.text.trim()}</text> <text fg={theme.text}>{props.part.text.trim()}</text>
</box> </box>
</box> </box>
</Show> </Show>
@@ -791,13 +791,14 @@ function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }
function TextPart(props: { part: TextPart; message: AssistantMessage }) { function TextPart(props: { part: TextPart; message: AssistantMessage }) {
const ctx = use() const ctx = use()
const { syntax } = useTheme()
return ( return (
<Show when={props.part.text.trim()}> <Show when={props.part.text.trim()}>
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}> <box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
<code <code
filetype="markdown" filetype="markdown"
drawUnstyledText={false} drawUnstyledText={false}
syntaxStyle={SyntaxTheme} syntaxStyle={syntax()}
content={props.part.text.trim()} content={props.part.text.trim()}
conceal={ctx.conceal()} conceal={ctx.conceal()}
/> />
@@ -997,7 +998,7 @@ ToolRegistry.register<typeof WriteTool>({
name: "write", name: "write",
container: "block", container: "block",
render(props) { render(props) {
const { theme } = useTheme() const { theme, syntax } = useTheme()
const lines = createMemo(() => { const lines = createMemo(() => {
return props.input.content?.split("\n") ?? [] return props.input.content?.split("\n") ?? []
}) })
@@ -1028,7 +1029,7 @@ ToolRegistry.register<typeof WriteTool>({
<box paddingLeft={1} flexGrow={1}> <box paddingLeft={1} flexGrow={1}>
<code <code
filetype={filetype(props.input.filePath!)} filetype={filetype(props.input.filePath!)}
syntaxStyle={SyntaxTheme} syntaxStyle={syntax()}
content={code()} content={code()}
/> />
</box> </box>
@@ -1131,6 +1132,7 @@ ToolRegistry.register<typeof EditTool>({
container: "block", container: "block",
render(props) { render(props) {
const ctx = use() const ctx = use()
const { theme, syntax } = useTheme()
const style = createMemo(() => (ctx.width > 120 ? "split" : "stacked")) const style = createMemo(() => (ctx.width > 120 ? "split" : "stacked"))
@@ -1210,21 +1212,21 @@ ToolRegistry.register<typeof EditTool>({
</ToolTitle> </ToolTitle>
<Switch> <Switch>
<Match when={props.permission["diff"]}> <Match when={props.permission["diff"]}>
<text>{props.permission["diff"]?.trim()}</text> <text fg={theme.text}>{props.permission["diff"]?.trim()}</text>
</Match> </Match>
<Match when={diff() && style() === "split"}> <Match when={diff() && style() === "split"}>
<box paddingLeft={1} flexDirection="row" gap={2}> <box paddingLeft={1} flexDirection="row" gap={2}>
<box flexGrow={1} flexBasis={0}> <box flexGrow={1} flexBasis={0}>
<code filetype={ft()} syntaxStyle={SyntaxTheme} content={diff()!.oldContent} /> <code filetype={ft()} syntaxStyle={syntax()} content={diff()!.oldContent} />
</box> </box>
<box flexGrow={1} flexBasis={0}> <box flexGrow={1} flexBasis={0}>
<code filetype={ft()} syntaxStyle={SyntaxTheme} content={diff()!.newContent} /> <code filetype={ft()} syntaxStyle={syntax()} content={diff()!.newContent} />
</box> </box>
</box> </box>
</Match> </Match>
<Match when={code()}> <Match when={code()}>
<box paddingLeft={1}> <box paddingLeft={1}>
<code filetype={ft()} syntaxStyle={SyntaxTheme} content={code()} /> <code filetype={ft()} syntaxStyle={syntax()} content={code()} />
</box> </box>
</Match> </Match>
</Switch> </Switch>
@@ -1237,6 +1239,7 @@ ToolRegistry.register<typeof PatchTool>({
name: "patch", name: "patch",
container: "block", container: "block",
render(props) { render(props) {
const { theme } = useTheme()
return ( return (
<> <>
<ToolTitle icon="%" fallback="Preparing patch..." when={true}> <ToolTitle icon="%" fallback="Preparing patch..." when={true}>
@@ -1244,7 +1247,7 @@ ToolRegistry.register<typeof PatchTool>({
</ToolTitle> </ToolTitle>
<Show when={props.output}> <Show when={props.output}>
<box> <box>
<text>{props.output?.trim()}</text> <text fg={theme.text}>{props.output?.trim()}</text>
</box> </box>
</Show> </Show>
</> </>

View File

@@ -42,7 +42,7 @@ export function Sidebar(props: { sessionID: string }) {
<Show when={session()}> <Show when={session()}>
<box flexShrink={0} gap={1} width={40}> <box flexShrink={0} gap={1} width={40}>
<box> <box>
<text> <text fg={theme.text}>
<b>{session().title}</b> <b>{session().title}</b>
</text> </text>
<Show when={session().share?.url}> <Show when={session().share?.url}>
@@ -50,7 +50,7 @@ export function Sidebar(props: { sessionID: string }) {
</Show> </Show>
</box> </box>
<box> <box>
<text> <text fg={theme.text}>
<b>Context</b> <b>Context</b>
</text> </text>
<text fg={theme.textMuted}>{context()?.tokens ?? 0} tokens</text> <text fg={theme.textMuted}>{context()?.tokens ?? 0} tokens</text>
@@ -59,7 +59,7 @@ export function Sidebar(props: { sessionID: string }) {
</box> </box>
<Show when={Object.keys(sync.data.mcp).length > 0}> <Show when={Object.keys(sync.data.mcp).length > 0}>
<box> <box>
<text> <text fg={theme.text}>
<b>MCP</b> <b>MCP</b>
</text> </text>
<For each={Object.entries(sync.data.mcp)}> <For each={Object.entries(sync.data.mcp)}>
@@ -77,7 +77,7 @@ export function Sidebar(props: { sessionID: string }) {
> >
</text> </text>
<text wrapMode="word"> <text fg={theme.text} wrapMode="word">
{key}{" "} {key}{" "}
<span style={{ fg: theme.textMuted }}> <span style={{ fg: theme.textMuted }}>
<Switch> <Switch>
@@ -96,7 +96,7 @@ export function Sidebar(props: { sessionID: string }) {
</Show> </Show>
<Show when={sync.data.lsp.length > 0}> <Show when={sync.data.lsp.length > 0}>
<box> <box>
<text> <text fg={theme.text}>
<b>LSP</b> <b>LSP</b>
</text> </text>
<For each={sync.data.lsp}> <For each={sync.data.lsp}>
@@ -123,7 +123,7 @@ export function Sidebar(props: { sessionID: string }) {
</Show> </Show>
<Show when={session().summary?.diffs}> <Show when={session().summary?.diffs}>
<box> <box>
<text> <text fg={theme.text}>
<b>Modified Files</b> <b>Modified Files</b>
</text> </text>
<For each={session().summary?.diffs || []}> <For each={session().summary?.diffs || []}>
@@ -155,7 +155,7 @@ export function Sidebar(props: { sessionID: string }) {
</Show> </Show>
<Show when={todo().length > 0}> <Show when={todo().length > 0}>
<box> <box>
<text> <text fg={theme.text}>
<b>Todo</b> <b>Todo</b>
</text> </text>
<For each={todo()}> <For each={todo()}>

View File

@@ -161,7 +161,9 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
<box gap={1}> <box gap={1}>
<box paddingLeft={3} paddingRight={2}> <box paddingLeft={3} paddingRight={2}>
<box flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD}>{props.title}</text> <text fg={theme.text} attributes={TextAttributes.BOLD}>
{props.title}
</text>
<text fg={theme.textMuted}>esc</text> <text fg={theme.textMuted}>esc</text>
</box> </box>
<box paddingTop={1} paddingBottom={1}> <box paddingTop={1} paddingBottom={1}>