refactor(tui): expose theme colors as properties (#37995)

This commit is contained in:
James Long 2026-07-20 16:17:09 -04:00 committed by GitHub
commit c0ba62b972
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 740 additions and 749 deletions

View file

@ -17,26 +17,26 @@ export function DevToolsSidebar() {
paddingBottom={1}
paddingLeft={2}
paddingRight={2}
backgroundColor={themeV2.background()}
backgroundColor={themeV2.background.default}
>
<box flexShrink={0} marginBottom={1}>
<box marginBottom={1}>
<text fg={themeV2.text.action()} attributes={TextAttributes.BOLD}>
<text fg={themeV2.text.action.primary.default} attributes={TextAttributes.BOLD}>
Theme
</text>
</box>
<box flexDirection="row">
<text fg={themeV2.text.subdued()}>Mode</text>
<text fg={themeV2.text.subdued}>Mode</text>
<box flexGrow={1} />
<box
paddingLeft={1}
paddingRight={1}
backgroundColor={modeHovered() && canSwitchMode() ? themeV2.background.action("hovered") : undefined}
backgroundColor={modeHovered() && canSwitchMode() ? themeV2.background.action.primary.hovered : undefined}
onMouseOver={() => setModeHovered(canSwitchMode())}
onMouseOut={() => setModeHovered(false)}
onMouseUp={canSwitchMode() ? () => setMode(nextMode()) : undefined}
>
<text fg={canSwitchMode() ? themeV2.text() : themeV2.text.subdued()}>{mode()}</text>
<text fg={canSwitchMode() ? themeV2.text.default : themeV2.text.subdued}>{mode()}</text>
</box>
</box>
</box>
@ -44,16 +44,16 @@ export function DevToolsSidebar() {
{(group) => (
<box flexShrink={0} marginBottom={1}>
<box marginBottom={1}>
<text fg={themeV2.text.action()} attributes={TextAttributes.BOLD}>
<text fg={themeV2.text.action.primary.default} attributes={TextAttributes.BOLD}>
{group.title}
</text>
</box>
<For each={group.entries}>
{(entry) => (
<box flexDirection="row">
<text fg={themeV2.text.subdued()}>{entry.key}</text>
<text fg={themeV2.text.subdued}>{entry.key}</text>
<box flexGrow={1} />
<text fg={themeV2.text()}>{String(entry.value)}</text>
<text fg={themeV2.text.default}>{String(entry.value)}</text>
</box>
)}
</For>

View file

@ -54,10 +54,10 @@ export function DialogDebug() {
return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<text fg={themeV2.text()} attributes={TextAttributes.BOLD}>
<text fg={themeV2.text.default} attributes={TextAttributes.BOLD}>
Debug
</text>
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
<text fg={themeV2.text.subdued} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
@ -67,10 +67,10 @@ export function DialogDebug() {
<For each={entries()}>
{(entry) => (
<box flexDirection="row" gap={1}>
<text flexShrink={0} fg={themeV2.text.subdued()}>
<text flexShrink={0} fg={themeV2.text.subdued}>
{entry.label.padEnd(10)}
</text>
<text fg={themeV2.text()} wrapMode="word">
<text fg={themeV2.text.default} wrapMode="word">
{entry.value}
</text>
</box>
@ -78,12 +78,12 @@ export function DialogDebug() {
</For>
</box>
<box flexDirection="row" justifyContent="space-between">
<text fg={themeV2.text.subdued()}>Share this when reporting an issue.</text>
<text fg={themeV2.text.subdued}>Share this when reporting an issue.</text>
<text onMouseUp={copy}>
<span style={{ fg: copied() ? themeV2.text.feedback.success() : themeV2.text() }}>
<span style={{ fg: copied() ? themeV2.text.feedback.success.default : themeV2.text.default }}>
<b>{copied() ? "✓ copied" : "copy"}</b>{" "}
</span>
<span style={{ fg: themeV2.text.subdued() }}>enter</span>
<span style={{ fg: themeV2.text.subdued }}>enter</span>
</text>
</box>
</box>

View file

@ -74,7 +74,7 @@ export function DialogIntegration(props: { onConnected?: OnIntegrationConnected
footer: connectionSummary(integration) || undefined,
category: integration.id in INTEGRATION_PRIORITY ? "Popular" : "Services",
disabled: methods.length === 0,
gutter: connected ? () => <text fg={themeV2.text.feedback.success()}></text> : undefined,
gutter: connected ? () => <text fg={themeV2.text.feedback.success.default}></text> : undefined,
onSelect: () =>
credentialConnections(integration).length
? manageConnections(integration, methods, dialog, props.onConnected)
@ -89,12 +89,12 @@ export function DialogIntegration(props: { onConnected?: OnIntegrationConnected
options={options()}
emptyView={
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
<text fg={themeV2.text.subdued()}>No integrations available</text>
<text fg={themeV2.text.subdued}>No integrations available</text>
</box>
}
noMatchView={
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
<text fg={themeV2.text.subdued()}>No integrations found</text>
<text fg={themeV2.text.subdued}>No integrations found</text>
</box>
}
/>
@ -295,24 +295,24 @@ function CommandView(props: { title: string; output: string; message: string })
return (
<box gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
<text attributes={TextAttributes.BOLD} fg={themeV2.text.default}>
{props.title}
</text>
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
<text fg={themeV2.text.subdued} onMouseUp={() => dialog.clear()}>
esc close
</text>
</box>
<box
backgroundColor={overlayTheme.background()}
backgroundColor={overlayTheme.background.default}
paddingLeft={2}
paddingRight={2}
paddingTop={1}
paddingBottom={1}
>
<text fg={overlayTheme.text()}>{props.output.trim()}</text>
<text fg={overlayTheme.text.default}>{props.output.trim()}</text>
</box>
<box paddingLeft={2} paddingRight={2}>
<text fg={themeV2.text.subdued()}>{props.message}</text>
<text fg={themeV2.text.subdued}>{props.message}</text>
</box>
</box>
)
@ -346,7 +346,7 @@ function KeyMethod(props: {
.catch((cause) => setError(message(cause)))
}}
description={() => (
<Show when={error()}>{(value) => <text fg={themeV2.text.feedback.error()}>{value()}</text>}</Show>
<Show when={error()}>{(value) => <text fg={themeV2.text.feedback.error.default}>{value()}</text>}</Show>
)}
/>
)
@ -536,9 +536,9 @@ function OAuthCode(props: {
}}
description={() => (
<box gap={1}>
<text fg={themeV2.text.subdued()}>{props.attempt.instructions}</text>
<Link href={props.attempt.url} fg={themeV2.markdown.link()} />
<Show when={error()}>{(value) => <text fg={themeV2.text.feedback.error()}>{value()}</text>}</Show>
<text fg={themeV2.text.subdued}>{props.attempt.instructions}</text>
<Link href={props.attempt.url} fg={themeV2.markdown.link} />
<Show when={error()}>{(value) => <text fg={themeV2.text.feedback.error.default}>{value()}</text>}</Show>
</box>
)}
/>
@ -551,27 +551,27 @@ function OAuthView(props: { title: string; url?: string; instructions?: string;
return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
<text attributes={TextAttributes.BOLD} fg={themeV2.text.default}>
{props.title}
</text>
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
<text fg={themeV2.text.subdued} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<Show when={props.url}>
{(url) => (
<box gap={1}>
<Link href={url()} fg={themeV2.markdown.link()} />
<Link href={url()} fg={themeV2.markdown.link} />
<Show when={props.instructions}>
{(instructions) => <text fg={themeV2.text.subdued()}>{instructions()}</text>}
{(instructions) => <text fg={themeV2.text.subdued}>{instructions()}</text>}
</Show>
</box>
)}
</Show>
<text fg={themeV2.text.subdued()}>{props.message}</text>
<text fg={themeV2.text.subdued}>{props.message}</text>
<Show when={props.copy}>
<text fg={themeV2.text()}>
c <span style={{ fg: themeV2.text.subdued() }}>copy</span>
<text fg={themeV2.text.default}>
c <span style={{ fg: themeV2.text.subdued }}>copy</span>
</text>
</Show>
</box>

View file

@ -21,11 +21,11 @@ function statusError(status: McpServer["status"]) {
function Status(props: { enabled: boolean; loading: boolean }) {
const { themeV2 } = useTheme().contextual("elevated")
if (props.loading) return <span style={{ fg: themeV2.text.subdued() }}> Loading</span>
if (props.loading) return <span style={{ fg: themeV2.text.subdued }}> Loading</span>
if (props.enabled) {
return <span style={{ fg: themeV2.text.feedback.success(), attributes: TextAttributes.BOLD }}> Enabled</span>
return <span style={{ fg: themeV2.text.feedback.success.default, attributes: TextAttributes.BOLD }}> Enabled</span>
}
return <span style={{ fg: themeV2.text.subdued() }}> Disabled</span>
return <span style={{ fg: themeV2.text.subdued }}> Disabled</span>
}
export function DialogMcp() {
@ -110,7 +110,7 @@ export function DialogMcp() {
]}
footer={
<Show when={focusedError()}>
<text fg={themeV2.text.subdued()}>enter to view error</text>
<text fg={themeV2.text.subdued}>enter to view error</text>
</Show>
}
/>
@ -171,16 +171,16 @@ function DialogMcpError(props: { server: McpServer; onBack: () => void }) {
return (
<box paddingLeft={4} paddingRight={4} paddingBottom={1} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
<text attributes={TextAttributes.BOLD} fg={themeV2.text.default}>
MCP server: {props.server.name}
</text>
<text fg={themeV2.text.subdued()} onMouseUp={props.onBack}>
<text fg={themeV2.text.subdued} onMouseUp={props.onBack}>
esc back
</text>
</box>
<text fg={themeV2.text.feedback.error()}> Failed</text>
<text fg={themeV2.text.feedback.error.default}> Failed</text>
<box
backgroundColor={overlayTheme.background()}
backgroundColor={overlayTheme.background.default}
paddingLeft={2}
paddingRight={2}
paddingTop={1}
@ -192,14 +192,14 @@ function DialogMcpError(props: { server: McpServer; onBack: () => void }) {
scrollbarOptions={{ visible: false }}
scrollAcceleration={getScrollAcceleration(config)}
>
<text fg={overlayTheme.text()} wrapMode="word">
<text fg={overlayTheme.text.default} wrapMode="word">
{error()}
</text>
</scrollbox>
</box>
<box flexDirection="row" justifyContent="space-between">
<text fg={themeV2.text.subdued()}> scroll</text>
<text fg={themeV2.text.subdued()} onMouseUp={copy}>
<text fg={themeV2.text.subdued}> scroll</text>
<text fg={themeV2.text.subdued} onMouseUp={copy}>
{copied() ? "✓ copied" : "c copy details"}
</text>
</box>

View file

@ -172,18 +172,18 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
return {
title,
titleView: isRemoving ? (
<span style={{ fg: themeV2.text.feedback.error() }}>Deleting {item.location}</span>
<span style={{ fg: themeV2.text.feedback.error.default }}>Deleting {item.location}</span>
) : deleting ? (
<span style={{ fg: themeV2.text.action.destructive() }}>
<span style={{ fg: themeV2.text.action.destructive.default }}>
Press {shortcuts.get("dialog.move_session.delete")} again to confirm
</span>
) : suffix ? (
<>
{visible.slice(0, split)}
<span style={{ fg: themeV2.text.subdued() }}>{visible.slice(split)}</span>
<span style={{ fg: themeV2.text.subdued }}>{visible.slice(split)}</span>
</>
) : undefined,
bg: deleting ? themeV2.background.action.destructive() : undefined,
bg: deleting ? themeV2.background.action.destructive.default : undefined,
value: {
type: "directory",
directory: item.location,
@ -316,7 +316,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
title="Move session"
titleView={
<box flexDirection="row" gap={1}>
<text fg={themeV2.text()} attributes={TextAttributes.BOLD}>
<text fg={themeV2.text.default} attributes={TextAttributes.BOLD}>
Move session
</text>
<Show when={working() || directories.loading || loadedProject.loading}>
@ -329,25 +329,25 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
emptyView={
showError() ? (
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
<text fg={themeV2.text.feedback.error()} attributes={TextAttributes.BOLD}>
<text fg={themeV2.text.feedback.error.default} attributes={TextAttributes.BOLD}>
Could not load project directories
</text>
<text fg={themeV2.text.subdued()}>{errorMessage(loadError())}</text>
<text fg={themeV2.text.subdued()}>Close and reopen Move session to try again.</text>
<text fg={themeV2.text.subdued}>{errorMessage(loadError())}</text>
<text fg={themeV2.text.subdued}>Close and reopen Move session to try again.</text>
</box>
) : directories.loading || loadedProject.loading ? (
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
<text fg={themeV2.text.subdued()}>Loading project directories</text>
<text fg={themeV2.text.subdued}>Loading project directories</text>
</box>
) : (
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
<text fg={themeV2.text.subdued()}>No project directories available</text>
<text fg={themeV2.text.subdued}>No project directories available</text>
</box>
)
}
noMatchView={
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
<text fg={themeV2.text.subdued()}>No project directories found</text>
<text fg={themeV2.text.subdued}>No project directories found</text>
</box>
}
locked={showError() || directories.loading || loadedProject.loading || Boolean(removing())}

View file

@ -47,17 +47,17 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
<box flexDirection={horizontal() ? "row" : "column"} alignItems={horizontal() ? "flex-start" : "center"} gap={2}>
<box width={horizontal() ? 29 : "100%"} flexShrink={0} gap={1}>
<box>
<text fg={themeV2.text.subdued()}>URLs</text>
<For each={value.urls}>{(url) => <text fg={themeV2.text()}>{url}</text>}</For>
<text fg={themeV2.text.subdued}>URLs</text>
<For each={value.urls}>{(url) => <text fg={themeV2.text.default}>{url}</text>}</For>
</box>
<box>
<text fg={themeV2.text.subdued()}>Username</text>
<text fg={themeV2.text()}>{value.username}</text>
<text fg={themeV2.text.subdued}>Username</text>
<text fg={themeV2.text.default}>{value.username}</text>
</box>
<box>
<text fg={themeV2.text.subdued()}>Password</text>
<text fg={themeV2.text.subdued}>Password</text>
<text
fg={passwordHover() ? themeV2.text() : themeV2.text.subdued()}
fg={passwordHover() ? themeV2.text.default : themeV2.text.subdued}
wrapMode="word"
onMouseOver={() => setPasswordHover(true)}
onMouseOut={() => setPasswordHover(false)}
@ -67,7 +67,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
</text>
</box>
<Show when={value.urls.some((url) => ["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname))}>
<text fg={themeV2.text.subdued()} wrapMode="word">
<text fg={themeV2.text.subdued} wrapMode="word">
Run `opencode service set hostname 0.0.0.0` to access the service remotely.
</text>
</Show>
@ -78,7 +78,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
flexShrink={0}
alignItems={horizontal() ? "flex-end" : "center"}
>
<text fg={themeV2.text()}>{renderUnicodeCompact(JSON.stringify(value), { border: 1 })}</text>
<text fg={themeV2.text.default}>{renderUnicodeCompact(JSON.stringify(value), { border: 1 })}</text>
</box>
</box>
)
@ -87,17 +87,17 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
return (
<box paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text fg={themeV2.text()} attributes={TextAttributes.BOLD}>
<text fg={themeV2.text.default} attributes={TextAttributes.BOLD}>
Pair
</text>
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
<text fg={themeV2.text.subdued} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<Show
when={loadError()}
fallback={
<Show when={info()} fallback={<text fg={themeV2.text.subdued()}>Loading server information</text>}>
<Show when={info()} fallback={<text fg={themeV2.text.subdued}>Loading server information</text>}>
<Show
when={dimensions().height >= 36}
fallback={
@ -116,11 +116,11 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
>
{(error) => (
<box>
<text fg={themeV2.text.feedback.error()} attributes={TextAttributes.BOLD}>
<text fg={themeV2.text.feedback.error.default} attributes={TextAttributes.BOLD}>
Could not load server information
</text>
<text fg={themeV2.text.subdued()}>{errorMessage(error())}</text>
<text fg={themeV2.text.subdued()}>Close and reopen Pair to try again.</text>
<text fg={themeV2.text.subdued}>{errorMessage(error())}</text>
<text fg={themeV2.text.subdued}>Close and reopen Pair to try again.</text>
</box>
)}
</Show>

View file

@ -47,10 +47,10 @@ export function DialogProjectCopyName(props: { onConfirm: (name: string) => void
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
<text attributes={TextAttributes.BOLD} fg={themeV2.text.default}>
Name project copy
</text>
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
<text fg={themeV2.text.subdued} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
@ -61,18 +61,17 @@ export function DialogProjectCopyName(props: { onConfirm: (name: string) => void
}}
onSubmit={confirm}
placeholder="Project copy name"
placeholderColor={themeV2.text.subdued()}
textColor={themeV2.text.formfield()}
focusedTextColor={themeV2.text.formfield()}
cursorColor={themeV2.text.formfield()}
placeholderColor={themeV2.text.subdued}
textColor={themeV2.text.formfield.default}
focusedTextColor={themeV2.text.formfield.default}
cursorColor={themeV2.text.formfield.default}
/>
<box paddingBottom={1} flexDirection="row" gap={2}>
<text fg={themeV2.text()}>
enter <span style={{ fg: themeV2.text.subdued() }}>submit</span>
<text fg={themeV2.text.default}>
enter <span style={{ fg: themeV2.text.subdued }}>submit</span>
</text>
<text fg={themeV2.text()}>
{shortcuts.get("dialog.project_copy.generate")}{" "}
<span style={{ fg: themeV2.text.subdued() }}>generate one</span>
<text fg={themeV2.text.default}>
{shortcuts.get("dialog.project_copy.generate")} <span style={{ fg: themeV2.text.subdued }}>generate one</span>
</text>
</box>
</box>

View file

@ -40,7 +40,7 @@ export function DialogRetryAction(props: DialogRetryActionProps) {
const dialog = useDialog()
const { themeV2 } = useTheme().contextual("elevated")
const showGoTreatment = () => props.link === GO_URL
const textBg = () => (showGoTreatment() ? panelOverlay(themeV2.background()) : undefined)
const textBg = () => (showGoTreatment() ? panelOverlay(themeV2.background.default) : undefined)
const [selected, setSelected] = createSignal<"dismiss" | "action">("action")
Keymap.createLayer(() => ({
@ -85,26 +85,26 @@ export function DialogRetryAction(props: DialogRetryActionProps) {
) : null}
<box zIndex={1} paddingLeft={PAD_X} paddingRight={PAD_X} paddingBottom={1} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={themeV2.text()} bg={textBg()}>
<text attributes={TextAttributes.BOLD} fg={themeV2.text.default} bg={textBg()}>
{props.title}
</text>
<text fg={themeV2.text.subdued()} bg={textBg()} onMouseUp={() => dialog.clear()}>
<text fg={themeV2.text.subdued} bg={textBg()} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box gap={0}>
<text fg={themeV2.text.subdued()} bg={textBg()}>
<text fg={themeV2.text.subdued} bg={textBg()}>
{props.message}
</text>
</box>
{props.link ? (
showGoTreatment() ? (
<box alignItems="center" justifyContent="flex-end" height={7} paddingBottom={1}>
<Link href={props.link} fg={themeV2.markdown.link()} bg={textBg()} wrapMode="none" />
<Link href={props.link} fg={themeV2.markdown.link} bg={textBg()} wrapMode="none" />
</box>
) : (
<box width="100%" flexDirection="row" justifyContent="center" paddingBottom={1}>
<Link href={props.link} fg={themeV2.markdown.link()} wrapMode="none" />
<Link href={props.link} fg={themeV2.markdown.link} wrapMode="none" />
</box>
)
) : (
@ -115,13 +115,13 @@ export function DialogRetryAction(props: DialogRetryActionProps) {
paddingLeft={2}
paddingRight={2}
backgroundColor={
selected() === "dismiss" ? themeV2.background.action("focused") : RGBA.fromInts(0, 0, 0, 0)
selected() === "dismiss" ? themeV2.background.action.primary.focused : RGBA.fromInts(0, 0, 0, 0)
}
onMouseOver={() => setSelected("dismiss")}
onMouseUp={() => dismiss(props, dialog)}
>
<text
fg={selected() === "dismiss" ? themeV2.text.action("focused") : themeV2.text.subdued()}
fg={selected() === "dismiss" ? themeV2.text.action.primary.focused : themeV2.text.subdued}
bg={selected() === "dismiss" ? undefined : textBg()}
attributes={selected() === "dismiss" ? TextAttributes.BOLD : undefined}
>
@ -131,12 +131,14 @@ export function DialogRetryAction(props: DialogRetryActionProps) {
<box
paddingLeft={2}
paddingRight={2}
backgroundColor={selected() === "action" ? themeV2.background.action("focused") : RGBA.fromInts(0, 0, 0, 0)}
backgroundColor={
selected() === "action" ? themeV2.background.action.primary.focused : RGBA.fromInts(0, 0, 0, 0)
}
onMouseOver={() => setSelected("action")}
onMouseUp={() => runAction(props, dialog)}
>
<text
fg={selected() === "action" ? themeV2.text.action("focused") : themeV2.text()}
fg={selected() === "action" ? themeV2.text.action.primary.focused : themeV2.text.default}
bg={selected() === "action" ? undefined : textBg()}
attributes={selected() === "action" ? TextAttributes.BOLD : undefined}
>

View file

@ -64,17 +64,17 @@ export function DialogSessionDeleteFailed(props: {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
<text attributes={TextAttributes.BOLD} fg={themeV2.text.default}>
Failed to Delete Session
</text>
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
<text fg={themeV2.text.subdued} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<text fg={themeV2.text.subdued()} wrapMode="word">
<text fg={themeV2.text.subdued} wrapMode="word">
{`The session "${props.session}" could not be deleted because the workspace "${props.workspace}" is not available.`}
</text>
<text fg={themeV2.text.subdued()} wrapMode="word">
<text fg={themeV2.text.subdued} wrapMode="word">
Choose how you want to recover this broken workspace session.
</text>
<box flexDirection="column" paddingBottom={1} gap={1}>
@ -86,7 +86,7 @@ export function DialogSessionDeleteFailed(props: {
paddingRight={1}
paddingTop={1}
paddingBottom={1}
backgroundColor={item.id === store.active ? themeV2.background.action("focused") : undefined}
backgroundColor={item.id === store.active ? themeV2.background.action.primary.focused : undefined}
onMouseUp={() => {
setStore("active", item.id)
void confirm()
@ -94,12 +94,12 @@ export function DialogSessionDeleteFailed(props: {
>
<text
attributes={TextAttributes.BOLD}
fg={item.id === store.active ? themeV2.text.action("focused") : themeV2.text()}
fg={item.id === store.active ? themeV2.text.action.primary.focused : themeV2.text.default}
>
{item.title}
</text>
<text
fg={item.id === store.active ? themeV2.text.action("focused") : themeV2.text.subdued()}
fg={item.id === store.active ? themeV2.text.action.primary.focused : themeV2.text.subdued}
wrapMode="word"
>
{item.description}

View file

@ -63,29 +63,29 @@ export function DialogSkill(props: DialogSkillProps) {
<Switch
fallback={
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
<text fg={themeV2.text.subdued()}>No skills available</text>
<text fg={themeV2.text.subdued}>No skills available</text>
</box>
}
>
<Match when={showError()}>
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
<text fg={themeV2.text.feedback.error()} attributes={TextAttributes.BOLD}>
<text fg={themeV2.text.feedback.error.default} attributes={TextAttributes.BOLD}>
Could not load skills
</text>
<text fg={themeV2.text.subdued()}>{errorMessage(loadError())}</text>
<text fg={themeV2.text.subdued()}>Close and reopen Skills to try again.</text>
<text fg={themeV2.text.subdued}>{errorMessage(loadError())}</text>
<text fg={themeV2.text.subdued}>Close and reopen Skills to try again.</text>
</box>
</Match>
<Match when={skills.loading}>
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
<text fg={themeV2.text.subdued()}>Loading skills</text>
<text fg={themeV2.text.subdued}>Loading skills</text>
</box>
</Match>
</Switch>
}
noMatchView={
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
<text fg={themeV2.text.subdued()}>No skills found</text>
<text fg={themeV2.text.subdued}>No skills found</text>
</box>
}
/>

View file

@ -13,25 +13,25 @@ export function DialogStatus() {
const mcp = createMemo(() => data.location.mcp.server.list() ?? [])
const color = (status: string) => {
if (status === "connected") return themeV2.text.feedback.success()
if (status === "failed") return themeV2.text.feedback.error()
if (status === "needs_auth") return themeV2.text.feedback.warning()
if (status === "needs_client_registration") return themeV2.text.feedback.error()
return themeV2.text.subdued()
if (status === "connected") return themeV2.text.feedback.success.default
if (status === "failed") return themeV2.text.feedback.error.default
if (status === "needs_auth") return themeV2.text.feedback.warning.default
if (status === "needs_client_registration") return themeV2.text.feedback.error.default
return themeV2.text.subdued
}
return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<text fg={themeV2.text()} attributes={TextAttributes.BOLD}>
<text fg={themeV2.text.default} attributes={TextAttributes.BOLD}>
Status
</text>
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
<text fg={themeV2.text.subdued} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<Show when={mcp().length > 0} fallback={<text fg={themeV2.text()}>No MCP servers</text>}>
<Show when={mcp().length > 0} fallback={<text fg={themeV2.text.default}>No MCP servers</text>}>
<box>
<text fg={themeV2.text()}>
<text fg={themeV2.text.default}>
{mcp().length} MCP server{mcp().length === 1 ? "" : "s"}
</text>
<For each={mcp()}>
@ -40,9 +40,9 @@ export function DialogStatus() {
<text flexShrink={0} style={{ fg: color(item.status.status) }}>
</text>
<text fg={themeV2.text()} wrapMode="word">
<text fg={themeV2.text.default} wrapMode="word">
<b>{item.name}</b>{" "}
<span style={{ fg: themeV2.text.subdued() }}>
<span style={{ fg: themeV2.text.subdued }}>
<Switch fallback={item.status.status}>
<Match when={item.status.status === "connected"}>Connected</Match>
<Match when={item.status.status === "failed" && item.status}>{(val) => val().error}</Match>

View file

@ -72,21 +72,21 @@ export function DialogWorkspaceFileChanges(props: {
return (
<box gap={1}>
<box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
<text attributes={TextAttributes.BOLD} fg={themeV2.text.default}>
{props.title ?? "File Changes Found"}
</text>
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
<text fg={themeV2.text.subdued} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingLeft={2} paddingRight={2}>
<text fg={themeV2.text.subdued()} wrapMode="word">
<text fg={themeV2.text.subdued} wrapMode="word">
{props.message ?? "Do you want to move these changes with the session?"}
</text>
</box>
<scrollbox
height={height()}
backgroundColor={overlayTheme.background()}
backgroundColor={overlayTheme.background.default}
scrollbarOptions={{ visible: false }}
scrollAcceleration={scrollAcceleration()}
>
@ -95,18 +95,16 @@ export function DialogWorkspaceFileChanges(props: {
<box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
<box flexDirection="row" minWidth={0} flexShrink={1}>
<box width={2} flexShrink={0}>
<text fg={overlayTheme.text.subdued()}>{statusLabel(item.status)}</text>
<text fg={overlayTheme.text.subdued}>{statusLabel(item.status)}</text>
</box>
<FilePath value={item.file} maxWidth={fileNameWidth()} fg={overlayTheme.text.subdued()} />
<FilePath value={item.file} maxWidth={fileNameWidth()} fg={overlayTheme.text.subdued} />
</box>
<box flexDirection="row" gap={1} minWidth={7} flexShrink={0} justifyContent="flex-end">
<text>
{" "}
{item.additions ? (
<span style={{ fg: overlayTheme.diff.text.added() }}>+{item.additions}</span>
) : null}
{item.additions ? <span style={{ fg: overlayTheme.diff.text.added }}>+{item.additions}</span> : null}
{item.deletions ? (
<span style={{ fg: overlayTheme.diff.text.removed() }}> -{item.deletions}</span>
<span style={{ fg: overlayTheme.diff.text.removed }}> -{item.deletions}</span>
) : null}
</text>
</box>
@ -120,14 +118,16 @@ export function DialogWorkspaceFileChanges(props: {
<box
paddingLeft={2}
paddingRight={2}
backgroundColor={item === store.active ? themeV2.background.action("focused") : undefined}
backgroundColor={item === store.active ? themeV2.background.action.primary.focused : undefined}
onMouseUp={() => {
setStore("active", item)
props.onSelect(item)
dialog.clear()
}}
>
<text fg={item === store.active ? themeV2.text.action("focused") : themeV2.text.subdued()}>{item}</text>
<text fg={item === store.active ? themeV2.text.action.primary.focused : themeV2.text.subdued}>
{item}
</text>
</box>
)}
</For>

View file

@ -8,7 +8,7 @@ export function Logo() {
const { themeV2 } = useTheme()
const renderLine = (line: string, fg: RGBA, bold: boolean): JSX.Element[] => {
const shadow = tint(themeV2.background(), fg, 0.25)
const shadow = tint(themeV2.background.default, fg, 0.25)
const attrs = bold ? TextAttributes.BOLD : undefined
return Array.from(line).map((char) => {
if (char === "_") {
@ -52,8 +52,8 @@ export function Logo() {
<For each={logo.left}>
{(line, index) => (
<box flexDirection="row" gap={1}>
<box flexDirection="row">{renderLine(line, themeV2.text.subdued(), false)}</box>
<box flexDirection="row">{renderLine(logo.right[index()], themeV2.text(), true)}</box>
<box flexDirection="row">{renderLine(line, themeV2.text.subdued, false)}</box>
<box flexDirection="row">{renderLine(logo.right[index()], themeV2.text.default, true)}</box>
</box>
)}
</For>

View file

@ -5,16 +5,16 @@ export function PluginRouteMissing(props: { id: string; name: string; onHome: ()
return (
<box width="100%" height="100%" alignItems="center" justifyContent="center" flexDirection="column" gap={1}>
<text fg={themeV2.text.feedback.warning()}>
<text fg={themeV2.text.feedback.warning.default}>
Unknown plugin route: {props.id}/{props.name}
</text>
<box
onMouseUp={props.onHome}
backgroundColor={themeV2.background.action("hovered")}
backgroundColor={themeV2.background.action.primary.hovered}
paddingLeft={1}
paddingRight={1}
>
<text fg={themeV2.text.action("hovered")}>go home</text>
<text fg={themeV2.text.action.primary.hovered}>go home</text>
</box>
</box>
)

View file

@ -297,8 +297,8 @@ export function Prompt(props: PromptProps) {
createEffect(() => {
if (!input || input.isDestroyed) return
if (props.disabled) input.cursorColor = themeV2.background.surface.offset()
if (!props.disabled) input.cursorColor = themeV2.text()
if (props.disabled) input.cursorColor = themeV2.background.surface.offset
if (!props.disabled) input.cursorColor = themeV2.text.default
})
const usage = createMemo(() => {
@ -1304,10 +1304,10 @@ export function Prompt(props: PromptProps) {
}
const highlight = createMemo(() => {
if (leader()) return themeV2.border()
if (store.mode === "shell") return themeV2.background.action()
if (leader()) return themeV2.border.default
if (store.mode === "shell") return themeV2.background.action.primary.default
const agent = local.agent.current()
if (!agent) return themeV2.border()
if (!agent) return themeV2.border.default
return local.agent.color(agent.id)
})
@ -1324,7 +1324,7 @@ export function Prompt(props: PromptProps) {
() => !!local.agent.current() && store.mode === "normal" && showVariant(),
animationsEnabled,
)
const borderHighlight = createMemo(() => tint(themeV2.border(), highlight(), agentMetaAlpha()))
const borderHighlight = createMemo(() => tint(themeV2.border.default, highlight(), agentMetaAlpha()))
const placeholderText = createMemo(() => {
if (props.showPlaceholder === false) return undefined
@ -1344,7 +1344,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) : themeV2.border()
const color = agent ? local.agent.color(agent.id) : themeV2.border.default
return {
frames: createFrames({
color,
@ -1364,7 +1364,7 @@ export function Prompt(props: PromptProps) {
})
const maxHeight = createMemo(() => Math.max(6, Math.floor(dimensions().height / 3)))
const promptBg = createMemo(() => themeV2.raise(themeV2.background.surface.offset()))
const promptBg = createMemo(() => themeV2.raise(themeV2.background.surface.offset))
return (
<>
@ -1390,9 +1390,9 @@ export function Prompt(props: PromptProps) {
<textarea
width="100%"
placeholder={placeholderText()}
placeholderColor={themeV2.text.subdued()}
textColor={leader() ? themeV2.text.subdued() : themeV2.text()}
focusedTextColor={leader() ? themeV2.text.subdued() : themeV2.text()}
placeholderColor={themeV2.text.subdued}
textColor={leader() ? themeV2.text.subdued : themeV2.text.default}
focusedTextColor={leader() ? themeV2.text.subdued : themeV2.text.default}
minHeight={1}
maxHeight={maxHeight()}
onContentChange={() => {
@ -1452,7 +1452,7 @@ export function Prompt(props: PromptProps) {
setTimeout(() => {
// setTimeout is a workaround and needs to be addressed properly
if (!input || input.isDestroyed) return
input.cursorColor = themeV2.text()
input.cursorColor = themeV2.text.default
}, 0)
}}
onMouseDown={(r: MouseEvent) => {
@ -1460,7 +1460,7 @@ export function Prompt(props: PromptProps) {
r.target?.focus()
}}
focusedBackgroundColor="transparent"
cursorColor={props.disabled ? themeV2.background.surface.offset() : themeV2.text()}
cursorColor={props.disabled ? themeV2.background.surface.offset : themeV2.text.default}
syntaxStyle={syntax()}
/>
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1} justifyContent="space-between">
@ -1472,24 +1472,24 @@ export function Prompt(props: PromptProps) {
{store.mode === "shell" ? "Shell" : Locale.titlecase(agent().id)}
</text>
<Show when={store.mode === "normal" && local.permission.mode === "auto"}>
<text fg={fadeColor(themeV2.text.subdued(), agentMetaAlpha())}>auto</text>
<text fg={fadeColor(themeV2.text.subdued, agentMetaAlpha())}>auto</text>
</Show>
<Show when={store.mode === "normal"}>
<box flexDirection="row" gap={1}>
<text fg={fadeColor(themeV2.text.subdued(), modelMetaAlpha())}>·</text>
<text fg={fadeColor(themeV2.text.subdued, modelMetaAlpha())}>·</text>
<text
flexShrink={0}
fg={fadeColor(leader() ? themeV2.text.subdued() : themeV2.text(), modelMetaAlpha())}
fg={fadeColor(leader() ? themeV2.text.subdued : themeV2.text.default, modelMetaAlpha())}
>
{local.model.parsed().model}
</text>
<text fg={fadeColor(themeV2.text.subdued(), modelMetaAlpha())}>{currentProviderLabel()}</text>
<text fg={fadeColor(themeV2.text.subdued, modelMetaAlpha())}>{currentProviderLabel()}</text>
<Show when={showVariant()}>
<text fg={fadeColor(themeV2.text.subdued(), variantMetaAlpha())}>·</text>
<text fg={fadeColor(themeV2.text.subdued, variantMetaAlpha())}>·</text>
<text>
<span
style={{
fg: fadeColor(themeV2.text.feedback.warning(), variantMetaAlpha()),
fg: fadeColor(themeV2.text.feedback.warning.default, variantMetaAlpha()),
bold: true,
}}
>
@ -1543,12 +1543,12 @@ export function Prompt(props: PromptProps) {
<Match when={status() === "running"}>
<box flexDirection="row" gap={1} flexGrow={1} justifyContent="flex-start">
<box marginLeft={1}>
<Show when={config.animations ?? true} fallback={<text fg={themeV2.text.subdued()}>[]</text>}>
<Show when={config.animations ?? true} fallback={<text fg={themeV2.text.subdued}>[]</text>}>
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
</Show>
</box>
<text
fg={store.interrupt > 0 ? themeV2.background.action() : themeV2.text()}
fg={store.interrupt > 0 ? themeV2.background.action.primary.default : themeV2.text.default}
wrapMode="none"
truncate
flexShrink={1}
@ -1556,7 +1556,7 @@ export function Prompt(props: PromptProps) {
esc{" "}
<span
style={{
fg: store.interrupt > 0 ? themeV2.background.action() : themeV2.text.subdued(),
fg: store.interrupt > 0 ? themeV2.background.action.primary.default : themeV2.text.subdued,
}}
>
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
@ -1567,16 +1567,16 @@ export function Prompt(props: PromptProps) {
<Match when={move.progress()}>
{(progress) => (
<box paddingLeft={3} height={1} minHeight={0} flexShrink={1}>
<Spinner color={themeV2.hue.accent(500)}>
<Spinner color={themeV2.hue.accent[500]}>
{progress()}
<span style={{ fg: themeV2.text.subdued() }}>{".".repeat(move.creatingDots())}</span>
<span style={{ fg: themeV2.text.subdued }}>{".".repeat(move.creatingDots())}</span>
</Spinner>
</box>
)}
</Match>
<Match when={move.pendingNew()}>
<box paddingLeft={3} height={1} minHeight={0} flexShrink={1}>
<text fg={themeV2.hue.accent(500)} wrapMode="none" truncate>
<text fg={themeV2.hue.accent[500]} wrapMode="none" truncate>
(new working copy)
</text>
</box>
@ -1584,7 +1584,7 @@ export function Prompt(props: PromptProps) {
<Match when={true}>
<Show when={!props.hint && locationLabel()} fallback={props.hint ?? <text />}>
{(location) => (
<text fg={themeV2.text.subdued()} wrapMode="none" truncate flexGrow={1} flexShrink={1}>
<text fg={themeV2.text.subdued} wrapMode="none" truncate flexGrow={1} flexShrink={1}>
{location()}
</text>
)}
@ -1598,7 +1598,7 @@ export function Prompt(props: PromptProps) {
wrapMode="none"
truncate
flexShrink={1}
fg={editorContextLabelState() === "pending" ? themeV2.hue.accent(500) : themeV2.text.subdued()}
fg={editorContextLabelState() === "pending" ? themeV2.hue.accent[500] : themeV2.text.subdued}
>
{file()}
</text>
@ -1608,40 +1608,40 @@ export function Prompt(props: PromptProps) {
<Match when={store.mode === "normal"}>
<Switch>
<Match when={liveWorkStatusVisible() || statusItems().length > 0}>
<text fg={themeV2.text.subdued()} wrapMode="none" truncate flexShrink={1}>
<text fg={themeV2.text.subdued} wrapMode="none" truncate flexShrink={1}>
<Show when={liveWorkStatusVisible() && liveWorkShortcut()}>
{(shortcut) => <span style={{ fg: themeV2.text() }}>{shortcut()} </span>}
{(shortcut) => <span style={{ fg: themeV2.text.default }}>{shortcut()} </span>}
</Show>
<Show when={subagentStatusLabel()}>
{(label) => <span style={{ fg: themeV2.text.subdued() }}>{label()}</span>}
{(label) => <span style={{ fg: themeV2.text.subdued }}>{label()}</span>}
</Show>
<Show when={subagentStatusLabel() && shellStatusLabel()}>
<span style={{ fg: themeV2.text.subdued() }}> · </span>
<span style={{ fg: themeV2.text.subdued }}> · </span>
</Show>
<Show when={shellStatusLabel()}>
{(label) => <span style={{ fg: themeV2.text.subdued() }}>{label()}</span>}
{(label) => <span style={{ fg: themeV2.text.subdued }}>{label()}</span>}
</Show>
<Show when={liveWorkStatusVisible() && statusItems().length > 0}>
<span style={{ fg: themeV2.text.subdued() }}> · </span>
<span style={{ fg: themeV2.text.subdued }}> · </span>
</Show>
<Show when={statusItems().length > 0}>
<span style={{ fg: themeV2.text.subdued() }}>{statusItems().join(" · ")}</span>
<span style={{ fg: themeV2.text.subdued }}>{statusItems().join(" · ")}</span>
</Show>
</text>
</Match>
<Match when={true}>
<text fg={themeV2.text()} flexShrink={0}>
{agentShortcut()} <span style={{ fg: themeV2.text.subdued() }}>agents</span>
<text fg={themeV2.text.default} flexShrink={0}>
{agentShortcut()} <span style={{ fg: themeV2.text.subdued }}>agents</span>
</text>
</Match>
</Switch>
<text fg={themeV2.text()} flexShrink={0}>
{paletteShortcut()} <span style={{ fg: themeV2.text.subdued() }}>commands</span>
<text fg={themeV2.text.default} flexShrink={0}>
{paletteShortcut()} <span style={{ fg: themeV2.text.subdued }}>commands</span>
</text>
</Match>
<Match when={store.mode === "shell"}>
<text fg={themeV2.text()} flexShrink={0}>
esc <span style={{ fg: themeV2.text.subdued() }}>exit shell mode</span>
<text fg={themeV2.text.default} flexShrink={0}>
esc <span style={{ fg: themeV2.text.subdued }}>exit shell mode</span>
</text>
</Match>
</Switch>

View file

@ -12,12 +12,12 @@ export function Reconnecting() {
right={0}
bottom={0}
left={0}
backgroundColor={themeV2.background()}
backgroundColor={themeV2.background.default}
alignItems="center"
justifyContent="center"
>
<box width={62} maxWidth="90%" flexDirection="column" alignItems="center" gap={1}>
<Spinner color={themeV2.text.subdued()}>Waiting for background service...</Spinner>
<Spinner color={themeV2.text.subdued}>Waiting for background service...</Spinner>
</box>
</box>
)

View file

@ -13,7 +13,7 @@ registerOpencodeSpinner()
export function Spinner(props: { children?: JSX.Element; color?: RGBA }) {
const { themeV2 } = useTheme()
const config = useConfig().data
const color = () => props.color ?? themeV2.text.subdued()
const color = () => props.color ?? themeV2.text.subdued
return (
<Show
when={config.animations ?? true}

View file

@ -54,8 +54,8 @@ export function StartupLoading(props: { ready: () => boolean }) {
return (
<Show when={show()}>
<box position="absolute" zIndex={5000} left={0} right={0} bottom={1} justifyContent="center" alignItems="center">
<box backgroundColor={themeV2.background()} paddingLeft={1} paddingRight={1}>
<Spinner color={themeV2.text.subdued()}>{text()}</Spinner>
<box backgroundColor={themeV2.background.default} paddingLeft={1} paddingRight={1}>
<Spinner color={themeV2.text.subdued}>{text()}</Spinner>
</box>
</box>
</Show>