refactor(theme): rename default tokens to base (#49837)

This commit is contained in:
James Long
2026-09-18 15:20:15 -04:00
committed by GitHub
parent dae1e8abbd
commit c720ca20ed
95 changed files with 1010 additions and 1010 deletions
+4 -4
View File
@@ -36,17 +36,17 @@ export function ErrorOverlay(props: { component: string; error: unknown; onClose
<Dialog centered onClose={props.onClose}>
<box maxHeight={Math.max(1, dimensions().height - 3)} paddingX={2} paddingBottom={1} gap={1}>
<box flexDirection="row" justifyContent="space-between" flexShrink={0}>
<text fg={theme.text.feedback.error.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.feedback.error.base} attributes={TextAttributes.BOLD}>
Error while hot reloading
</text>
<text fg={theme.text.subdued} onMouseUp={props.onClose}>
<text fg={theme.text.muted} onMouseUp={props.onClose}>
esc
</text>
</box>
<text maxHeight={Math.max(1, dimensions().height - 9)} fg={theme.text.default}>
<text maxHeight={Math.max(1, dimensions().height - 9)} fg={theme.text.base}>
{props.error instanceof Error ? props.error.message : String(props.error)}
</text>
<text flexShrink={0} fg={theme.text.subdued}>
<text flexShrink={0} fg={theme.text.muted}>
{props.component} · Fix the component and save to retry.
</text>
</box>
+2 -2
View File
@@ -5,8 +5,8 @@ export default Plugin.define({
id: "opencode.latex",
setup(context) {
const render = createLatexCodeBlockRenderer(context.renderer, () => ({
text: context.theme.text.default,
subdued: context.theme.text.subdued,
text: context.theme.text.base,
subdued: context.theme.text.muted,
}))
context.markdown.registerCodeBlockRenderer("latex", render)
context.markdown.registerCodeBlockRenderer("math", render)
+6 -6
View File
@@ -83,15 +83,15 @@ describe("OpenCode diagram palette", () => {
}
const theme = {
text: {
default: rgb([230, 232, 240]),
subdued: rgb([114, 120, 138]),
base: rgb([230, 232, 240]),
muted: rgb([114, 120, 138]),
feedback: {
info: { default: rgb([40, 120, 220]) },
success: { default: rgb([80, 180, 120]) },
warning: { default: rgb([220, 160, 80]) },
info: { base: rgb([40, 120, 220]) },
success: { base: rgb([80, 180, 120]) },
warning: { base: rgb([220, 160, 80]) },
},
},
background: { default: rgb([250, 250, 250]) },
background: { base: rgb([250, 250, 250]) },
categorical: [accent],
}
const palette = resolveOpenCodeDiagramPalette(theme, mode)
+12 -12
View File
@@ -46,27 +46,27 @@ export function createOpenCodeDiagramPalette(input: OpenCodeDiagramPaletteInput)
export function resolveOpenCodeDiagramPalette(
theme: {
readonly text: {
readonly default: RGBA
readonly subdued: RGBA
readonly base: RGBA
readonly muted: RGBA
readonly feedback: {
readonly info: { readonly default: RGBA }
readonly success: { readonly default: RGBA }
readonly warning: { readonly default: RGBA }
readonly info: { readonly base: RGBA }
readonly success: { readonly base: RGBA }
readonly warning: { readonly base: RGBA }
}
}
readonly background: { readonly default: RGBA }
readonly background: { readonly base: RGBA }
readonly categorical: readonly Readonly<Record<200 | 300 | 700 | 800, RGBA>>[]
},
mode: "dark" | "light",
) {
const accent = theme.categorical[3] ?? theme.categorical[0]!
return createOpenCodeDiagramPalette({
text: theme.text.default,
subdued: theme.text.subdued,
info: theme.text.feedback.info.default,
success: theme.text.feedback.success.default,
warning: theme.text.feedback.warning.default,
background: theme.background.default,
text: theme.text.base,
subdued: theme.text.muted,
info: theme.text.feedback.info.base,
success: theme.text.feedback.success.base,
warning: theme.text.feedback.warning.base,
background: theme.background.base,
accent: {
soft: accent[mode === "dark" ? 300 : 700],
clear: accent[mode === "dark" ? 200 : 800],
+56 -56
View File
@@ -106,16 +106,16 @@ const modes = {
},
categorical: DEFAULT_CATEGORICAL,
text: {
default: "$hue.neutral.200",
subdued: "$hue.neutral.400",
base: "$hue.neutral.200",
muted: "$hue.neutral.400",
action: {
primary: { default: "$hue.neutral.800", $disabled: "$hue.neutral.500" },
secondary: { default: "$text.subdued", $hovered: "$text.default" },
destructive: { default: "$hue.red.800", $disabled: "$hue.neutral.500" },
primary: { base: "$hue.neutral.800", $disabled: "$hue.neutral.500" },
secondary: { base: "$text.muted", $hovered: "$text.base" },
destructive: { base: "$hue.red.800", $disabled: "$hue.neutral.500" },
},
formfield: {
default: "$hue.neutral.200",
$focused: "$text.action.primary.default",
base: "$hue.neutral.200",
$focused: "$text.action.primary.base",
$pressed: "$hue.neutral.800",
$disabled: "$hue.neutral.500",
$selected: "$hue.interactive.300",
@@ -127,14 +127,14 @@ const modes = {
unread: "$hue.accent.200",
},
feedback: {
error: { default: "$hue.red.300", subdued: "$hue.red.400" },
warning: { default: "$hue.yellow.200", subdued: "$hue.yellow.300" },
success: { default: "$hue.green.300", subdued: "$hue.green.400" },
info: { default: "$hue.cyan.300", subdued: "$hue.cyan.400" },
error: { base: "$hue.red.300", muted: "$hue.red.400" },
warning: { base: "$hue.yellow.200", muted: "$hue.yellow.300" },
success: { base: "$hue.green.300", muted: "$hue.green.400" },
info: { base: "$hue.cyan.300", muted: "$hue.cyan.400" },
},
},
background: {
default: "$hue.neutral.800",
base: "$hue.neutral.800",
raised: {
base: "$hue.neutral.700",
high: "$hue.neutral.600",
@@ -142,16 +142,16 @@ const modes = {
},
action: {
primary: {
default: "$hue.interactive.400",
base: "$hue.interactive.400",
$hovered: "$hue.interactive.300",
$focused: "$hue.interactive.300",
$pressed: "$hue.interactive.200",
$selected: "$hue.interactive.300",
$disabled: "$hue.neutral.700",
},
secondary: { default: "transparent" },
secondary: { base: "transparent" },
destructive: {
default: "$hue.red.400",
base: "$hue.red.400",
$hovered: "$hue.red.300",
$focused: "$hue.red.300",
$pressed: "$hue.red.200",
@@ -160,22 +160,22 @@ const modes = {
},
},
formfield: {
default: "$background.default",
base: "$background.base",
$hovered: "$background.raised.base",
$focused: "$background.action.primary.default",
$focused: "$background.action.primary.base",
$pressed: "$hue.interactive.200",
$disabled: "$background.default",
$selected: "$background.formfield.default",
$disabled: "$background.base",
$selected: "$background.formfield.base",
},
feedback: {
error: { default: "$background.default" },
warning: { default: "$background.default" },
success: { default: "$background.default" },
info: { default: "$background.default" },
error: { base: "$background.base" },
warning: { base: "$background.base" },
success: { base: "$background.base" },
info: { base: "$background.base" },
},
},
border: { default: "$hue.neutral.700" },
scrollbar: { default: "$hue.neutral.600" },
border: { base: "$hue.neutral.700" },
scrollbar: { base: "$hue.neutral.600" },
diff: {
text: {
added: "$hue.green.300",
@@ -218,10 +218,10 @@ const modes = {
codeBlock: "$hue.neutral.100",
},
"@dialog": {
text: { action: { primary: { default: "$hue.neutral.900" } } },
text: { action: { primary: { base: "$hue.neutral.900" } } },
background: {
default: "$background.raised.base",
action: { primary: { default: "$hue.interactive.500", $hovered: "$background.raised.high" } },
base: "$background.raised.base",
action: { primary: { base: "$hue.interactive.500", $hovered: "$background.raised.high" } },
},
},
},
@@ -321,16 +321,16 @@ const modes = {
},
categorical: DEFAULT_CATEGORICAL,
text: {
default: "$hue.neutral.200",
subdued: "$hue.neutral.400",
base: "$hue.neutral.200",
muted: "$hue.neutral.400",
action: {
primary: { default: "$hue.neutral.200", $disabled: "$hue.neutral.500" },
secondary: { default: "$text.subdued", $hovered: "$text.default" },
destructive: { default: "$hue.red.200", $disabled: "$hue.neutral.500" },
primary: { base: "$hue.neutral.200", $disabled: "$hue.neutral.500" },
secondary: { base: "$text.muted", $hovered: "$text.base" },
destructive: { base: "$hue.red.200", $disabled: "$hue.neutral.500" },
},
formfield: {
default: "$hue.neutral.200",
$focused: "$text.action.primary.default",
base: "$hue.neutral.200",
$focused: "$text.action.primary.base",
$pressed: "$hue.neutral.200",
$disabled: "$hue.neutral.500",
$selected: "$hue.interactive.500",
@@ -342,14 +342,14 @@ const modes = {
unread: "$hue.accent.200",
},
feedback: {
error: { default: "$hue.red.300", subdued: "$hue.red.400" },
warning: { default: "$hue.yellow.200", subdued: "$hue.yellow.300" },
success: { default: "$hue.green.300", subdued: "$hue.green.400" },
info: { default: "$hue.cyan.300", subdued: "$hue.cyan.400" },
error: { base: "$hue.red.300", muted: "$hue.red.400" },
warning: { base: "$hue.yellow.200", muted: "$hue.yellow.300" },
success: { base: "$hue.green.300", muted: "$hue.green.400" },
info: { base: "$hue.cyan.300", muted: "$hue.cyan.400" },
},
},
background: {
default: "$hue.neutral.800",
base: "$hue.neutral.800",
raised: {
base: "$hue.neutral.700",
high: "$hue.neutral.600",
@@ -357,16 +357,16 @@ const modes = {
},
action: {
primary: {
default: "$hue.interactive.500",
base: "$hue.interactive.500",
$hovered: "$hue.interactive.600",
$focused: "$hue.interactive.600",
$pressed: "$hue.interactive.800",
$selected: "$hue.interactive.600",
$disabled: "$hue.neutral.800",
},
secondary: { default: "transparent" },
secondary: { base: "transparent" },
destructive: {
default: "$hue.red.600",
base: "$hue.red.600",
$hovered: "$hue.red.700",
$focused: "$hue.red.700",
$pressed: "$hue.red.800",
@@ -375,22 +375,22 @@ const modes = {
},
},
formfield: {
default: "$background.default",
base: "$background.base",
$hovered: "$background.raised.base",
$focused: "$background.action.primary.default",
$focused: "$background.action.primary.base",
$pressed: "$hue.interactive.800",
$disabled: "$background.default",
$selected: "$background.formfield.default",
$disabled: "$background.base",
$selected: "$background.formfield.base",
},
feedback: {
error: { default: "$background.default" },
warning: { default: "$background.default" },
success: { default: "$background.default" },
info: { default: "$background.default" },
error: { base: "$background.base" },
warning: { base: "$background.base" },
success: { base: "$background.base" },
info: { base: "$background.base" },
},
},
border: { default: "$hue.neutral.700" },
scrollbar: { default: "$hue.neutral.600" },
border: { base: "$hue.neutral.700" },
scrollbar: { base: "$hue.neutral.600" },
diff: {
text: {
added: "$hue.green.300",
@@ -433,10 +433,10 @@ const modes = {
codeBlock: "$hue.neutral.100",
},
"@dialog": {
text: { action: { primary: { default: "$hue.neutral.200" } } },
text: { action: { primary: { base: "$hue.neutral.200" } } },
background: {
default: "$background.raised.base",
action: { primary: { default: "$hue.interactive.400", $hovered: "$background.raised.high" } },
base: "$background.raised.base",
action: { primary: { base: "$hue.interactive.400", $hovered: "$background.raised.high" } },
},
},
},
+6 -6
View File
@@ -40,7 +40,7 @@ function expandText(definition: TextDefinition | undefined): TextDefinition | un
if (!definition) return
return {
...definition,
subdued: definition.subdued ?? (definition.default ? "$text.default" : undefined),
muted: definition.muted ?? (definition.base ? "$text.base" : undefined),
action: expandActions(definition.action, "text.action"),
formfield: expandFormfield(definition.formfield, "text.formfield"),
feedback: definition.feedback
@@ -50,7 +50,7 @@ function expandText(definition: TextDefinition | undefined): TextDefinition | un
kind,
{
...feedback,
subdued: feedback.subdued ?? (feedback.default ? `$text.feedback.${kind}.default` : undefined),
muted: feedback.muted ?? (feedback.base ? `$text.feedback.${kind}.base` : undefined),
},
]
}),
@@ -69,11 +69,11 @@ function expandBackground(definition: BackgroundDefinition | undefined): Backgro
}
function expandFormfield(definition: StatefulColorDefinition | undefined, path: string) {
if (!definition?.default) return definition
if (!definition?.base) return definition
return {
...definition,
...Object.fromEntries(
ActionState.literals.map((state) => [`$${state}`, definition[`$${state}`] ?? `$${path}.default`]),
ActionState.literals.map((state) => [`$${state}`, definition[`$${state}`] ?? `$${path}.base`]),
),
}
}
@@ -85,13 +85,13 @@ function expandActions<Definition extends Partial<Record<string, StatefulColorDe
if (!definition) return
return Object.fromEntries(
Object.entries(definition).map(([variant, value]) => {
if (!value?.default) return [variant, value]
if (!value?.base) return [variant, value]
return [
variant,
{
...value,
...Object.fromEntries(
ActionState.literals.map((state) => [`$${state}`, value[`$${state}`] ?? `$${path}.${variant}.default`]),
ActionState.literals.map((state) => [`$${state}`, value[`$${state}`] ?? `$${path}.${variant}.base`]),
),
},
]
+6 -6
View File
@@ -100,14 +100,14 @@ function contextualActions(
return [
variant,
Object.fromEntries(
(["default", ...ActionState.literals] as readonly ResolvedActionState[]).map((state) => {
const key = state === "default" ? undefined : (`$${state}` as ActionStateKey)
(["base", ...ActionState.literals] as readonly ResolvedActionState[]).map((state) => {
const key = state === "base" ? undefined : (`$${state}` as ActionStateKey)
return [
key ?? "default",
key ?? "base",
(key ? surfaceVariant?.[key] : undefined) ??
surfaceVariant?.default ??
surfaceVariant?.base ??
(key ? baseVariant?.[key] : undefined) ??
baseVariant?.default,
baseVariant?.base,
]
}),
),
@@ -151,7 +151,7 @@ function statefulActions(actions: Readonly<Record<ActionVariant, StatefulColor>>
function statefulColor(color: StatefulColor): StatefulColor {
return {
...color,
state: (states: ActionStates) => color[ActionState.literals.find((state) => states[state]) ?? "default"],
state: (states: ActionStates) => color[ActionState.literals.find((state) => states[state]) ?? "base"],
}
}
+17 -17
View File
@@ -62,7 +62,7 @@ const HueDefinition = Schema.Struct({
export type HueDefinition = Schema.Schema.Type<typeof HueDefinition>
const StatefulColorDefinition = Schema.Struct({
default: Schema.optional(ColorValue),
base: Schema.optional(ColorValue),
$hovered: Schema.optional(ColorValue),
$focused: Schema.optional(ColorValue),
$pressed: Schema.optional(ColorValue),
@@ -80,17 +80,17 @@ const ActionColorDefinition = Schema.Struct({
})
const TextFeedbackDefinition = Schema.Struct({
default: Schema.optional(ColorValue),
subdued: Schema.optional(ColorValue),
base: Schema.optional(ColorValue),
muted: Schema.optional(ColorValue),
})
const BackgroundFeedbackDefinition = Schema.Struct({
default: Schema.optional(ColorValue),
base: Schema.optional(ColorValue),
})
const TextDefinition = Schema.Struct({
default: Schema.optional(ColorValue),
subdued: Schema.optional(ColorValue),
base: Schema.optional(ColorValue),
muted: Schema.optional(ColorValue),
action: Schema.optional(ActionColorDefinition),
formfield: Schema.optional(StatefulColorDefinition),
status: Schema.optional(
@@ -113,7 +113,7 @@ const TextDefinition = Schema.Struct({
export type TextDefinition = Schema.Schema.Type<typeof TextDefinition>
const BackgroundDefinition = Schema.Struct({
default: Schema.optional(ColorValue),
base: Schema.optional(ColorValue),
raised: Schema.optional(
Schema.Struct({
base: Schema.optional(ColorValue),
@@ -202,8 +202,8 @@ export type DiffDefinition = Schema.Schema.Type<typeof DiffDefinition>
const ThemeTokensDefinition = Schema.Struct({
text: Schema.optional(TextDefinition),
background: Schema.optional(BackgroundDefinition),
border: Schema.optional(Schema.Struct({ default: Schema.optional(ColorValue) })),
scrollbar: Schema.optional(Schema.Struct({ default: Schema.optional(ColorValue) })),
border: Schema.optional(Schema.Struct({ base: Schema.optional(ColorValue) })),
scrollbar: Schema.optional(Schema.Struct({ base: Schema.optional(ColorValue) })),
diff: Schema.optional(DiffDefinition),
syntax: Schema.optional(SyntaxDefinition),
markdown: Schema.optional(MarkdownDefinition),
@@ -211,7 +211,7 @@ const ThemeTokensDefinition = Schema.Struct({
export type ThemeTokensDefinition = Schema.Schema.Type<typeof ThemeTokensDefinition>
const CompleteStatefulColorDefinition = Schema.Struct({
default: ColorValue,
base: ColorValue,
$hovered: Schema.optional(ColorValue),
$focused: Schema.optional(ColorValue),
$pressed: Schema.optional(ColorValue),
@@ -225,13 +225,13 @@ const CompleteActionColorDefinition = Schema.Struct({
destructive: CompleteStatefulColorDefinition,
})
const CompleteTextFeedbackDefinition = Schema.Struct({ default: ColorValue, subdued: Schema.optional(ColorValue) })
const CompleteBackgroundFeedbackDefinition = Schema.Struct({ default: ColorValue })
const CompleteTextFeedbackDefinition = Schema.Struct({ base: ColorValue, muted: Schema.optional(ColorValue) })
const CompleteBackgroundFeedbackDefinition = Schema.Struct({ base: ColorValue })
const CompleteThemeTokensDefinition = Schema.Struct({
text: Schema.Struct({
default: ColorValue,
subdued: ColorValue,
base: ColorValue,
muted: ColorValue,
action: CompleteActionColorDefinition,
formfield: CompleteStatefulColorDefinition,
status: Schema.Struct({
@@ -248,7 +248,7 @@ const CompleteThemeTokensDefinition = Schema.Struct({
}),
}),
background: Schema.Struct({
default: ColorValue,
base: ColorValue,
raised: Schema.Struct({ base: ColorValue, high: ColorValue, max: ColorValue }),
action: CompleteActionColorDefinition,
formfield: CompleteStatefulColorDefinition,
@@ -259,8 +259,8 @@ const CompleteThemeTokensDefinition = Schema.Struct({
info: CompleteBackgroundFeedbackDefinition,
}),
}),
border: Schema.Struct({ default: ColorValue }),
scrollbar: Schema.Struct({ default: ColorValue }),
border: Schema.Struct({ base: ColorValue }),
scrollbar: Schema.Struct({ base: ColorValue }),
diff: Schema.Struct({
text: Schema.Struct({ added: ColorValue, removed: ColorValue, context: ColorValue, hunkHeader: ColorValue }),
background: Schema.Struct({ added: ColorValue, removed: ColorValue, context: ColorValue }),
+17 -17
View File
@@ -8,14 +8,14 @@ export function generateSyntax(theme: ResolvedThemeTokens) {
const feedback = theme.text.feedback
return SyntaxStyle.fromTheme([
rule(["default"], theme.text.default),
rule(["default"], theme.text.base),
rule(["prompt"], theme.hue.accent[step]),
rule(["extmark.file"], feedback.warning.default, { bold: true }),
rule(["extmark.file"], feedback.warning.base, { bold: true }),
rule(["extmark.agent"], theme.categorical[0][step], { bold: true }),
rule(["extmark.skill"], (theme.categorical[1] ?? theme.categorical[0])[step], { bold: true }),
// V1 migration preserves its selected/inverse foreground in this action state.
rule(["extmark.paste"], theme.text.action.primary.focused, {
background: feedback.warning.default,
background: feedback.warning.base,
bold: true,
}),
rule(["comment", "comment.documentation"], syntax.comment, { italic: true }),
@@ -39,7 +39,7 @@ export function generateSyntax(theme: ResolvedThemeTokens) {
rule(["punctuation", "punctuation.bracket"], syntax.punctuation),
rule(
["variable.builtin", "type.builtin", "function.builtin", "module.builtin", "constant.builtin", "variable.super"],
feedback.error.default,
feedback.error.base,
),
rule(["keyword.directive", "keyword.modifier", "keyword.exception"], syntax.keyword, { italic: true }),
rule(["punctuation.special", "tag.delimiter"], syntax.operator),
@@ -61,27 +61,27 @@ export function generateSyntax(theme: ResolvedThemeTokens) {
rule(["markup.list"], markdown.listItem),
rule(["markup.quote"], markdown.blockQuote, { italic: true }),
rule(["markup.raw", "markup.raw.block"], markdown.code),
rule(["markup.raw.inline"], markdown.code, { background: theme.background.default }),
rule(["markup.raw.inline"], markdown.code, { background: theme.background.base }),
rule(["markup.link", "markup.link.url", "string.special", "string.special.url"], markdown.link, {
underline: true,
}),
rule(["markup.link.label"], markdown.linkText, { underline: true }),
rule(["label"], markdown.linkText),
rule(["spell", "nospell"], theme.text.default),
rule(["markup.underline"], theme.text.default, { underline: true }),
rule(["comment.error"], feedback.error.default, { italic: true, bold: true }),
rule(["comment.warning"], feedback.warning.default, { italic: true, bold: true }),
rule(["comment.todo", "comment.note"], feedback.info.default, { italic: true, bold: true }),
rule(["attribute", "annotation"], feedback.warning.default),
rule(["tag"], feedback.error.default),
rule(["markup.strikethrough", "markup.list.unchecked", "debug"], theme.text.subdued),
rule(["markup.list.checked"], feedback.success.default),
rule(["spell", "nospell"], theme.text.base),
rule(["markup.underline"], theme.text.base, { underline: true }),
rule(["comment.error"], feedback.error.base, { italic: true, bold: true }),
rule(["comment.warning"], feedback.warning.base, { italic: true, bold: true }),
rule(["comment.todo", "comment.note"], feedback.info.base, { italic: true, bold: true }),
rule(["attribute", "annotation"], feedback.warning.base),
rule(["tag"], feedback.error.base),
rule(["markup.strikethrough", "markup.list.unchecked", "debug"], theme.text.muted),
rule(["markup.list.checked"], feedback.success.base),
rule(["diff.plus"], theme.diff.text.added, { background: theme.diff.background.added }),
rule(["diff.minus"], theme.diff.text.removed, { background: theme.diff.background.removed }),
rule(["diff.delta"], theme.diff.text.context, { background: theme.diff.background.context }),
rule(["error"], feedback.error.default, { bold: true }),
rule(["warning"], feedback.warning.default, { bold: true }),
rule(["info"], feedback.info.default),
rule(["error"], feedback.error.base, { bold: true }),
rule(["warning"], feedback.warning.base, { bold: true }),
rule(["info"], feedback.info.base),
])
}
+8 -8
View File
@@ -11,7 +11,7 @@ import type {
SyntaxToken,
} from "./schema.js"
export type ResolvedActionState = "default" | ActionState
export type ResolvedActionState = "base" | ActionState
export type ResolvedFormfieldState = ResolvedActionState
export type HueScale = Readonly<Record<HueStep, RGBA>>
export type Hue = Readonly<Record<BaseHue | HueAlias, HueScale>>
@@ -30,8 +30,8 @@ export type ResolvedThemeTokens = {
readonly increase: (color: RGBA, amount?: number) => RGBA
readonly decrease: (color: RGBA, amount?: number) => RGBA
readonly text: {
readonly default: RGBA
readonly subdued: RGBA
readonly base: RGBA
readonly muted: RGBA
readonly action: Readonly<Record<ActionVariant, StatefulColor>>
readonly formfield: FormfieldColor
readonly status: {
@@ -40,10 +40,10 @@ export type ResolvedThemeTokens = {
readonly permission: RGBA
readonly unread: RGBA
}
readonly feedback: Readonly<Record<FeedbackKind, { readonly default: RGBA; readonly subdued: RGBA }>>
readonly feedback: Readonly<Record<FeedbackKind, { readonly base: RGBA; readonly muted: RGBA }>>
}
readonly background: {
readonly default: RGBA
readonly base: RGBA
readonly raised: {
readonly base: RGBA
readonly high: RGBA
@@ -51,10 +51,10 @@ export type ResolvedThemeTokens = {
}
readonly action: Readonly<Record<ActionVariant, StatefulColor>>
readonly formfield: FormfieldColor
readonly feedback: Readonly<Record<FeedbackKind, { readonly default: RGBA }>>
readonly feedback: Readonly<Record<FeedbackKind, { readonly base: RGBA }>>
}
readonly border: { readonly default: RGBA }
readonly scrollbar: { readonly default: RGBA }
readonly border: { readonly base: RGBA }
readonly scrollbar: { readonly base: RGBA }
readonly diff: {
readonly text: {
readonly added: RGBA
+22 -22
View File
@@ -110,20 +110,20 @@ function migrateMode(theme: Theme, mode: Mode): ThemeDefinition {
} as HueDefinition,
categorical: uniqueCategorical.length ? uniqueCategorical : DEFAULT_CATEGORICAL,
text: {
default: text,
subdued: textMuted,
base: text,
muted: textMuted,
action: {
primary: {
default: "$text.default",
base: "$text.base",
$disabled: textMuted,
$focused: selected,
$selected: primary,
},
secondary: { default: "$text.subdued", $hovered: "$text.default" },
destructive: { default: destructive, $disabled: textMuted },
secondary: { base: "$text.muted", $hovered: "$text.base" },
destructive: { base: destructive, $disabled: textMuted },
},
formfield: {
default: text,
base: text,
$hovered: primary,
$focused: primary,
$pressed: primary,
@@ -137,36 +137,36 @@ function migrateMode(theme: Theme, mode: Mode): ThemeDefinition {
unread: "$hue.accent.200",
},
feedback: {
error: { default: color("error") },
warning: { default: color("warning") },
success: { default: color("success") },
info: { default: color("info") },
error: { base: color("error") },
warning: { base: color("warning") },
success: { base: color("success") },
info: { base: color("info") },
},
},
background: {
default: background,
base: background,
raised: {
base: backgroundPanel,
high: backgroundMenu,
max: backgroundRaisedMax,
},
action: {
primary: { default: "transparent", $hovered: backgroundPanel, $focused: primary, $selected: "transparent" },
secondary: { default: "transparent" },
destructive: { default: color("error") },
primary: { base: "transparent", $hovered: backgroundPanel, $focused: primary, $selected: "transparent" },
secondary: { base: "transparent" },
destructive: { base: color("error") },
},
formfield: {
default: "$background.default",
base: "$background.base",
},
feedback: {
error: { default: "$background.default" },
warning: { default: "$background.default" },
success: { default: "$background.default" },
info: { default: "$background.default" },
error: { base: "$background.base" },
warning: { base: "$background.base" },
success: { base: "$background.base" },
info: { base: "$background.base" },
},
},
border: { default: color("border") },
scrollbar: { default: color("borderActive") },
border: { base: color("border") },
scrollbar: { base: color("borderActive") },
diff: {
text: {
added: color("diffAdded"),
@@ -217,7 +217,7 @@ function migrateMode(theme: Theme, mode: Mode): ThemeDefinition {
},
"@dialog": {
background: {
default: "$background.raised.base",
base: "$background.raised.base",
action: { primary: { $hovered: "$background.raised.high" } },
},
},
+1 -1
View File
@@ -1311,7 +1311,7 @@ function App(props: { pair?: DialogPairCredentials }) {
width={dimensions().width}
height={dimensions().height}
flexDirection="column"
backgroundColor={theme.background.default}
backgroundColor={theme.background.base}
onMouseDown={(evt) => {
if (copyOnSelectEnabled()) return
if (evt.button !== MouseButton.RIGHT) return
+26 -26
View File
@@ -229,7 +229,7 @@ export function DevToolsBar() {
}
return (
<box height={1} flexShrink={0} flexDirection="row" backgroundColor={theme.decrease(theme.background.default)}>
<box height={1} flexShrink={0} flexDirection="row" backgroundColor={theme.decrease(theme.background.base)}>
<Show when={panel()}>
<box
position="absolute"
@@ -248,10 +248,10 @@ export function DevToolsBar() {
panel() === "server"
? theme.text.action.primary.focused
: serverIndicator().state === "connected"
? theme.text.feedback.success.default
? theme.text.feedback.success.base
: serverIndicator().state === "disconnected"
? theme.text.feedback.error.default
: theme.text.default
? theme.text.feedback.error.base
: theme.text.base
}
>
{serverIndicator().icon}
@@ -261,8 +261,8 @@ export function DevToolsBar() {
panel() === "server"
? theme.text.action.primary.focused
: serverIndicator().state === "disconnected"
? theme.text.feedback.error.default
: theme.text.subdued
? theme.text.feedback.error.base
: theme.text.muted
}
>
{" "}
@@ -286,7 +286,7 @@ export function DevToolsBar() {
)}
</Show>
<Show when={server.error}>
<text fg={elevatedTheme.text.feedback.error.default}>Server details unavailable</text>
<text fg={elevatedTheme.text.feedback.error.base}>Server details unavailable</text>
</Show>
</PanelBox>
</Show>
@@ -297,8 +297,8 @@ export function DevToolsBar() {
panel() === "ui"
? theme.text.action.primary.focused
: runtime() === "high"
? theme.text.feedback.error.default
: theme.text.subdued
? theme.text.feedback.error.base
: theme.text.muted
}
>
{statusIcon(runtime())}
@@ -308,8 +308,8 @@ export function DevToolsBar() {
panel() === "ui"
? theme.text.action.primary.focused
: runtime() === "high"
? theme.text.feedback.error.default
: theme.text.subdued
? theme.text.feedback.error.base
: theme.text.muted
}
>
{" "}
@@ -339,7 +339,7 @@ export function DevToolsBar() {
</Show>
</BarItem>
<BarItem active={panel() === "theme"} onClick={() => toggle("theme")}>
<text fg={panel() === "theme" ? theme.text.action.primary.focused : theme.text.subdued}>Theme</text>
<text fg={panel() === "theme" ? theme.text.action.primary.focused : theme.text.muted}>Theme</text>
<Show when={panel() === "theme"}>
<PanelBox>
<PanelTitle>Theme</PanelTitle>
@@ -355,7 +355,7 @@ export function DevToolsBar() {
</Show>
</BarItem>
<BarItem active={panel() === "tools"} onClick={() => toggle("tools")}>
<text fg={panel() === "tools" ? theme.text.action.primary.focused : theme.text.subdued}>Tools</text>
<text fg={panel() === "tools" ? theme.text.action.primary.focused : theme.text.muted}>Tools</text>
<Show when={panel() === "tools"}>
<PanelBox>
<PanelTitle>Tools</PanelTitle>
@@ -364,20 +364,20 @@ export function DevToolsBar() {
</Action>
<Show when={dumpPath()}>
{(file) => (
<text fg={elevatedTheme.text.subdued} wrapMode="word">
<text fg={elevatedTheme.text.muted} wrapMode="word">
{file()}
</text>
)}
</Show>
<Show when={dumpError()}>
{(error) => (
<text fg={elevatedTheme.text.feedback.error.default} wrapMode="word">
<text fg={elevatedTheme.text.feedback.error.base} wrapMode="word">
{error()}
</text>
)}
</Show>
<box marginTop={1}>
<text fg={elevatedTheme.text.default} attributes={TextAttributes.BOLD}>
<text fg={elevatedTheme.text.base} attributes={TextAttributes.BOLD}>
Render
</text>
<Action
@@ -416,7 +416,7 @@ export function DevToolsBar() {
<For each={groups()}>
{(group) => (
<box marginTop={1}>
<text fg={elevatedTheme.text.default} attributes={TextAttributes.BOLD}>
<text fg={elevatedTheme.text.base} attributes={TextAttributes.BOLD}>
{group.title}
</text>
<For each={group.entries}>{(entry) => <Row label={entry.key} value={String(entry.value)} />}</For>
@@ -433,10 +433,10 @@ export function DevToolsBar() {
dialog.replace(() => <DialogExperiments />)
}}
>
<text fg={theme.text.subdued}>Experiments</text>
<text fg={theme.text.muted}>Experiments</text>
</BarItem>
<box flexGrow={1} minWidth={0}>
<TimeToFirstDraw visible={timing()} width="100%" fg={theme.text.subdued} label="Time to first draw" />
<TimeToFirstDraw visible={timing()} width="100%" fg={theme.text.muted} label="Time to first draw" />
</box>
</box>
)
@@ -502,7 +502,7 @@ function PanelBox(props: ParentProps) {
function PanelTitle(props: ParentProps) {
const theme = useTheme()
return (
<text fg={theme.text.default} attributes={TextAttributes.BOLD} marginBottom={1}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD} marginBottom={1}>
{props.children}
</text>
)
@@ -512,9 +512,9 @@ function Row(props: { label: string; value: string }) {
const theme = useTheme()
return (
<box flexDirection="row">
<text fg={theme.text.subdued}>{props.label}</text>
<text fg={theme.text.muted}>{props.label}</text>
<box flexGrow={1} />
<text fg={theme.text.default}>{props.value}</text>
<text fg={theme.text.base}>{props.value}</text>
</box>
)
}
@@ -534,7 +534,7 @@ function Action(props: ParentProps<{ onClick: () => void; disabled?: boolean; ho
if (!props.disabled) props.onClick()
}}
>
<text fg={props.disabled ? theme.text.subdued : theme.text.action.primary.default}>{props.children}</text>
<text fg={props.disabled ? theme.text.muted : theme.text.action.primary.base}>{props.children}</text>
</box>
)
}
@@ -554,13 +554,13 @@ function ProcessStat(props: { label: string; values: readonly number[]; unit: st
return (
<box flexDirection="row">
<box width={7}>
<text fg={theme.text.subdued}>{props.label}</text>
<text fg={theme.text.muted}>{props.label}</text>
</box>
<box flexGrow={1}>
<text fg={props.values.length ? theme.text.default : theme.text.subdued}>{brailleGraph(props.values)}</text>
<text fg={props.values.length ? theme.text.base : theme.text.muted}>{brailleGraph(props.values)}</text>
</box>
<box width={8} alignItems="flex-end">
<text fg={props.values.length ? theme.text.default : theme.text.subdued}>{value()}</text>
<text fg={props.values.length ? theme.text.base : theme.text.muted}>{value()}</text>
</box>
</box>
)
+7 -7
View File
@@ -55,10 +55,10 @@ export function DialogDebug() {
return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
Debug
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
@@ -68,10 +68,10 @@ export function DialogDebug() {
<For each={entries()}>
{(entry) => (
<box flexDirection="row" gap={1}>
<text flexShrink={0} fg={theme.text.subdued}>
<text flexShrink={0} fg={theme.text.muted}>
{entry.label.padEnd(10)}
</text>
<text fg={theme.text.default} wrapMode="word">
<text fg={theme.text.base} wrapMode="word">
{entry.value}
</text>
</box>
@@ -79,12 +79,12 @@ export function DialogDebug() {
</For>
</box>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text.subdued}>Share this when reporting an issue.</text>
<text fg={theme.text.muted}>Share this when reporting an issue.</text>
<text onMouseUp={copy}>
<span style={{ fg: copied() ? theme.text.feedback.success.default : theme.text.default }}>
<span style={{ fg: copied() ? theme.text.feedback.success.base : theme.text.base }}>
<b>{copied() ? "✓ copied" : "copy"}</b>{" "}
</span>
<span style={{ fg: theme.text.subdued }}>enter</span>
<span style={{ fg: theme.text.muted }}>enter</span>
</text>
</box>
</box>
@@ -76,7 +76,7 @@ export function DialogErrorDetails(props: {
<box flexDirection="row" gap={2}>
<text
attributes={TextAttributes.BOLD}
fg={theme.text.default}
fg={theme.text.base}
flexGrow={1}
minWidth={0}
wrapMode="none"
@@ -84,7 +84,7 @@ export function DialogErrorDetails(props: {
>
{props.title}
</text>
<text fg={theme.text.subdued} flexShrink={0} onMouseUp={props.onBack}>
<text fg={theme.text.muted} flexShrink={0} onMouseUp={props.onBack}>
esc
</text>
</box>
@@ -93,7 +93,7 @@ export function DialogErrorDetails(props: {
<FilePath
value={source()}
maxWidth={Math.min(dialogWidth(dialog.size), dimensions().width - 2) - 4}
fg={theme.text.subdued}
fg={theme.text.muted}
/>
)}
</Show>
@@ -106,28 +106,28 @@ export function DialogErrorDetails(props: {
scrollbarOptions={{ visible: false }}
scrollAcceleration={getScrollAcceleration(config)}
>
<text fg={theme.text.default} wrapMode="word">
<text fg={theme.text.base} wrapMode="word">
{props.error}
</text>
</scrollbox>
<Show when={props.diagnosticRef}>
<text fg={theme.text.subdued}>Reference: {props.diagnosticRef}</text>
<text fg={theme.text.muted}>Reference: {props.diagnosticRef}</text>
</Show>
</box>
<box flexDirection="row" gap={3} flexWrap="wrap">
<text onMouseUp={investigate}>
<span style={{ fg: theme.text.default }}>
<span style={{ fg: theme.text.base }}>
<b>i</b>
</span>
<span style={{ fg: theme.text.subdued }}> investigate</span>
<span style={{ fg: theme.text.muted }}> investigate</span>
</text>
<text onMouseUp={copy}>
<span style={{ fg: copied() ? theme.text.feedback.success.default : theme.text.default }}>
<span style={{ fg: copied() ? theme.text.feedback.success.base : theme.text.base }}>
<b>{copied() ? "✓ copied" : "c"}</b>
</span>
<span style={{ fg: theme.text.subdued }}>{copied() ? "" : " copy details"}</span>
<span style={{ fg: theme.text.muted }}>{copied() ? "" : " copy details"}</span>
</text>
<text fg={theme.text.subdued}>↑/↓ scroll</text>
<text fg={theme.text.muted}>↑/↓ scroll</text>
</box>
</box>
)
@@ -57,7 +57,7 @@ export function DialogExperiments() {
onSelect={(option) => void change(option.value)}
emptyView={
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No experiments available</text>
<text fg={theme.text.muted}>No experiments available</text>
</box>
}
footerHints={experiments.length > 0 ? [{ title: "←/→", label: "change" }] : []}
@@ -39,10 +39,10 @@ export function DialogImagePreview(props: { images: readonly ImagePreviewItem[];
return (
<box id="prompt-image-viewer" paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
Image {index() + 1} of {props.images.length}
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
@@ -56,13 +56,13 @@ export function DialogImagePreview(props: { images: readonly ImagePreviewItem[];
onError={() => setFailed(true)}
/>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text.subdued} onMouseUp={() => move(-1)}>
<text fg={theme.text.muted} onMouseUp={() => move(-1)}>
{props.images.length > 1 ? "← previous" : ""}
</text>
<text fg={failed() ? theme.text.feedback.error.default : theme.text.subdued} wrapMode="none" truncate>
<text fg={failed() ? theme.text.feedback.error.base : theme.text.muted} wrapMode="none" truncate>
{failed() ? "No preview" : (current().mention?.text ?? `Image ${index() + 1}`)}
</text>
<text fg={theme.text.subdued} onMouseUp={() => move(1)}>
<text fg={theme.text.muted} onMouseUp={() => move(1)}>
{props.images.length > 1 ? "next →" : ""}
</text>
</box>
@@ -114,7 +114,7 @@ export function DialogIntegration(
disabled: methods.length === 0 && credentials.length === 0,
gutter:
integration.connections.length > 0
? () => <text fg={theme.text.feedback.success.default}>✓</text>
? () => <text fg={theme.text.feedback.success.base}>✓</text>
: undefined,
onSelect: () => {
if (credentials.length) return manageConnections(integration, methods, location, dialog, props.onConnected)
@@ -130,12 +130,12 @@ export function DialogIntegration(
options={options()}
emptyView={
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No integrations available</text>
<text fg={theme.text.muted}>No integrations available</text>
</box>
}
noMatchView={
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No integrations found</text>
<text fg={theme.text.muted}>No integrations found</text>
</box>
}
/>
@@ -433,10 +433,10 @@ 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={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
{props.title}
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc close
</text>
</box>
@@ -447,10 +447,10 @@ function CommandView(props: { title: string; output: string; message: string })
paddingTop={1}
paddingBottom={1}
>
<text fg={overlayTheme.text.default}>{props.output.trim()}</text>
<text fg={overlayTheme.text.base}>{props.output.trim()}</text>
</box>
<box paddingLeft={2} paddingRight={2}>
<text fg={theme.text.subdued}>{props.message}</text>
<text fg={theme.text.muted}>{props.message}</text>
</box>
</box>
)
@@ -487,7 +487,7 @@ function KeyMethod(props: {
.catch((cause) => setError(message(cause)))
}}
description={() => (
<Show when={error()}>{(value) => <text fg={theme.text.feedback.error.default}>{value()}</text>}</Show>
<Show when={error()}>{(value) => <text fg={theme.text.feedback.error.base}>{value()}</text>}</Show>
)}
/>
)
@@ -706,9 +706,9 @@ function OAuthCode(props: {
}}
description={() => (
<box gap={1}>
<text fg={theme.text.subdued}>{props.attempt.instructions}</text>
<text fg={theme.text.muted}>{props.attempt.instructions}</text>
<Link href={props.attempt.url} fg={theme.markdown.link} />
<Show when={error()}>{(value) => <text fg={theme.text.feedback.error.default}>{value()}</text>}</Show>
<Show when={error()}>{(value) => <text fg={theme.text.feedback.error.base}>{value()}</text>}</Show>
</box>
)}
/>
@@ -728,10 +728,10 @@ function OAuthView(props: {
return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
{props.title}
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
@@ -740,21 +740,21 @@ function OAuthView(props: {
<box gap={1}>
<Link href={url()} fg={theme.markdown.link} />
<Show when={props.instructions}>
{(instructions) => <text fg={theme.text.subdued}>{instructions()}</text>}
{(instructions) => <text fg={theme.text.muted}>{instructions()}</text>}
</Show>
</box>
)}
</Show>
<text fg={theme.text.subdued}>{props.message}</text>
<text fg={theme.text.muted}>{props.message}</text>
<box flexDirection="row" gap={2}>
<Show when={props.open}>
<text fg={theme.text.default}>
o <span style={{ fg: theme.text.subdued }}>open</span>
<text fg={theme.text.base}>
o <span style={{ fg: theme.text.muted }}>open</span>
</text>
</Show>
<Show when={props.copy}>
<text fg={theme.text.default}>
c <span style={{ fg: theme.text.subdued }}>copy</span>
<text fg={theme.text.base}>
c <span style={{ fg: theme.text.muted }}>copy</span>
</text>
</Show>
</box>
@@ -872,9 +872,9 @@ function textAnswer(
description={() => (
<box gap={1}>
<Show when={field.description}>
{(description) => <text fg={theme.text.subdued}>{description()}</text>}
{(description) => <text fg={theme.text.muted}>{description()}</text>}
</Show>
<Show when={error()}>{(value) => <text fg={theme.text.feedback.error.default}>{value()}</text>}</Show>
<Show when={error()}>{(value) => <text fg={theme.text.feedback.error.base}>{value()}</text>}</Show>
</box>
)}
/>
+6 -6
View File
@@ -56,10 +56,10 @@ export function DialogMcp(props: { initialServer?: string; details?: boolean } =
const [loading, setLoading] = createSignal<ReadonlySet<string>>(new Set())
const statusColor = (status: McpServer["status"]) => {
if (status.status === "connected") return theme.text.feedback.success.default
if (status.status === "failed") return theme.text.feedback.error.default
if (status.status === "needs_auth") return theme.text.feedback.warning.default
return theme.text.subdued
if (status.status === "connected") return theme.text.feedback.success.base
if (status.status === "failed") return theme.text.feedback.error.base
if (status.status === "needs_auth") return theme.text.feedback.warning.base
return theme.text.muted
}
createEffect(() => {
@@ -76,7 +76,7 @@ export function DialogMcp(props: { initialServer?: string; details?: boolean } =
value: server.name,
title: server.name,
footer: <Status status={server.status} loading={pending} />,
footerColor: pending ? theme.text.subdued : statusColor(server.status),
footerColor: pending ? theme.text.muted : statusColor(server.status),
}
})
})
@@ -155,7 +155,7 @@ export function DialogMcp(props: { initialServer?: string; details?: boolean } =
]}
footer={
<Show when={focusedError()}>
<text fg={theme.text.subdued}>enter to view error</text>
<text fg={theme.text.muted}>enter to view error</text>
</Show>
}
/>
+6 -6
View File
@@ -336,7 +336,7 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
emptyView={
<Show when={!recent.loading && !projects.loading}>
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No recent sessions or projects</text>
<text fg={theme.text.muted}>No recent sessions or projects</text>
</box>
</Show>
}
@@ -350,13 +350,13 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
>
<box>
<Show when={projectID() && worktrees.loading}>
<Spinner color={theme.text.subdued}>Loading worktrees…</Spinner>
<Spinner color={theme.text.muted}>Loading worktrees…</Spinner>
</Show>
<Show when={!projectID() && (recent.loading || projects.loading)}>
<Spinner color={theme.text.subdued}>Refreshing sessions and projects…</Spinner>
<Spinner color={theme.text.muted}>Refreshing sessions and projects…</Spinner>
</Show>
<Show when={!projectID() && (recent() === false || projects() === false)}>
<text fg={theme.text.feedback.error.default}>
<text fg={theme.text.feedback.error.base}>
Could not refresh{" "}
{recent() === false ? (projects() === false ? "sessions and projects" : "sessions") : "projects"}.
</text>
@@ -402,7 +402,7 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
footerHints={[...(projectID() ? [{ title: "new worktree", label: "ctrl+n" }] : [])]}
noMatchView={
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
{projectID()
? worktrees.loading
? "Loading worktrees…"
@@ -434,7 +434,7 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
size="large"
title={`${projectName(data.project.get(projectID()!)) ?? "Project"} / New worktree`}
placeholder="Worktree name (optional)"
description={() => <text fg={theme.text.subdued}>Leave blank for a random name.</text>}
description={() => <text fg={theme.text.muted}>Leave blank for a random name.</text>}
busy={creating()}
busyText="Creating worktree…"
onCancel={cancelCreation}
+16 -16
View File
@@ -61,33 +61,33 @@ 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={theme.text.subdued}>This device</text>
<text fg={theme.text.muted}>This device</text>
<Show when={localhost()}>
{(url) => (
<Link href={href(url())} fg={theme.text.default}>
<Link href={href(url())} fg={theme.text.base}>
{url()}
</Link>
)}
</Show>
</box>
<box>
<text fg={theme.text.subdued}>URLs</text>
<text fg={theme.text.muted}>URLs</text>
<For each={value.urls}>
{(url) => (
<Link href={href(url)} fg={theme.text.default}>
<Link href={href(url)} fg={theme.text.base}>
{url}
</Link>
)}
</For>
</box>
<box>
<text fg={theme.text.subdued}>Username</text>
<text fg={theme.text.default}>{value.username}</text>
<text fg={theme.text.muted}>Username</text>
<text fg={theme.text.base}>{value.username}</text>
</box>
<box>
<text fg={theme.text.subdued}>Password</text>
<text fg={theme.text.muted}>Password</text>
<text
fg={passwordHover() ? theme.text.default : theme.text.subdued}
fg={passwordHover() ? theme.text.base : theme.text.muted}
wrapMode="word"
onMouseOver={() => setPasswordHover(true)}
onMouseOut={() => setPasswordHover(false)}
@@ -97,7 +97,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={theme.text.subdued} wrapMode="word">
<text fg={theme.text.muted} wrapMode="word">
Run `opencode service set hostname 0.0.0.0` to access the service remotely.
</text>
</Show>
@@ -108,7 +108,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
flexShrink={0}
alignItems={horizontal() ? "flex-end" : "center"}
>
<text fg={theme.text.default}>{renderUnicodeCompact(JSON.stringify(value), { border: 1 })}</text>
<text fg={theme.text.base}>{renderUnicodeCompact(JSON.stringify(value), { border: 1 })}</text>
</box>
</box>
)
@@ -117,17 +117,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={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
Pair
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<Show
when={loadError()}
fallback={
<Show when={info()} fallback={<text fg={theme.text.subdued}>Loading server information…</text>}>
<Show when={info()} fallback={<text fg={theme.text.muted}>Loading server information…</text>}>
<Show
when={dimensions().height >= 36}
fallback={
@@ -146,11 +146,11 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
>
{(error) => (
<box>
<text fg={theme.text.feedback.error.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.feedback.error.base} attributes={TextAttributes.BOLD}>
Could not load server information
</text>
<text fg={theme.text.subdued}>{errorMessage(error())}</text>
<text fg={theme.text.subdued}>Close and reopen Pair to try again.</text>
<text fg={theme.text.muted}>{errorMessage(error())}</text>
<text fg={theme.text.muted}>Close and reopen Pair to try again.</text>
</box>
)}
</Show>
@@ -197,11 +197,11 @@ export function DialogSessionList() {
title="Sessions"
titleView={
<box flexDirection="row">
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
Sessions
</text>
<Show when={!allProjects() && currentProjectName()}>
<text fg={theme.text.subdued}> for {currentProjectName()}</text>
<text fg={theme.text.muted}> for {currentProjectName()}</text>
</Show>
</box>
}
@@ -226,14 +226,14 @@ export function DialogSessionList() {
]}
emptyView={
<box paddingLeft={4} paddingRight={4}>
<text fg={searchState().error ? theme.text.feedback.error.default : theme.text.subdued}>
<text fg={searchState().error ? theme.text.feedback.error.base : theme.text.muted}>
{searchState().message}
</text>
</box>
}
noMatchView={
<box paddingLeft={4} paddingRight={4}>
<text fg={searchState().error ? theme.text.feedback.error.default : theme.text.subdued}>
<text fg={searchState().error ? theme.text.feedback.error.base : theme.text.muted}>
{searchState().message}
</text>
</box>
@@ -102,19 +102,19 @@ export function DialogShellOutput(props: { shell: ShellInfo; location: LocationR
return (
<box paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}>
<box flexDirection="row" gap={2}>
<text fg={theme.text.default} attributes={TextAttributes.BOLD} flexGrow={1}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD} flexGrow={1}>
Shell output
</text>
<text fg={theme.text.subdued}>{status()}</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted}>{status()}</text>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<text fg={theme.text.subdued} maxHeight={3} wrapMode="word">
<text fg={theme.text.muted} maxHeight={3} wrapMode="word">
{props.shell.command}
</text>
<Show when={omitted()}>
<text fg={theme.text.subdued}>Earlier output omitted · showing recent output</text>
<text fg={theme.text.muted}>Earlier output omitted · showing recent output</text>
</Show>
<scrollbox
id="shell-output-scroll"
@@ -124,7 +124,7 @@ export function DialogShellOutput(props: { shell: ShellInfo; location: LocationR
stickyStart="bottom"
scrollbarOptions={{ visible: false }}
>
<text fg={theme.text.default} wrapMode="word">
<text fg={theme.text.base} wrapMode="word">
{text() ||
(output() === undefined
? "Loading output…"
@@ -132,12 +132,12 @@ export function DialogShellOutput(props: { shell: ShellInfo; location: LocationR
</text>
</scrollbox>
<Show when={error()}>
<text fg={theme.text.feedback.error.default}>{error()}</text>
<text fg={theme.text.feedback.error.base}>{error()}</text>
</Show>
<box flexDirection="row" gap={2} flexWrap="wrap">
<text fg={theme.text.subdued}>↑/↓ scroll</text>
<text fg={theme.text.subdued}>end follow</text>
<text fg={theme.text.subdued}>esc back</text>
<text fg={theme.text.muted}>↑/↓ scroll</text>
<text fg={theme.text.muted}>end follow</text>
<text fg={theme.text.muted}>esc back</text>
</box>
</box>
)
+6 -6
View File
@@ -63,29 +63,29 @@ export function DialogSkill(props: DialogSkillProps) {
<Switch
fallback={
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No skills available</text>
<text fg={theme.text.muted}>No skills available</text>
</box>
}
>
<Match when={showError()}>
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.feedback.error.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.feedback.error.base} attributes={TextAttributes.BOLD}>
Could not load skills
</text>
<text fg={theme.text.subdued}>{errorMessage(loadError())}</text>
<text fg={theme.text.subdued}>Close and reopen Skills to try again.</text>
<text fg={theme.text.muted}>{errorMessage(loadError())}</text>
<text fg={theme.text.muted}>Close and reopen Skills to try again.</text>
</box>
</Match>
<Match when={skills.loading}>
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>Loading skills…</text>
<text fg={theme.text.muted}>Loading skills…</text>
</box>
</Match>
</Switch>
}
noMatchView={
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No skills found</text>
<text fg={theme.text.muted}>No skills found</text>
</box>
}
/>
+10 -10
View File
@@ -11,24 +11,24 @@ export function DialogStatus() {
const mcp = createMemo(() => data.location.mcp.server.list() ?? [])
const color = (status: string) => {
if (status === "connected") return theme.text.feedback.success.default
if (status === "failed") return theme.text.feedback.error.default
if (status === "needs_auth") return theme.text.feedback.warning.default
return theme.text.subdued
if (status === "connected") return theme.text.feedback.success.base
if (status === "failed") return theme.text.feedback.error.base
if (status === "needs_auth") return theme.text.feedback.warning.base
return theme.text.muted
}
return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
Status
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<Show when={mcp().length > 0} fallback={<text fg={theme.text.default}>No MCP servers</text>}>
<Show when={mcp().length > 0} fallback={<text fg={theme.text.base}>No MCP servers</text>}>
<box>
<text fg={theme.text.default}>
<text fg={theme.text.base}>
{mcp().length} MCP server{mcp().length === 1 ? "" : "s"}
</text>
<For each={mcp()}>
@@ -37,9 +37,9 @@ export function DialogStatus() {
<text flexShrink={0} style={{ fg: color(item.status.status) }}>
•
</text>
<text fg={theme.text.default} wrapMode="word">
<text fg={theme.text.base} wrapMode="word">
<b>{item.name}</b>{" "}
<span style={{ fg: theme.text.subdued }}>
<span style={{ fg: theme.text.muted }}>
<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>
+10 -10
View File
@@ -88,14 +88,14 @@ export function DialogUpdate(props: {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
{state().type === "installing"
? "Updating OpenCode"
: state().type === "available" || state().type === "failed"
? "Update available"
: "Update"}
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
@@ -104,33 +104,33 @@ export function DialogUpdate(props: {
{(current) => (
<Switch>
<Match when={current.type === "checking"}>
<Spinner shimmer={theme.text.default}>Checking for updates…</Spinner>
<Spinner shimmer={theme.text.base}>Checking for updates…</Spinner>
</Match>
<Match when={current.type === "available"}>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
An update is available. After installing, you'll be prompted to restart OpenCode.
</text>
</Match>
<Match when={current.type === "installing"}>
<Spinner shimmer={theme.text.default}>
<Spinner shimmer={theme.text.base}>
{current.type === "installing" ? `Installing OpenCode ${current.version}…` : ""}
</Spinner>
</Match>
<Match when={current.type === "installed"}>
<text fg={theme.text.subdued} wrapMode="word">
<text fg={theme.text.muted} wrapMode="word">
Update successful! A restart is required. Any active sessions will be resumed automatically.
</text>
</Match>
<Match when={current.type === "current"}>
<text fg={theme.text.subdued}>OpenCode is already up to date.</text>
<text fg={theme.text.muted}>OpenCode is already up to date.</text>
</Match>
<Match when={current.type === "unavailable"}>
<text fg={theme.text.subdued} wrapMode="word">
<text fg={theme.text.muted} wrapMode="word">
{current.type === "unavailable" ? current.message : ""}
</text>
</Match>
<Match when={current.type === "failed" || current.type === "check-failed"}>
<text fg={theme.text.feedback.error.default}>
<text fg={theme.text.feedback.error.base}>
{current.type === "failed" || current.type === "check-failed" ? current.message : ""}
</text>
</Match>
@@ -148,7 +148,7 @@ export function DialogUpdate(props: {
backgroundColor={active() === index() ? theme.background.action.primary.focused : undefined}
onMouseUp={() => void button.run()}
>
<text fg={active() === index() ? theme.text.action.primary.focused : theme.text.subdued}>
<text fg={active() === index() ? theme.text.action.primary.focused : theme.text.muted}>
{button.label}
</text>
</box>
@@ -72,15 +72,15 @@ export function DialogWorkspaceFileChanges(props: {
return (
<box gap={1}>
<box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
{props.title ?? "File Changes Found"}
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingLeft={2} paddingRight={2}>
<text fg={theme.text.subdued} wrapMode="word">
<text fg={theme.text.muted} wrapMode="word">
{props.message ?? "Do you want to move these changes with the session?"}
</text>
</box>
@@ -95,9 +95,9 @@ 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.muted}>{statusLabel(item.status)}</text>
</box>
<FilePath value={item.file} maxWidth={fileNameWidth()} fg={overlayTheme.text.subdued} />
<FilePath value={item.file} maxWidth={fileNameWidth()} fg={overlayTheme.text.muted} />
</box>
<box flexDirection="row" gap={1} minWidth={7} flexShrink={0} justifyContent="flex-end">
<text>
@@ -125,7 +125,7 @@ export function DialogWorkspaceFileChanges(props: {
dialog.clear()
}}
>
<text fg={item === store.active ? theme.text.action.primary.focused : theme.text.subdued}>{item}</text>
<text fg={item === store.active ? theme.text.action.primary.focused : theme.text.muted}>{item}</text>
</box>
)}
</For>
@@ -175,18 +175,18 @@ export function DialogWorkspaces(props: DialogWorkspacesProps) {
return {
title,
titleView: isRemoving ? (
<span style={{ fg: theme.text.feedback.error.default }}>Deleting {item.location}</span>
<span style={{ fg: theme.text.feedback.error.base }}>Deleting {item.location}</span>
) : deleting ? (
<span style={{ fg: theme.text.action.destructive.default }}>
<span style={{ fg: theme.text.action.destructive.base }}>
Press {shortcuts.get("dialog.move_session.delete")} again to confirm
</span>
) : suffix ? (
<>
{visible.slice(0, split)}
<span style={{ fg: theme.text.subdued }}>{visible.slice(split)}</span>
<span style={{ fg: theme.text.muted }}>{visible.slice(split)}</span>
</>
) : undefined,
bg: deleting ? theme.background.action.destructive.default : undefined,
bg: deleting ? theme.background.action.destructive.base : undefined,
value: {
type: "directory",
directory: item.location,
@@ -327,7 +327,7 @@ export function DialogWorkspaces(props: DialogWorkspacesProps) {
title="Worktrees"
titleView={
<box flexDirection="row" gap={1}>
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
Worktrees
</text>
<Show when={working() || directories.loading || loadedProject.loading}>
@@ -341,25 +341,25 @@ export function DialogWorkspaces(props: DialogWorkspacesProps) {
emptyView={
showError() ? (
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.feedback.error.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.feedback.error.base} attributes={TextAttributes.BOLD}>
Could not load worktrees
</text>
<text fg={theme.text.subdued}>{errorMessage(loadError())}</text>
<text fg={theme.text.subdued}>Close and reopen Worktrees to try again.</text>
<text fg={theme.text.muted}>{errorMessage(loadError())}</text>
<text fg={theme.text.muted}>Close and reopen Worktrees to try again.</text>
</box>
) : directories.loading || loadedProject.loading ? (
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>Loading worktrees…</text>
<text fg={theme.text.muted}>Loading worktrees…</text>
</box>
) : (
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No worktrees available</text>
<text fg={theme.text.muted}>No worktrees available</text>
</box>
)
}
noMatchView={
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No worktrees found</text>
<text fg={theme.text.muted}>No worktrees found</text>
</box>
}
locked={showError() || directories.loading || loadedProject.loading || Boolean(removing())}
@@ -49,10 +49,10 @@ export function DialogWorktreeName(props: { onConfirm: (name: string) => void })
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
Name worktree
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
@@ -64,17 +64,17 @@ export function DialogWorktreeName(props: { onConfirm: (name: string) => void })
}}
onSubmit={confirm}
placeholder="Worktree name"
placeholderColor={theme.text.subdued}
textColor={theme.text.formfield.default}
focusedTextColor={theme.text.formfield.default}
cursorColor={theme.text.formfield.default}
placeholderColor={theme.text.muted}
textColor={theme.text.formfield.base}
focusedTextColor={theme.text.formfield.base}
cursorColor={theme.text.formfield.base}
/>
<box paddingBottom={1} flexDirection="row" gap={2}>
<text fg={theme.text.default}>
enter <span style={{ fg: theme.text.subdued }}>submit</span>
<text fg={theme.text.base}>
enter <span style={{ fg: theme.text.muted }}>submit</span>
</text>
<text fg={theme.text.default}>
{shortcuts.get("dialog.worktree.generate")} <span style={{ fg: theme.text.subdued }}>generate one</span>
<text fg={theme.text.base}>
{shortcuts.get("dialog.worktree.generate")} <span style={{ fg: theme.text.muted }}>generate one</span>
</text>
</box>
</box>
+6 -6
View File
@@ -10,7 +10,7 @@ export function Logo() {
const dimensions = useTerminalDimensions()
const renderLine = (line: string, fg: RGBA, bold: boolean): JSX.Element[] => {
const shadow = tint(theme.background.default, fg, 0.25)
const shadow = tint(theme.background.base, fg, 0.25)
const attrs = bold ? TextAttributes.BOLD : undefined
return Array.from(line).map((char) => {
if (char === "_") {
@@ -53,23 +53,23 @@ export function Logo() {
<box>
{dimensions().height < 12 ? null : dimensions().width < 22 ? (
<For each={go.right.slice(1)}>
{(line) => <box flexDirection="row">{renderLine(line, theme.text.default, true)}</box>}
{(line) => <box flexDirection="row">{renderLine(line, theme.text.base, true)}</box>}
</For>
) : dimensions().width < 44 ? (
<>
<For each={logo.left.slice(1)}>
{(line) => <box flexDirection="row">{renderLine(line, theme.text.subdued, false)}</box>}
{(line) => <box flexDirection="row">{renderLine(line, theme.text.muted, false)}</box>}
</For>
<For each={logo.right}>
{(line) => <box flexDirection="row">{renderLine(line, theme.text.default, true)}</box>}
{(line) => <box flexDirection="row">{renderLine(line, theme.text.base, true)}</box>}
</For>
</>
) : (
<For each={logo.left}>
{(line, index) => (
<box flexDirection="row" gap={1}>
<box flexDirection="row">{renderLine(line, theme.text.subdued, false)}</box>
<box flexDirection="row">{renderLine(logo.right[index()], theme.text.default, true)}</box>
<box flexDirection="row">{renderLine(line, theme.text.muted, false)}</box>
<box flexDirection="row">{renderLine(logo.right[index()], theme.text.base, true)}</box>
</box>
)}
</For>
@@ -54,14 +54,14 @@ export function MigrationOverlay() {
flexDirection="row"
backgroundColor={theme.background.raised.high}
border={["left"]}
borderColor={theme.text.feedback.info.default}
borderColor={theme.text.feedback.info.base}
customBorderChars={SplitBorder.customBorderChars}
paddingLeft={2}
paddingRight={2}
paddingTop={1}
paddingBottom={1}
>
<Spinner color={theme.text.feedback.info.default}>
<Spinner color={theme.text.feedback.info.base}>
{value().label}
{count(value())}
</Spinner>
+1 -1
View File
@@ -21,7 +21,7 @@ export function PanelHost(props: {
const theme = useTheme()
// Side panels sit on a raised surface; fullscreen takes over the base background.
const background = () =>
panels.presentation() === "panel" ? theme.background.raised.base : theme.background.default
panels.presentation() === "panel" ? theme.background.raised.base : theme.background.base
return (
<box
id="session-panel"
@@ -5,7 +5,7 @@ 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={theme.text.feedback.warning.default}>
<text fg={theme.text.feedback.warning.base}>
Unknown plugin route: {props.id}/{props.name}
</text>
<box
@@ -879,7 +879,7 @@ export function Autocomplete(props: {
width={position().width}
zIndex={100}
{...SplitBorder}
borderColor={theme.border.default}
borderColor={theme.border.base}
>
<scrollbox
ref={(r: ScrollBoxRenderable) => {
@@ -896,7 +896,7 @@ export function Autocomplete(props: {
each={options()}
fallback={
<box paddingLeft={1} paddingRight={1}>
<text fg={emptyError() ? theme.text.feedback.error.default : theme.text.subdued}>{emptyMessage()}</text>
<text fg={emptyError() ? theme.text.feedback.error.base : theme.text.muted}>{emptyMessage()}</text>
</box>
}
>
@@ -928,7 +928,7 @@ export function Autocomplete(props: {
? theme.text.action.destructive.focused
: index === store.selected
? theme.text.action.primary.focused
: theme.text.default
: theme.text.base
}
flexShrink={0}
>
@@ -936,7 +936,7 @@ export function Autocomplete(props: {
</text>
<Show when={!confirmingAction() && option().description}>
<text
fg={index === store.selected ? theme.text.action.primary.focused : theme.text.subdued}
fg={index === store.selected ? theme.text.action.primary.focused : theme.text.muted}
wrapMode="none"
>
{" " + option().description?.replace(/\s+/g, " ").trim()}
+20 -20
View File
@@ -350,7 +350,7 @@ export function Prompt(props: PromptProps) {
createEffect(() => {
if (!input || input.isDestroyed) return
input.cursorColor = disabled() ? theme.background.raised.base : theme.text.default
input.cursorColor = disabled() ? theme.background.raised.base : theme.text.base
if (config.cursor) input.cursorStyle = config.cursor
})
@@ -1554,9 +1554,9 @@ export function Prompt(props: PromptProps) {
},
)
const highlight = createMemo(() => {
if (muted()) return theme.border.default
if (muted()) return theme.border.base
if (store.mode === "shell") return theme.text.action.primary.selected
return promptDisplay().agentColor ?? theme.border.default
return promptDisplay().agentColor ?? theme.border.base
})
const agentLabel = createMemo(() => (store.mode === "shell" ? "Shell" : promptDisplay().agentLabel))
const animateMetadata = !revealedPromptMetadata.has(local)
@@ -1573,7 +1573,7 @@ export function Prompt(props: PromptProps) {
createEffect(() => {
if (agentLabel()) revealedPromptMetadata.add(local)
})
const borderHighlight = createMemo(() => tint(theme.border.default, highlight(), agentMetaAlpha()))
const borderHighlight = createMemo(() => tint(theme.border.base, highlight(), agentMetaAlpha()))
const footerInput = () => ({
sessionID: props.sessionID,
mode: store.mode,
@@ -1622,7 +1622,7 @@ export function Prompt(props: PromptProps) {
})
const spinnerDef = createMemo(() => {
const color = promptDisplay().agentColor ?? theme.border.default
const color = promptDisplay().agentColor ?? theme.border.base
return {
frames: createFrames({
color,
@@ -1694,7 +1694,7 @@ export function Prompt(props: PromptProps) {
when={!failed()}
fallback={
<box width="100%" height="100%" alignItems="center" justifyContent="center">
<text fg={theme.text.subdued}>No preview</text>
<text fg={theme.text.muted}>No preview</text>
</box>
}
>
@@ -1726,7 +1726,7 @@ export function Prompt(props: PromptProps) {
openImagePreview(visibleImageAttachments().length)
}}
>
<text fg={theme.text.subdued} wrapMode="none" truncate>
<text fg={theme.text.muted} wrapMode="none" truncate>
+{imageAttachments().length - visibleImageAttachments().length} more
</text>
</box>
@@ -1736,9 +1736,9 @@ export function Prompt(props: PromptProps) {
<textarea
width="100%"
placeholder={placeholderText()}
placeholderColor={theme.text.subdued}
textColor={muted() ? theme.text.subdued : theme.text.default}
focusedTextColor={muted() ? theme.text.subdued : theme.text.default}
placeholderColor={theme.text.muted}
textColor={muted() ? theme.text.muted : theme.text.base}
focusedTextColor={muted() ? theme.text.muted : theme.text.base}
minHeight={1}
maxHeight={maxHeight()}
cursorStyle={config.cursor}
@@ -1799,7 +1799,7 @@ export function Prompt(props: PromptProps) {
setTimeout(() => {
// setTimeout is a workaround and needs to be addressed properly
if (!input || input.isDestroyed) return
input.cursorColor = disabled() ? theme.background.raised.base : theme.text.default
input.cursorColor = disabled() ? theme.background.raised.base : theme.text.base
if (config.cursor) input.cursorStyle = config.cursor
}, 0)
}}
@@ -1818,7 +1818,7 @@ export function Prompt(props: PromptProps) {
r.stopPropagation()
}}
focusedBackgroundColor="transparent"
cursorColor={disabled() ? theme.background.raised.base : theme.text.default}
cursorColor={disabled() ? theme.background.raised.base : theme.text.base}
syntaxStyle={syntax()}
/>
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1} justifyContent="space-between">
@@ -1885,17 +1885,17 @@ 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={theme.text.subdued}>[⋯]</text>}>
<Show when={config.animations ?? true} fallback={<text fg={theme.text.muted}>[⋯]</text>}>
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
</Show>
</box>
<PromptInterruptStatus
armed={store.interrupt > 0}
animations={animationsEnabled()}
text={theme.text.default}
subdued={theme.text.subdued}
warning={theme.text.feedback.warning.default}
flash={theme.decrease(theme.text.feedback.warning.default, 2)}
text={theme.text.base}
subdued={theme.text.muted}
warning={theme.text.feedback.warning.base}
flash={theme.decrease(theme.text.feedback.warning.base, 2)}
/>
</box>
</Match>
@@ -1904,7 +1904,7 @@ export function Prompt(props: PromptProps) {
<box paddingLeft={3} height={1} minHeight={0} flexShrink={1}>
<Spinner color={theme.hue.accent[500]}>
{progress()}
<span style={{ fg: theme.text.subdued }}>{".".repeat(move.creatingDots())}</span>
<span style={{ fg: theme.text.muted }}>{".".repeat(move.creatingDots())}</span>
</Spinner>
</box>
)}
@@ -1921,7 +1921,7 @@ export function Prompt(props: PromptProps) {
{(location) => (
<text
id="prompt.footer.location"
fg={locationActions.hovered() ? theme.text.default : theme.text.subdued}
fg={locationActions.hovered() ? theme.text.base : theme.text.muted}
wrapMode="none"
truncate
flexGrow={1}
@@ -1945,7 +1945,7 @@ export function Prompt(props: PromptProps) {
wrapMode="none"
truncate
flexShrink={1}
fg={editorContextLabelState() === "pending" ? theme.hue.accent[500] : theme.text.subdued}
fg={editorContextLabelState() === "pending" ? theme.hue.accent[500] : theme.text.muted}
>
{file()}
</text>
@@ -43,25 +43,25 @@ export function PromptMetadataRow(props: {
{(agent) => <text fg={fade(props.highlight, props.agentAlpha)}>{agent()}</text>}
</Show>
<Show when={props.mode === "normal" && layout().auto}>
<text fg={fade(theme.text.subdued, props.agentAlpha)}>auto</text>
<text fg={fade(theme.text.muted, props.agentAlpha)}>auto</text>
</Show>
<Show when={props.mode === "normal" && layout().model}>
<box flexDirection="row" gap={1} flexGrow={1} flexShrink={1} minWidth={0}>
<Show when={layout().agent}>
<text fg={fade(theme.text.subdued, props.modelAlpha)}>·</text>
<text fg={fade(theme.text.muted, props.modelAlpha)}>·</text>
</Show>
<text
flexShrink={1}
minWidth={0}
wrapMode="none"
truncate
fg={fade(props.muted ? theme.text.subdued : theme.text.default, props.modelAlpha)}
fg={fade(props.muted ? theme.text.muted : theme.text.base, props.modelAlpha)}
>
{layout().model}
</text>
<Show when={layout().provider}>
{(provider) => (
<text flexShrink={0} fg={fade(theme.text.subdued, props.modelAlpha)}>
<text flexShrink={0} fg={fade(theme.text.muted, props.modelAlpha)}>
{provider()}
</text>
)}
@@ -69,9 +69,9 @@ export function PromptMetadataRow(props: {
<Show when={layout().variant}>
{(variant) => (
<>
<text fg={fade(theme.text.subdued, props.variantAlpha)}>·</text>
<text fg={fade(theme.text.muted, props.variantAlpha)}>·</text>
<text
fg={fade(theme.text.feedback.warning.default, props.variantAlpha)}
fg={fade(theme.text.feedback.warning.base, props.variantAlpha)}
attributes={TextAttributes.BOLD}
>
{variant()}
+2 -2
View File
@@ -28,8 +28,8 @@ export function Reconnecting(props: { managed?: boolean }) {
paddingRight={2}
gap={1}
>
<Spinner color={theme.text.default}>{props.managed ? "Restarting service…" : "Connection lost…"}</Spinner>
<text fg={theme.text.subdued}>
<Spinner color={theme.text.base}>{props.managed ? "Restarting service…" : "Connection lost…"}</Spinner>
<text fg={theme.text.muted}>
{props.managed
? "Your session will resume automatically."
: "Reconnecting to the server automatically."}
@@ -47,7 +47,7 @@ export function SessionTabsRailControls(props: {
}}
onMouseDragEnd={() => (pressed = false)}
>
<text width={1} height={1} fg={theme.text.action.secondary.default} selectable={false} wrapMode="none">
<text width={1} height={1} fg={theme.text.action.secondary.base} selectable={false} wrapMode="none">
⌕
</text>
</box>
+46 -46
View File
@@ -121,7 +121,7 @@ const glowTextColor = (base: RGBA, glow: RGBA, index: number, width: number, lev
function tabFeedbackColor(status: SessionTabsStatus, theme: ReturnType<typeof useTheme>) {
if (status.attention) return theme.text.status[status.attention]
if (status.unread === "error") return theme.text.feedback.error.default
if (status.unread === "error") return theme.text.feedback.error.base
return undefined
}
@@ -455,7 +455,7 @@ function TabContextMenu(props: { state: TabContextMenuState; tabs: SessionTabsCo
run(index())
}}
>
<text fg={theme.text.default} selectable={false}>
<text fg={theme.text.base} selectable={false}>
{action.title}
</text>
</box>
@@ -530,9 +530,9 @@ function VerticalSessionTabs(props: {
const stride = () => (compact() ? 2 : 3)
const unreadColor = () => theme.text.status.unread
const activeNumber = () => theme.text.status.running
const idleNumber = () => tint(theme.text.formfield.default, background(), 0.55)
const separatorUpperPulseColor = createMemo(() => tint(background(), theme.text.default, 0.04))
const separatorLowerPulseColor = createMemo(() => tint(background(), theme.text.default, 0.05))
const idleNumber = () => tint(theme.text.formfield.base, background(), 0.55)
const separatorUpperPulseColor = createMemo(() => tint(background(), theme.text.base, 0.04))
const separatorLowerPulseColor = createMemo(() => tint(background(), theme.text.base, 0.05))
const [addHovered, setAddHovered] = createSignal(false)
const marquee = createTabMarquee(animations)
const hovered = marquee.hovered
@@ -750,16 +750,16 @@ function VerticalSessionTabs(props: {
const base =
hovered() === tab.sessionID && !selected()
? foreground()
: tint(idleNumber(), tint(theme.text.default, pulseBackground(), 0.25), Number(selected()))
: tint(idleNumber(), tint(theme.text.base, pulseBackground(), 0.25), Number(selected()))
const color = tabFeedbackColor(status(), theme) ?? tint(base, glowHue(), numberGlow.value().level)
const runningColor = runs() ? activeNumber() : color
return sweepLevel() === 0
? tint(runningColor, theme.text.default, numberIgnition.value().level)
: tint(runningColor, theme.text.default, Math.max(numberIgnition.value().level, 0.35 * sweepLevel()))
? tint(runningColor, theme.text.base, numberIgnition.value().level)
: tint(runningColor, theme.text.base, Math.max(numberIgnition.value().level, 0.35 * sweepLevel()))
}
const foreground = () => {
if (hovered() === tab.sessionID) return theme.text.default
return selected() ? theme.text.default : theme.text.subdued
if (hovered() === tab.sessionID) return theme.text.base
return selected() ? theme.text.base : theme.text.muted
}
const complete = () => status().complete
// Latched so a resolving glow fades out in the hue it lit with instead of snapping to the unread color.
@@ -770,14 +770,14 @@ function VerticalSessionTabs(props: {
if (status().unread !== undefined) return (lastGlowHue = unreadColor())
return lastGlowHue ?? unreadColor()
}
const pulseColor = createMemo(() => tint(pulseBackground(), theme.text.default, 0.25))
const flashColor = createMemo(() => tint(pulseBackground(), theme.text.default, 0.7))
const pulseColor = createMemo(() => tint(pulseBackground(), theme.text.base, 0.25))
const flashColor = createMemo(() => tint(pulseBackground(), theme.text.base, 0.7))
const glowLevel = createGlowLevel(() => selected() && Boolean(status().attention), animations)
const glowColor = createMemo(() => tint(pulseBackground(), glowHue(), 0.45 * glowLevel()))
const detailPulseColor = createMemo(() => tint(pulseBackground(), theme.text.default, 0.13))
const detailFlashColor = createMemo(() => tint(pulseBackground(), theme.text.default, 0.42))
const detailPulseColor = createMemo(() => tint(pulseBackground(), theme.text.base, 0.13))
const detailFlashColor = createMemo(() => tint(pulseBackground(), theme.text.base, 0.42))
const detailGlowColor = createMemo(() => tint(pulseBackground(), glowHue(), 0.25 * glowLevel()))
const detailColor = createMemo(() => tint(theme.text.subdued, pulseBackground(), 0.35))
const detailColor = createMemo(() => tint(theme.text.muted, pulseBackground(), 0.35))
const detailTextColor = (index: number) =>
detailFades()
? fadeTitleColor(detailColor(), pulseBackground(), index, visibleDetailParts().length, 0)
@@ -919,14 +919,14 @@ function VerticalSessionTabs(props: {
idleLabel={Locale.graphemes(title().trimStart())[0] ?? "U"}
color={
selected()
? theme.text.default
? theme.text.base
: props.numbers
? numberColor()
: (tabFeedbackColor(status(), theme) ?? (runs() ? activeNumber() : foreground()))
}
unreadColor={tabFeedbackColor(status(), theme) ?? unreadColor()}
backgroundColor={pulseBackground()}
flashColor={theme.text.default}
flashColor={theme.text.base}
animations={animations()}
numbers={props.numbers}
spinner={props.spinner}
@@ -951,8 +951,8 @@ function VerticalSessionTabs(props: {
color={separatorLowerPulseColor()}
width={indicatorWidth}
outerColor={separatorUpperPulseColor()}
flashColor={tint(background(), theme.text.default, 0.22)}
outerFlashColor={tint(background(), theme.text.default, 0.18)}
flashColor={tint(background(), theme.text.base, 0.22)}
outerFlashColor={tint(background(), theme.text.base, 0.18)}
flashTail={8}
glowColor={separatorLowerColor()}
outerGlowColor={separatorUpperColor()}
@@ -975,10 +975,10 @@ function VerticalSessionTabs(props: {
outerComplete={false}
glow={glows()}
outerGlow={false}
color={tint(background(), theme.text.default, 0.04)}
color={tint(background(), theme.text.base, 0.04)}
width={indicatorWidth}
outerColor={tint(background(), theme.text.default, 0.006)}
flashColor={tint(background(), theme.text.default, 0.18)}
outerColor={tint(background(), theme.text.base, 0.006)}
flashColor={tint(background(), theme.text.base, 0.18)}
flashTail={8}
glowColor={tint(background(), glowHue(), 0.1 * glowLevel())}
outerGlowColor={background()}
@@ -1013,7 +1013,7 @@ function VerticalSessionTabs(props: {
color={numberColor()}
unreadColor={tabFeedbackColor(status(), theme) ?? unreadColor()}
backgroundColor={pulseBackground()}
flashColor={theme.text.default}
flashColor={theme.text.base}
animations={animations()}
numbers={props.numbers}
spinner={props.spinner}
@@ -1053,7 +1053,7 @@ function VerticalSessionTabs(props: {
right={1}
zIndex={2}
width={1}
fg={closeHovered() ? theme.text.default : theme.text.subdued}
fg={closeHovered() ? theme.text.base : theme.text.muted}
selectable={false}
onMouseOver={() => setCloseHovered(true)}
onMouseOut={() => setCloseHovered(false)}
@@ -1105,7 +1105,7 @@ function VerticalSessionTabs(props: {
)
}}
</For>
{/* One slot with two states: a subdued affordance that promotes in place into the
{/* One slot with two states: a muted affordance that promotes in place into the
active new-session tab, instead of spawning a separate pseudo tab above itself. */}
<Show when={tabs.add || newTab()}>
<box
@@ -1162,7 +1162,7 @@ function VerticalSessionTabs(props: {
</Show>
<text
width={compact() ? 1 : 2}
fg={newTab() || addHovered() ? theme.text.default : idleNumber()}
fg={newTab() || addHovered() ? theme.text.base : idleNumber()}
selectable={false}
attributes={newTab() ? TextAttributes.BOLD : undefined}
>
@@ -1170,7 +1170,7 @@ function VerticalSessionTabs(props: {
</text>
<Show when={!compact()}>
<text
fg={newTab() || addHovered() ? theme.text.default : theme.text.subdued}
fg={newTab() || addHovered() ? theme.text.base : theme.text.muted}
wrapMode="none"
selectable={false}
attributes={newTab() ? TextAttributes.BOLD : undefined}
@@ -1184,7 +1184,7 @@ function VerticalSessionTabs(props: {
right={1}
zIndex={2}
width={1}
fg={theme.text.subdued}
fg={theme.text.muted}
selectable={false}
onMouseUp={(event) => {
if (event.button === RIGHT_MOUSE_BUTTON) return
@@ -1217,10 +1217,10 @@ function VerticalSessionTabs(props: {
edge="top"
width={tooltipWidth()}
color={background()}
background={base.background.default}
background={base.background.base}
/>
<box height={2} paddingX={1} backgroundColor={background()}>
<text fg={theme.text.default} wrapMode="none" selectable={false}>
<text fg={theme.text.base} wrapMode="none" selectable={false}>
{Locale.truncateWidth(
data?.session.get(sessionID())?.title ??
items().find((tab) => tab.sessionID === sessionID())?.title ??
@@ -1228,7 +1228,7 @@ function VerticalSessionTabs(props: {
tooltipWidth() - 2,
)}
</text>
<text fg={theme.text.subdued} wrapMode="none" selectable={false}>
<text fg={theme.text.muted} wrapMode="none" selectable={false}>
{Locale.takeWidth(detail(sessionID()), tooltipWidth() - 2)}
</text>
</box>
@@ -1237,7 +1237,7 @@ function VerticalSessionTabs(props: {
edge="bottom"
width={tooltipWidth()}
color={background()}
background={base.background.default}
background={base.background.base}
/>
</box>
)}
@@ -1293,7 +1293,7 @@ function HorizontalSessionTabs(props: {
let suppressClick = false
const unreadColor = () => theme.text.status.unread
const activeNumber = () => theme.text.status.running
const idleNumber = () => tint(theme.text.formfield.default, theme.background.default, 0.55)
const idleNumber = () => tint(theme.text.formfield.base, theme.background.base, 0.55)
const newTab = () => tabs.newTab?.() ?? false
const activeID = createMemo(() => (newTab() ? NEW_SESSION_TAB.sessionID : tabs.current()))
const ordered = createMemo(() => {
@@ -1519,7 +1519,7 @@ function HorizontalSessionTabs(props: {
onMouseDragEnd={release}
>
<Show when={layout().before > 0}>
<text width={sessionTabOverflowWidth(layout().before)} fg={theme.text.subdued} selectable={false}>
<text width={sessionTabOverflowWidth(layout().before)} fg={theme.text.muted} selectable={false}>
‹{layout().before}
</text>
</Show>
@@ -1533,14 +1533,14 @@ function HorizontalSessionTabs(props: {
const dragged = () => dragging() === tab.sessionID
const background = createMemo(() => {
const lifted = (hovered() === tab.sessionID || dragged()) && !selected()
const base = lifted ? theme.background.action.primary.hovered : theme.background.default
const base = lifted ? theme.background.action.primary.hovered : theme.background.base
// A dragged tab lifts to full selected elevation while it is held.
return tint(base, theme.decrease(theme.background.raised.base), dragged() ? 1 : selection())
})
const pulseColor = () => tint(background(), theme.text.default, 0.45)
const pulseColor = () => tint(background(), theme.text.base, 0.45)
// The edge flash washes toward a brighter stop on the same background-to-text ramp,
// so it reads as a lift of the pulse color rather than a different hue.
const flashColor = () => tint(background(), theme.text.default, 0.65)
const flashColor = () => tint(background(), theme.text.base, 0.65)
const feedbackColor = () => tabFeedbackColor(status(), theme)
const glowLevel = createGlowLevel(() => selected() && Boolean(status().attention), animations)
const glowColor = createMemo(() => tint(background(), feedbackColor() ?? unreadColor(), glowLevel()))
@@ -1573,8 +1573,8 @@ function HorizontalSessionTabs(props: {
const runs = () => status().busy && !status().attention
const numberIgnition = createNumberIgnition(runs, () => status().promptPulse, animations)
const foreground = () => {
if (hovered() === tab.sessionID) return theme.text.default
return tint(theme.text.subdued, theme.text.default, selection())
if (hovered() === tab.sessionID) return theme.text.base
return tint(theme.text.muted, theme.text.base, selection())
}
// Title characters sitting over the glow tinge toward its color, following the same
// spatial falloff as the glow itself; characters beyond the tail stay neutral.
@@ -1600,13 +1600,13 @@ function HorizontalSessionTabs(props: {
const base =
hovered() === tab.sessionID && !selected()
? foreground()
: tint(idleNumber(), tint(theme.text.default, background(), 0.25), selection())
: tint(idleNumber(), tint(theme.text.base, background(), 0.25), selection())
const color = runs() ? activeNumber() : (feedback ?? tint(base, unreadColor(), activity()))
// The number brightens faintly as the running sweep passes beneath it.
return tint(color, theme.text.default, Math.max(numberIgnition.value().level, 0.15 * sweepLevel()))
return tint(color, theme.text.base, Math.max(numberIgnition.value().level, 0.15 * sweepLevel()))
}
const bold = () => (selected() || dragged() ? TextAttributes.BOLD : undefined)
const closeColor = () => tint(theme.text.subdued, theme.text.default, 0.6)
const closeColor = () => tint(theme.text.muted, theme.text.base, 0.6)
return (
<box
width={width()}
@@ -1666,7 +1666,7 @@ function HorizontalSessionTabs(props: {
color={numberColor()}
unreadColor={feedbackColor() ?? unreadColor()}
backgroundColor={background()}
flashColor={theme.text.default}
flashColor={theme.text.base}
animations={animations()}
numbers={props.numbers}
spinner={props.spinner}
@@ -1697,7 +1697,7 @@ function HorizontalSessionTabs(props: {
right={1}
zIndex={2}
width={1}
fg={closeHovered() ? theme.text.default : closeColor()}
fg={closeHovered() ? theme.text.base : closeColor()}
selectable={false}
onMouseOver={() => setCloseHovered(true)}
onMouseOut={() => setCloseHovered(false)}
@@ -1725,14 +1725,14 @@ function HorizontalSessionTabs(props: {
}}
</For>
<Show when={layout().after > 0}>
<text width={sessionTabOverflowWidth(layout().after)} fg={theme.text.subdued} selectable={false}>
<text width={sessionTabOverflowWidth(layout().after)} fg={theme.text.muted} selectable={false}>
{" " + layout().after}›
</text>
</Show>
<Show when={showPlus()}>
<text
width={ADD_TAB_WIDTH}
fg={addHovered() ? theme.text.default : theme.text.subdued}
fg={addHovered() ? theme.text.base : theme.text.muted}
bg={addHovered() ? theme.background.action.primary.hovered : undefined}
selectable={false}
onMouseOver={() => setAddHovered(true)}
+1 -1
View File
@@ -14,7 +14,7 @@ registerOpencodeSpinner()
export function Spinner(props: { children?: JSX.Element; color?: RGBA; shimmer?: RGBA }) {
const theme = useTheme()
const config = useConfig().data
const color = () => props.color ?? theme.text.subdued
const color = () => props.color ?? theme.text.muted
const [frame, setFrame] = createSignal(0)
createEffect(() => {
if (!(config.animations ?? true) || !props.shimmer) return
@@ -55,7 +55,7 @@ export function StartupLoading(props: { ready: () => boolean }) {
<Show when={show()}>
<box position="absolute" zIndex={5000} left={0} right={0} bottom={1} justifyContent="center" alignItems="center">
<box backgroundColor={theme.background.raised.base} paddingLeft={1} paddingRight={1}>
<Spinner color={theme.text.subdued}>{text()}</Spinner>
<Spinner color={theme.text.muted}>{text()}</Spinner>
</box>
</box>
</Show>
+11 -11
View File
@@ -292,7 +292,7 @@ export function TerminalPane(props: {
// TODO: Revisit when embedded terminal mouse handlers can compose without replacing its internal focus handler.
onMouseDown={() => interact()}
>
<Show when={!failure()} fallback={<text fg={theme.text.feedback.error.default}>{failure()}</text>}>
<Show when={!failure()} fallback={<text fg={theme.text.feedback.error.base}>{failure()}</text>}>
<>
<embeddedTerminal
ref={(value) => {
@@ -340,17 +340,17 @@ function terminalPalette(theme: ResolvedThemeTokens, background: RGBA) {
const bright = 100
const colors = [
background,
theme.text.feedback.error.default,
theme.text.feedback.success.default,
theme.text.feedback.warning.default,
theme.text.feedback.error.base,
theme.text.feedback.success.base,
theme.text.feedback.warning.base,
theme.hue.blue[base],
theme.hue.purple[base],
theme.text.feedback.info.default,
theme.text.default,
theme.text.subdued,
theme.text.feedback.error.subdued,
theme.text.feedback.success.subdued,
theme.text.feedback.warning.subdued,
theme.text.feedback.info.base,
theme.text.base,
theme.text.muted,
theme.text.feedback.error.muted,
theme.text.feedback.success.muted,
theme.text.feedback.warning.muted,
theme.hue.blue[bright],
theme.hue.purple[bright],
theme.hue.cyan[bright],
@@ -359,7 +359,7 @@ function terminalPalette(theme: ResolvedThemeTokens, background: RGBA) {
return Buffer.from(
colors
.map((color, index) => `\x1b]4;${index};${hex(color)}\x1b\\`)
.concat(`\x1b]10;${hex(theme.text.default)}\x1b\\`, `\x1b]11;${hex(background)}\x1b\\`)
.concat(`\x1b]10;${hex(theme.text.base)}\x1b\\`, `\x1b]11;${hex(background)}\x1b\\`)
.join(""),
)
}
+1 -1
View File
@@ -324,7 +324,7 @@ const themeContext = createSimpleContext({
themePerformance.set("Init", `${(performance.now() - initStarted).toFixed(2)} ms`)
const current = createComponentTheme(tokens)
createEffect(() => renderer.setBackgroundColor(tokens().background.default))
createEffect(() => renderer.setBackgroundColor(tokens().background.base))
const currentSyntax = createSyntaxStyleMemo(() => generateSyntax(tokens()))
const service: Themes = {
@@ -22,17 +22,17 @@ function Mcp(props: { context: Plugin.Context }) {
return (
<Show when={list().length}>
<box gap={1} flexDirection="row" flexShrink={0} onMouseUp={() => props.context.keymap.dispatch("mcp.list")}>
<text fg={props.context.theme.text.default}>
<text fg={props.context.theme.text.base}>
<Switch>
<Match when={failed()}>
<span style={{ fg: props.context.theme.text.feedback.error.default }}>⊙ </span>
<span style={{ fg: props.context.theme.text.feedback.error.base }}>⊙ </span>
{failed()} MCP failed
</Match>
<Match when={true}>
<span
style={{
fg:
count() > 0 ? props.context.theme.text.feedback.success.default : props.context.theme.text.subdued,
count() > 0 ? props.context.theme.text.feedback.success.base : props.context.theme.text.muted,
}}
>
⊙{" "}
@@ -42,7 +42,7 @@ function Mcp(props: { context: Plugin.Context }) {
</Switch>
</text>
<Show when={visibility().mcpCommand}>
<text fg={props.context.theme.text.subdued}>/mcps</text>
<text fg={props.context.theme.text.muted}>/mcps</text>
</Show>
</box>
</Show>
@@ -62,12 +62,12 @@ function Plugins(props: { context: Plugin.Context }) {
return (
<Show when={failed()}>
<box gap={1} flexDirection="row" flexShrink={0} onMouseUp={() => props.context.keymap.dispatch("plugins.list")}>
<text fg={props.context.theme.text.default}>
<span style={{ fg: props.context.theme.text.feedback.error.default }}>⊙ </span>
<text fg={props.context.theme.text.base}>
<span style={{ fg: props.context.theme.text.feedback.error.base }}>⊙ </span>
{failed()} plugin{failed() === 1 ? "" : "s"} failed
</text>
<Show when={visibility().pluginCommand}>
<text fg={props.context.theme.text.subdued}>/plugins</text>
<text fg={props.context.theme.text.muted}>/plugins</text>
</Show>
</box>
</Show>
@@ -96,7 +96,7 @@ function View(props: { context: Plugin.Context }) {
<box flexGrow={1} />
<Show when={visibility().version}>
<box flexShrink={0}>
<text fg={props.context.theme.text.subdued}>{props.context.app.version}</text>
<text fg={props.context.theme.text.muted}>{props.context.app.version}</text>
</box>
</Show>
</box>
@@ -126,15 +126,15 @@ export function Answer(props: {
<box gap={1}>
<box paddingLeft={2} paddingRight={2}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
/btw
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingTop={1}>
<text fg={theme.text.subdued} wrapMode="word">
<text fg={theme.text.muted} wrapMode="word">
{props.question}
</text>
</box>
@@ -161,12 +161,12 @@ export function Answer(props: {
</scrollbox>
<box flexDirection="row" gap={3} paddingLeft={2} paddingRight={2} paddingBottom={1}>
<text onMouseUp={copy}>
<span style={{ fg: copied() ? theme.text.feedback.success.default : theme.text.default }}>
<span style={{ fg: copied() ? theme.text.feedback.success.base : theme.text.base }}>
<b>{copied() ? "✓ copied" : "c"}</b>
</span>
<span style={{ fg: theme.text.subdued }}>{copied() ? "" : " copy"}</span>
<span style={{ fg: theme.text.muted }}>{copied() ? "" : " copy"}</span>
</text>
<text fg={theme.text.subdued}>↑/↓ scroll</text>
<text fg={theme.text.muted}>↑/↓ scroll</text>
</box>
</box>
)
@@ -72,11 +72,11 @@ export function PromptFooter(props: {
onMouseUp={() => props.context.keymap.dispatch("session.child.first")}
>
<text
fg={liveHovered() ? props.context.theme.text.default : props.context.theme.text.subdued}
fg={liveHovered() ? props.context.theme.text.base : props.context.theme.text.muted}
wrapMode="none"
>
<Show when={shortcut("session.child.first")}>
{(value) => <span style={{ fg: props.context.theme.text.default }}>{value()} </span>}
{(value) => <span style={{ fg: props.context.theme.text.base }}>{value()} </span>}
</Show>
<Show when={subagents()}>{(value) => <>{value()}</>}</Show>
<Show when={subagents() && shells()}> · </Show>
@@ -85,7 +85,7 @@ export function PromptFooter(props: {
</box>
</Show>
<Show when={props.showDetails && layout().usage && status().length > 0}>
<text fg={props.context.theme.text.subdued} wrapMode="none" flexShrink={0}>
<text fg={props.context.theme.text.muted} wrapMode="none" flexShrink={0}>
<Show when={live()}> · </Show>
{status().join(" · ")}
</text>
@@ -93,21 +93,21 @@ export function PromptFooter(props: {
</box>
</Match>
<Match when={props.showDetails && layout().shortcuts}>
<text fg={props.context.theme.text.default} flexShrink={0}>
{shortcut("agent.cycle")} <span style={{ fg: props.context.theme.text.subdued }}>agents</span>
<text fg={props.context.theme.text.base} flexShrink={0}>
{shortcut("agent.cycle")} <span style={{ fg: props.context.theme.text.muted }}>agents</span>
</text>
</Match>
</Switch>
<Show when={props.showDetails && layout().shortcuts}>
<text fg={props.context.theme.text.default} wrapMode="none" flexShrink={0}>
{shortcut("command.palette.show")} <span style={{ fg: props.context.theme.text.subdued }}>commands</span>
<text fg={props.context.theme.text.base} wrapMode="none" flexShrink={0}>
{shortcut("command.palette.show")} <span style={{ fg: props.context.theme.text.muted }}>commands</span>
</text>
</Show>
</Match>
<Match when={props.mode === "shell"}>
<text fg={props.context.theme.text.default} flexShrink={0}>
<text fg={props.context.theme.text.base} flexShrink={0}>
esc{" "}
<span style={{ fg: props.context.theme.text.subdued }}>
<span style={{ fg: props.context.theme.text.muted }}>
{dimensions().width < 44 ? "shell" : "exit shell mode"}
</span>
</text>
@@ -20,21 +20,21 @@ export function SidebarContext(props: { context: Plugin.Context; sessionID: stri
return (
<Show when={state() || cost() > 0}>
<box>
<text fg={theme.text.default}>
<text fg={theme.text.base}>
<b>Context</b>
</text>
<Show when={state()}>
{(value) => (
<>
<text fg={theme.text.subdued}>{value().tokens.toLocaleString()} tokens</text>
<text fg={theme.text.muted}>{value().tokens.toLocaleString()} tokens</text>
<Show when={value().percent !== undefined}>
<text fg={theme.text.subdued}>{value().percent}% used</text>
<text fg={theme.text.muted}>{value().percent}% used</text>
</Show>
</>
)}
</Show>
<Show when={cost() > 0}>
<text fg={theme.text.subdued}>{money.format(cost())} spent</text>
<text fg={theme.text.muted}>{money.format(cost())} spent</text>
</Show>
</box>
</Show>
@@ -31,7 +31,7 @@ function View(props: { context: Plugin.Context; sessionID: string }) {
<FilePath
value={value()}
maxWidth={38}
fg={actions.hovered() ? props.context.theme.text.default : props.context.theme.text.subdued}
fg={actions.hovered() ? props.context.theme.text.base : props.context.theme.text.muted}
/>
</box>
)}
@@ -13,11 +13,11 @@ function View(props: { context: Plugin.Context; sessionID: string }) {
)
const dot = (status: string) => {
if (status === "connected") return theme.text.feedback.success.default
if (status === "failed") return theme.text.feedback.error.default
if (status === "disabled") return theme.text.subdued
if (status === "needs_auth") return theme.text.feedback.warning.default
return theme.text.subdued
if (status === "connected") return theme.text.feedback.success.base
if (status === "failed") return theme.text.feedback.error.base
if (status === "disabled") return theme.text.muted
if (status === "needs_auth") return theme.text.feedback.warning.base
return theme.text.muted
}
return (
@@ -25,12 +25,12 @@ function View(props: { context: Plugin.Context; sessionID: string }) {
<box>
<box flexDirection="row" gap={1} onMouseDown={() => list().length > 2 && setOpen((x) => !x)}>
<Show when={list().length > 2}>
<text fg={theme.text.default}>{open() ? "▼" : "▶"}</text>
<text fg={theme.text.base}>{open() ? "▼" : "▶"}</text>
</Show>
<text fg={theme.text.default}>
<text fg={theme.text.base}>
<b>MCP</b>
<Show when={!open()}>
<span style={{ fg: theme.text.subdued }}>
<span style={{ fg: theme.text.muted }}>
{" "}
({on()} active{bad() > 0 ? `, ${bad()} error${bad() > 1 ? "s" : ""}` : ""})
</span>
@@ -58,11 +58,11 @@ function View(props: { context: Plugin.Context; sessionID: string }) {
>
•
</text>
<text fg={theme.text.default} wrapMode="none" truncate flexGrow={1} flexShrink={1} minWidth={0}>
<text fg={theme.text.base} wrapMode="none" truncate flexGrow={1} flexShrink={1} minWidth={0}>
<b>{item.name}</b>
</text>
<text
fg={item.status.status === "failed" ? theme.text.feedback.error.default : theme.text.subdued}
fg={item.status.status === "failed" ? theme.text.feedback.error.base : theme.text.muted}
wrapMode="none"
flexShrink={0}
>
@@ -77,7 +77,7 @@ export function DiffFileMenu(props: {
if (event.button === MouseButton.LEFT) run()
}}
>
<text fg={theme.text.default} selectable={false} wrapMode="none" truncate>
<text fg={theme.text.base} selectable={false} wrapMode="none" truncate>
{label()}
</text>
</box>
@@ -36,10 +36,10 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
? flattenFileTree(tree()).filter((row) => row.fileIndex !== undefined)
: flattenFileTree(tree(), props.expandedNodes),
)
// Quieter than subdued text: markers are affordances, not content.
const faint = createMemo(() => tint(theme.text.subdued, theme.background.raised.base, 0.45))
// Quieter than muted text: markers are affordances, not content.
const faint = createMemo(() => tint(theme.text.muted, theme.background.raised.base, 0.45))
// Rails are pure texture; keep them barely above the surface.
const rail = createMemo(() => tint(theme.text.subdued, theme.background.raised.base, 0.7))
const rail = createMemo(() => tint(theme.text.muted, theme.background.raised.base, 0.7))
const reviewedCount = createMemo(() => props.files.filter((file) => props.reviewedFileNames?.has(file.file)).length)
const contentWidth = () => Math.max(0, props.width - 4 - FILE_TREE_STATUS_WIDTH - 1)
let scroll: ScrollBoxRenderable | undefined
@@ -85,8 +85,8 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
props.onSwitchSource
? sourceHovered()
? theme.text.action.secondary.hovered
: theme.text.action.secondary.default
: theme.text.default
: theme.text.action.secondary.base
: theme.text.base
}
attributes={TextAttributes.BOLD}
flexShrink={0}
@@ -96,12 +96,12 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
{props.source ?? "Files"}
</text>
<Show when={props.sourceDetail}>
<text fg={theme.text.subdued} selectable={false} flexGrow={1} minWidth={0} wrapMode="none" truncate>
<text fg={theme.text.muted} selectable={false} flexGrow={1} minWidth={0} wrapMode="none" truncate>
{` · ${props.sourceDetail}`}
</text>
</Show>
</box>
<text id="diff-review-count" fg={theme.text.subdued} wrapMode="none" flexShrink={0}>
<text id="diff-review-count" fg={theme.text.muted} wrapMode="none" flexShrink={0}>
{reviewedCount()}/{props.files.length}
{props.source ? "" : " reviewed"}
</text>
@@ -119,7 +119,7 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
<text />
</Match>
<Match when={props.files.length === 0}>
<text fg={theme.text.subdued}>No files</text>
<text fg={theme.text.muted}>No files</text>
</Match>
<Match when={props.files.length > 0}>
<box flexShrink={0} gap={list() ? 1 : 0}>
@@ -132,8 +132,8 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
return file !== undefined && (props.reviewedFileNames?.has(file) ?? false)
}
const foreground = () => {
if (row.kind === "directory") return theme.text.subdued
return reviewed() ? theme.text.subdued : theme.text.default
if (row.kind === "directory") return theme.text.muted
return reviewed() ? theme.text.muted : theme.text.base
}
const background = () => {
// Elevated context maps this to a quiet neutral surface step, not the loud accent.
@@ -151,11 +151,11 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
})
const status = () => fileTreeRowStatus(row, props.files, reviewed())
const statusColor = () => {
if (reviewed()) return theme.text.subdued
if (reviewed()) return theme.text.muted
const status = row.fileIndex === undefined ? undefined : props.files[row.fileIndex]?.status
if (status === "added") return theme.diff.text.added
if (status === "deleted") return theme.diff.text.removed
return theme.text.subdued
return theme.text.muted
}
const name = () => {
const width = contentWidth() - stringWidth(indent()) - stringWidth(marker())
@@ -30,14 +30,14 @@ export function DiffViewerImage(props: {
return (
<box width="100%" flexShrink={0} gap={1} paddingLeft={1} paddingRight={1} paddingBottom={1}>
<text fg={theme.text.subdued}>{props.label ?? "Working tree preview"}</text>
<text fg={theme.text.muted}>{props.label ?? "Working tree preview"}</text>
<box height={height() + 2} flexShrink={0} gap={1}>
<Switch>
<Match when={image.error}>
<text fg={theme.text.feedback.error.default}>Could not load image</text>
<text fg={theme.text.feedback.error.base}>Could not load image</text>
</Match>
<Match when={image.loading}>
<text fg={theme.text.subdued}>Loading image…</text>
<text fg={theme.text.muted}>Loading image…</text>
</Match>
<Match when={!image.error && image()} keyed>
{(bytes) => {
@@ -61,7 +61,7 @@ export function DiffViewerImage(props: {
return (
<Show
when={!failed()}
fallback={<text fg={theme.text.feedback.error.default}>Could not decode image</text>}
fallback={<text fg={theme.text.feedback.error.base}>Could not decode image</text>}
>
<box width="100%" height={height()} onMouseUp={open}>
<image
@@ -78,8 +78,8 @@ export function DiffViewerImage(props: {
<Show when={size()}>
{(value) => (
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text.subdued}>{value()}</text>
<text fg={theme.text.action.secondary.default} onMouseUp={open}>
<text fg={theme.text.muted}>{value()}</text>
<text fg={theme.text.action.secondary.base} onMouseUp={open}>
Click to enlarge
</text>
</box>
@@ -221,7 +221,7 @@ function DiffBaseDialog(props: {
)
const Empty = () => (
<box paddingLeft={4} paddingRight={4}>
<text fg={branches.error ? theme.text.feedback.error.default : theme.text.subdued}>
<text fg={branches.error ? theme.text.feedback.error.base : theme.text.muted}>
{branches.loading
? "Loading branches…"
: branches.error
@@ -240,7 +240,7 @@ function DiffBaseDialog(props: {
onFilter={setSearch}
emptyView={<Empty />}
noMatchView={<Empty />}
footer={<text fg={theme.text.subdued}>Remembered until the TUI exits</text>}
footer={<text fg={theme.text.muted}>Remembered until the TUI exits</text>}
options={(branches.loading || branches.error ? [] : (branches()?.data ?? [])).map((name) => ({
title: name,
value: name,
@@ -758,7 +758,7 @@ export function DiffViewerContent(props: {
{(shortcut) => (
<text
id="diff-help-shortcut"
fg={theme.text.default}
fg={theme.text.base}
selectable={false}
flexShrink={0}
wrapMode="none"
@@ -770,7 +770,7 @@ export function DiffViewerContent(props: {
>
{props.compact ? "?" : shortcut()}
<Show when={!props.compact}>
<span style={{ fg: theme.text.subdued }}> help</span>
<span style={{ fg: theme.text.muted }}> help</span>
</Show>
</text>
)}
@@ -782,7 +782,7 @@ export function DiffViewerContent(props: {
}))
return (
<box width="100%" height="100%" backgroundColor={theme.background.default}>
<box width="100%" height="100%" backgroundColor={theme.background.base}>
<Show when={!showFileTree()}>
<box
id="diff-source-header"
@@ -805,7 +805,7 @@ export function DiffViewerContent(props: {
}}
>
<text
fg={theme.text.action.secondary.default}
fg={theme.text.action.secondary.base}
attributes={TextAttributes.BOLD}
selectable={false}
flexShrink={0}
@@ -814,12 +814,12 @@ export function DiffViewerContent(props: {
{diffSourceLabel(mode())}
</text>
<Show when={props.sourceDetail}>
<text fg={theme.text.subdued} selectable={false} flexGrow={1} minWidth={0} wrapMode="none" truncate>
<text fg={theme.text.muted} selectable={false} flexGrow={1} minWidth={0} wrapMode="none" truncate>
{` · ${props.sourceDetail}`}
</text>
</Show>
</box>
<text id="diff-review-count" fg={theme.text.subdued} flexShrink={0} wrapMode="none">
<text id="diff-review-count" fg={theme.text.muted} flexShrink={0} wrapMode="none">
{files().filter((file) => reviewedFileNames().has(file.file)).length}/{files().length}
</text>
</box>
@@ -828,12 +828,12 @@ export function DiffViewerContent(props: {
<Switch>
<Match when={props.loading}>
<box flexGrow={1} padding={2}>
<text fg={theme.text.subdued}>Loading diff…</text>
<text fg={theme.text.muted}>Loading diff…</text>
</box>
</Match>
<Match when={!props.loading && props.error}>
<box flexGrow={1} padding={2}>
<text fg={theme.text.feedback.error.default}>
<text fg={theme.text.feedback.error.base}>
{!props.sourceBase && mode() !== "working"
? "Could not load diff. Choose a base branch from Diff source, or select Uncommitted."
: "Could not load diff. Reopen the diff viewer to try again."}
@@ -842,14 +842,14 @@ export function DiffViewerContent(props: {
</Match>
<Match when={!props.loading && props.unavailable}>
<box flexGrow={1} padding={2}>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
Committed comparison unavailable without base metadata. Choose a base branch from Diff source.
</text>
</box>
</Match>
<Match when={!props.loading && files().length === 0}>
<box flexGrow={1} padding={2}>
<text fg={theme.text.subdued}>No changes to show</text>
<text fg={theme.text.muted}>No changes to show</text>
</box>
</Match>
<Match when={!props.loading}>
@@ -924,7 +924,7 @@ export function DiffViewerContent(props: {
flexShrink={0}
border={["top"]}
borderColor={background()}
backgroundColor={theme.background.default}
backgroundColor={theme.background.base}
customBorderChars={{ ...EmptyBorder, horizontal: "▄" }}
/>
</Show>
@@ -961,20 +961,20 @@ export function DiffViewerContent(props: {
<FilePath
value={entry.file.file}
maxWidth={Math.max(1, patchPaneWidth() - countsWidth() - 2)}
fg={theme.text.subdued}
basenameFg={reviewed() ? theme.text.subdued : theme.text.default}
fg={theme.text.muted}
basenameFg={reviewed() ? theme.text.muted : theme.text.base}
/>
</box>
<Show when={reviewed()}>
<text fg={theme.text.subdued} flexShrink={0}>
<text fg={theme.text.muted} flexShrink={0}>
✓
</text>
</Show>
<Show when={!image()} fallback={<text fg={theme.text.subdued}>Image</text>}>
<text flexShrink={0} fg={reviewed() ? theme.text.subdued : theme.diff.text.added}>
<Show when={!image()} fallback={<text fg={theme.text.muted}>Image</text>}>
<text flexShrink={0} fg={reviewed() ? theme.text.muted : theme.diff.text.added}>
+{entry.file.additions}
</text>
<text flexShrink={0} fg={reviewed() ? theme.text.subdued : theme.diff.text.removed}>
<text flexShrink={0} fg={reviewed() ? theme.text.muted : theme.diff.text.removed}>
-{entry.file.deletions}
</text>
</Show>
@@ -987,7 +987,7 @@ export function DiffViewerContent(props: {
height={1}
border={["bottom"]}
borderColor={background()}
backgroundColor={theme.background.default}
backgroundColor={theme.background.base}
customBorderChars={{ ...EmptyBorder, horizontal: "▀" }}
/>
</Show>
@@ -996,7 +996,7 @@ export function DiffViewerContent(props: {
<Switch
fallback={
<box width="100%" flexShrink={0} paddingLeft={1} paddingRight={1} paddingBottom={1}>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
{mode() === "committed" && image()
? "Committed image preview unavailable. The working-tree image is not shown."
: entry.file.status === "deleted" && image()
@@ -1031,7 +1031,7 @@ export function DiffViewerContent(props: {
showLineNumbers={true}
width="100%"
wrapMode="char"
fg={theme.text.default}
fg={theme.text.base}
addedBg={theme.diff.background.added}
removedBg={theme.diff.background.removed}
contextBg={theme.diff.background.context}
@@ -1125,10 +1125,10 @@ function DiffViewerHelpDialog(props: { context: Plugin.Context; single: boolean
return (
<box paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
Diff shortcuts
</text>
<text fg={theme.text.subdued} selectable={false} onMouseUp={() => props.context.ui.dialog.clear()}>
<text fg={theme.text.muted} selectable={false} onMouseUp={() => props.context.ui.dialog.clear()}>
esc close
</text>
</box>
@@ -1149,16 +1149,16 @@ function DiffViewerHelpDialog(props: { context: Plugin.Context; single: boolean
<For each={groups}>
{(group) => (
<box flexShrink={0}>
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
{group.title}
</text>
<For each={group.rows}>
{(row) => (
<box flexDirection="row" gap={2}>
<text fg={theme.text.default} width={17} flexShrink={0}>
<text fg={theme.text.base} width={17} flexShrink={0}>
{row.shortcut() || "unbound"}
</text>
<text fg={theme.text.subdued} flexGrow={1} minWidth={0}>
<text fg={theme.text.muted} flexGrow={1} minWidth={0}>
{row.label}
</text>
</box>
@@ -98,14 +98,14 @@ export function PluginsDialog(props: {
footer: updating(entry) ? "updating" : footer(entry),
footerColor:
status(entry) === "failed"
? props.context.theme.text.feedback.error.default
? props.context.theme.text.feedback.error.base
: outdated(entry)
? props.context.theme.text.feedback.info.default
: props.context.theme.text.subdued,
? props.context.theme.text.feedback.info.base
: props.context.theme.text.muted,
gutter: updating(entry)
? (color) => <Spinner color={color} />
: status(entry) === "failed"
? () => <text fg={props.context.theme.text.feedback.error.default}>x</text>
? () => <text fg={props.context.theme.text.feedback.error.base}>x</text>
: undefined,
}),
),
@@ -249,10 +249,10 @@ export function PluginsDialog(props: {
footer={
<Show when={pluginError(focusedEntry()) && !focusedTui()}>
<text>
<span style={{ fg: props.context.theme.text.default }}>
<span style={{ fg: props.context.theme.text.base }}>
<b>enter</b>
</span>
<span style={{ fg: props.context.theme.text.subdued }}> view error</span>
<span style={{ fg: props.context.theme.text.muted }}> view error</span>
</text>
</Show>
}
@@ -52,19 +52,19 @@ export function StatsPoster(props: { stats: SessionStatsInfo }) {
}),
)
const shades = createMemo(() => [
theme.text.subdued,
theme.text.muted,
...[0.3, 0.5, 0.75, 1].map((alpha) =>
tint(theme.background.default, theme.categorical[0][200], alpha),
tint(theme.background.base, theme.categorical[0][200], alpha),
),
])
return (
<box width={width()} flexDirection="column" alignItems="center" flexShrink={0} gap={compact() ? 1 : 2}>
<box width="100%" flexDirection={width() < 44 ? "column" : "row"} justifyContent="space-between">
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
opencode / stats
</text>
<text fg={theme.text.subdued}>{dates()}</text>
<text fg={theme.text.muted}>{dates()}</text>
</box>
<Show when={!compact()}>
<Logo />
@@ -73,7 +73,7 @@ export function StatsPoster(props: { stats: SessionStatsInfo }) {
<Show
when={large()}
fallback={
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
{number()}
</text>
}
@@ -81,7 +81,7 @@ export function StatsPoster(props: { stats: SessionStatsInfo }) {
<box>
<For each={[0, 1, 2, 3, 4]}>
{(row) => (
<text fg={theme.text.default} selectable={false}>
<text fg={theme.text.base} selectable={false}>
{letters()
.map((char) => char[row].replaceAll("1", "\u2588\u2588").replaceAll("0", " "))
.join(" ")}
@@ -90,10 +90,10 @@ export function StatsPoster(props: { stats: SessionStatsInfo }) {
</For>
</box>
</Show>
<text fg={theme.text.subdued}>TOKENS</text>
<text fg={theme.text.muted}>TOKENS</text>
</box>
<box alignItems="center">
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
{" " +
calendar()
.months.map((month) => (month.label.length <= month.span * 2 ? month.label : "").padEnd(month.span * 2))
@@ -102,7 +102,7 @@ export function StatsPoster(props: { stats: SessionStatsInfo }) {
<For each={["M", "T", "W", "T", "F", "S", "S"]}>
{(day, index) => (
<box flexDirection="row" height={1}>
<text fg={theme.text.subdued}>{day + " "}</text>
<text fg={theme.text.muted}>{day + " "}</text>
<For each={calendar().weeks}>
{(week) => (
<text fg={shades()[Math.max(0, week[index()].level)]} selectable={false}>
@@ -114,24 +114,24 @@ export function StatsPoster(props: { stats: SessionStatsInfo }) {
)}
</For>
<Show when={calendar().clipped}>
<text fg={theme.text.subdued}>Your last {calendar().weeks.length} weeks</text>
<text fg={theme.text.muted}>Your last {calendar().weeks.length} weeks</text>
</Show>
</box>
<box width="100%" flexDirection="row" justifyContent="space-around">
<For each={metrics().slice(1)}>
{(metric) => (
<box alignItems="center">
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
{statsNumber(metric.value)}
{metric.label === "best streak" ? " days" : ""}
</text>
<text fg={theme.text.subdued}>{metric.label}</text>
<text fg={theme.text.muted}>{metric.label}</text>
</box>
)}
</For>
</box>
<box width="100%" flexDirection="row" justifyContent="flex-end">
<text fg={theme.text.default}>opencode.ai</text>
<text fg={theme.text.base}>opencode.ai</text>
</box>
</box>
)
@@ -153,7 +153,7 @@ function StatsPage(props: { context: Plugin.Context; onClose: () => void }) {
}))
return (
<box width="100%" height="100%" backgroundColor={theme.background.default}>
<box width="100%" height="100%" backgroundColor={theme.background.base}>
<scrollbox
flexGrow={1}
contentOptions={{
@@ -167,10 +167,10 @@ function StatsPage(props: { context: Plugin.Context; onClose: () => void }) {
<Show
when={!result.error}
fallback={
<text fg={theme.text.feedback.error.default}>Could not load stats. Reopen /stats to try again.</text>
<text fg={theme.text.feedback.error.base}>Could not load stats. Reopen /stats to try again.</text>
}
>
<Show when={result()} fallback={<text fg={theme.text.subdued}>Gathering your stats…</text>}>
<Show when={result()} fallback={<text fg={theme.text.muted}>Gathering your stats…</text>}>
{(value) => <StatsPoster stats={value()} />}
</Show>
</Show>
@@ -19,16 +19,16 @@ export function StoryFooter(props: {
return (
<box flexShrink={0} flexDirection="column" backgroundColor={theme.background.raised.base}>
<box height={1} paddingLeft={1} paddingRight={1} flexDirection="row">
<text fg={theme.text.default}>{props.title}</text>
<text fg={theme.text.base}>{props.title}</text>
<Show when={props.details?.length}>
<text fg={theme.text.subdued}> · {props.details?.join(" · ")}</text>
<text fg={theme.text.muted}> · {props.details?.join(" · ")}</text>
</Show>
</box>
<Show when={props.status || props.message}>
<box height={1} paddingLeft={1} paddingRight={1} flexDirection="row">
<text fg={theme.text.default} wrapMode="none">
<text fg={theme.text.base} wrapMode="none">
{props.status ?? ""}
<span style={{ fg: theme.text.subdued }}>
<span style={{ fg: theme.text.muted }}>
{props.status && props.message ? " · " : ""}
{props.message ?? ""}
</span>
@@ -38,8 +38,8 @@ export function StoryFooter(props: {
<box paddingLeft={1} paddingRight={1} flexDirection="row" flexWrap="wrap" columnGap={1}>
<For each={props.controls}>
{(control) => (
<text fg={theme.text.default} wrapMode="none" flexShrink={0}>
{control.shortcut} <span style={{ fg: theme.text.subdued }}>{control.label}</span>
<text fg={theme.text.base} wrapMode="none" flexShrink={0}>
{control.shortcut} <span style={{ fg: theme.text.muted }}>{control.label}</span>
</text>
)}
</For>
@@ -97,15 +97,15 @@ function StorybookIndex(props: { context: Plugin.Context }) {
width={dimensions().width}
height={dimensions().height}
flexDirection="column"
backgroundColor={theme.background.default}
backgroundColor={theme.background.base}
>
<box paddingTop={2} paddingLeft={2} flexDirection="column">
<text fg={theme.text.default}>storybook</text>
<text fg={theme.text.subdued}>fixture-driven simulations of production components</text>
<text fg={theme.text.base}>storybook</text>
<text fg={theme.text.muted}>fixture-driven simulations of production components</text>
<box height={1} />
<For each={stories}>
{(story, index) => (
<text fg={index() === selected() ? theme.text.default : theme.text.subdued}>
<text fg={index() === selected() ? theme.text.base : theme.text.muted}>
{index() === selected() ? "› " : " "}
{index() + 1} {story.title}
</text>
@@ -156,14 +156,14 @@ function MermanLayoutsStory(props: { context: Plugin.Context }) {
width={dimensions().width}
height={dimensions().height}
flexDirection="column"
backgroundColor={theme.background.default}
backgroundColor={theme.background.base}
>
<Show when={rendered()} keyed>
{(item) => (
<scrollbox flexGrow={1} minHeight={0} viewportOptions={{ paddingRight: 1 }}>
<box paddingLeft={2} paddingRight={2} paddingTop={1} flexDirection="column">
<text fg={theme.text.default}>{item.fixture.title}</text>
<text fg={theme.text.subdued}>{item.fixture.id}</text>
<text fg={theme.text.base}>{item.fixture.title}</text>
<text fg={theme.text.muted}>{item.fixture.id}</text>
<box height={1} />
<markdown
width="100%"
@@ -173,7 +173,7 @@ function MermanLayoutsStory(props: { context: Plugin.Context }) {
tableOptions={{ style: "grid", cellPaddingX: 1 }}
conceal={true}
fg={theme.markdown.text}
bg={theme.background.default}
bg={theme.background.base}
renderNode={plugins.markdown()}
/>
</box>
@@ -127,12 +127,12 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) {
<box
width={dimensions().width}
height={dimensions().height}
backgroundColor={theme.background.default}
backgroundColor={theme.background.base}
justifyContent={solo() ? "center" : undefined}
alignItems={solo() ? "center" : undefined}
>
<Show when={!solo()}>
<text fg={theme.text.default} flexShrink={0}>
<text fg={theme.text.base} flexShrink={0}>
one-cell motion lab.
</text>
</Show>
@@ -141,17 +141,17 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) {
<>
<Show when={!solo()}>
<box flexDirection="row" height={1} flexShrink={0} paddingLeft={1}>
<text width={22} fg={theme.text.subdued}>
<text width={22} fg={theme.text.muted}>
pattern
</text>
<For each={speeds()}>
{(value) => (
<text width={7} fg={theme.text.subdued}>
<text width={7} fg={theme.text.muted}>
{value}x
</text>
)}
</For>
<text fg={theme.text.subdued}>cycle @1x</text>
<text fg={theme.text.muted}>cycle @1x</text>
</box>
<scrollbox
ref={scroll}
@@ -166,7 +166,7 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) {
<text
width={22}
wrapMode="none"
fg={index() === selected() ? theme.text.formfield.selected : theme.text.formfield.default}
fg={index() === selected() ? theme.text.formfield.selected : theme.text.formfield.base}
>
{index() === selected() ? ">" : " "}
{String(index() + 1).padStart(2)} {item.name.toLowerCase()}.
@@ -186,11 +186,11 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) {
</box>
)}
</For>
<text width={10} fg={theme.text.subdued}>
<text width={10} fg={theme.text.muted}>
{item.pace ? "adaptive" : `${item.frames.length * item.interval}ms`}
</text>
<Show when={dimensions().width >= 80}>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
{[...new Set(item.frames)].join(" ")}
{item.levels ? " + intensity" : ""}
</text>
@@ -207,15 +207,15 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) {
paddingRight={1}
alignItems={solo() ? "center" : undefined}
>
<text fg={theme.text.default} maxWidth="100%" attributes={solo() ? TextAttributes.BOLD : 0}>
<text fg={theme.text.base} maxWidth="100%" attributes={solo() ? TextAttributes.BOLD : 0}>
<Show when={!solo()}>{String(selected() + 1).padStart(2, "0")} / </Show>
{animation().name.toLowerCase()}.
</text>
<Show when={!solo()}>
<text fg={theme.text.subdued} maxWidth="100%">
<text fg={theme.text.muted} maxWidth="100%">
{animation().description}
</text>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
{speed()}x: {timing()}
</text>
</Show>
@@ -226,7 +226,7 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) {
marginTop={solo() ? 1 : 0}
>
<box height={1} flexDirection="row">
<text width={7} fg={theme.text.subdued}>
<text width={7} fg={theme.text.muted}>
work
</text>
<OneCellSpinner
@@ -238,10 +238,10 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) {
glow={glow()}
color={theme.text.status.running}
/>
<text fg={theme.text.default}> esc stop</text>
<text fg={theme.text.base}> esc stop</text>
</box>
<box height={1} flexDirection="row">
<text width={7} fg={theme.text.subdued}>
<text width={7} fg={theme.text.muted}>
launch
</text>
<OneCellSpinner
@@ -250,11 +250,11 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) {
animations={animations()}
paused={paused()}
glow={glow()}
color={theme.text.default}
color={theme.text.base}
/>
<text fg={theme.text.default} wrapMode="none">
<text fg={theme.text.base} wrapMode="none">
{splash().label.slice(1)}
<span style={{ fg: theme.text.subdued }}>{splash().metadata}</span>
<span style={{ fg: theme.text.muted }}>{splash().metadata}</span>
</text>
</box>
</box>
@@ -47,16 +47,16 @@ function SessionLocationMissingStory(props: { context: Plugin.Context }) {
return (
<box width={dimensions().width} height={dimensions().height} backgroundColor={theme.background.raised.base}>
<box paddingLeft={2} paddingRight={2} paddingTop={1} flexGrow={1}>
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
Workerd Modal workspace driver
</text>
<text fg={theme.text.subdued}>build · GPT-5.6 Sol (high)</text>
<text fg={theme.text.muted}>build · GPT-5.6 Sol (high)</text>
<box height={1} />
<text fg={theme.text.default}>You</text>
<text fg={theme.text.subdued}>Test the mounted workspace and verify the deployment.</text>
<text fg={theme.text.base}>You</text>
<text fg={theme.text.muted}>Test the mounted workspace and verify the deployment.</text>
<box height={1} />
<text fg={theme.text.default}>Build · GPT-5.6 Sol (high)</text>
<text fg={theme.text.subdued}>The deployment is verified and the worktree is clean.</text>
<text fg={theme.text.base}>Build · GPT-5.6 Sol (high)</text>
<text fg={theme.text.muted}>The deployment is verified and the worktree is clean.</text>
<box flexGrow={1} />
<SessionLocationUnavailable directory={directory} onMove={open} />
</box>
@@ -258,7 +258,7 @@ function SessionTabsStory(props: { context: Plugin.Context }) {
// sessions; the tail line tracks the live status of the current run.
const transcript = () => {
const current = active()
if (!current) return [{ text: "no session selected", color: theme.text.subdued }]
if (!current) return [{ text: "no session selected", color: theme.text.muted }]
const index = Math.max(
0,
FIXTURE_TABS.findIndex((fixture) => fixture.sessionID === current),
@@ -268,20 +268,20 @@ function SessionTabsStory(props: { context: Plugin.Context }) {
const outcome = outcomes()[current]
const file = TRANSCRIPT_FILES[index % TRANSCRIPT_FILES.length]
const lines = [
{ text: `> ${fixture.title}`, color: theme.text.default },
{ text: "", color: theme.text.default },
{ text: `> ${fixture.title}`, color: theme.text.base },
{ text: "", color: theme.text.base },
]
if (!status.busy && !status.attention && outcome === undefined) {
lines.push({ text: "no activity yet — press s to run this session", color: theme.text.subdued })
lines.push({ text: "no activity yet — press s to run this session", color: theme.text.muted })
return lines
}
lines.push(
{ text: "● Taking a look — reading the relevant code first.", color: theme.text.default },
{ text: "", color: theme.text.default },
{ text: ` ✱ Read ${file}`, color: theme.text.subdued },
{ text: ` ✱ Edit ${file}`, color: theme.text.subdued },
{ text: ` ✱ Bash bun run test`, color: theme.text.subdued },
{ text: "", color: theme.text.default },
{ text: "● Taking a look — reading the relevant code first.", color: theme.text.base },
{ text: "", color: theme.text.base },
{ text: ` ✱ Read ${file}`, color: theme.text.muted },
{ text: ` ✱ Edit ${file}`, color: theme.text.muted },
{ text: ` ✱ Bash bun run test`, color: theme.text.muted },
{ text: "", color: theme.text.base },
)
if (status.attention === "question")
lines.push({ text: "? Which approach should I take?", color: theme.text.status.question })
@@ -291,12 +291,12 @@ function SessionTabsStory(props: { context: Plugin.Context }) {
else if (outcome === "failed")
lines.push({
text: `✗ bun run test failed — 3 tests failing in ${file}`,
color: theme.text.feedback.error.default,
color: theme.text.feedback.error.base,
})
else
lines.push({
text: `✓ Done — updated ${file} and the tests pass.`,
color: theme.text.feedback.success.default,
color: theme.text.feedback.success.base,
})
return lines
}
@@ -493,7 +493,7 @@ function SessionTabsStory(props: { context: Plugin.Context }) {
width={dimensions().width}
height={dimensions().height}
flexDirection="column"
backgroundColor={theme.background.default}
backgroundColor={theme.background.base}
>
<box
flexGrow={1}
+21 -21
View File
@@ -116,8 +116,8 @@ function map(
): RunTheme {
// V1 system migration serializes colors; restore terminal defaults before quantizing scrollback.
const exact = (color: RGBA) => {
if (system && color.equals(theme.text.default)) return RGBA.defaultForeground(color)
if (system && color.equals(theme.background.default)) return RGBA.defaultBackground(color)
if (system && color.equals(theme.text.base)) return RGBA.defaultForeground(color)
if (system && color.equals(theme.background.base)) return RGBA.defaultBackground(color)
return color
}
const scrollback = (color: RGBA) => {
@@ -133,50 +133,50 @@ function map(
})
return {
background: RGBA.defaultBackground(theme.background.default),
background: RGBA.defaultBackground(theme.background.base),
footer: {
actionSecondaryText: exact(theme.text.action.secondary.default),
actionSecondaryText: exact(theme.text.action.secondary.base),
actionFocusedBg: exact(theme.background.action.primary.focused),
actionFocusedText: exact(theme.text.action.primary.focused),
formfieldText: exact(theme.text.formfield.default),
formfieldText: exact(theme.text.formfield.base),
formfieldFocusedBg: exact(theme.background.formfield.focused),
formfieldFocusedText: exact(theme.text.formfield.focused),
selection: exact(theme.text.formfield.selected),
running: exact(theme.text.status.running),
question: exact(theme.text.status.question),
permission: exact(theme.text.status.permission),
success: exact(theme.text.feedback.success.default),
success: exact(theme.text.feedback.success.base),
link: exact(theme.markdown.link),
categorical: dedupeWith(
theme.categorical.map((scale) => exact(scale[200])),
(a, b) => a.equals(b),
),
warning: exact(theme.text.feedback.warning.default),
error: exact(theme.text.feedback.error.default),
muted: exact(theme.text.subdued),
text: exact(theme.text.default),
warning: exact(theme.text.feedback.warning.base),
error: exact(theme.text.feedback.error.base),
muted: exact(theme.text.muted),
text: exact(theme.text.base),
shade: exact(theme.background.raised.base),
surface: exact(theme.background.raised.base),
pane: exact(theme.background.raised.high),
border: exact(theme.border.default),
border: exact(theme.border.base),
line: exact(theme.background.raised.high),
},
entry: {
system: { body: scrollback(theme.text.subdued) },
user: { body: scrollback(theme.text.default) },
system: { body: scrollback(theme.text.muted) },
user: { body: scrollback(theme.text.base) },
assistant: { body: scrollback(theme.markdown.text) },
reasoning: { body: scrollback(theme.text.subdued) },
tool: { body: scrollback(theme.text.subdued), start: scrollback(theme.text.subdued) },
error: { body: scrollback(theme.text.feedback.error.default) },
reasoning: { body: scrollback(theme.text.muted) },
tool: { body: scrollback(theme.text.muted), start: scrollback(theme.text.muted) },
error: { body: scrollback(theme.text.feedback.error.base) },
},
splash: {
left: nearestIndexed(indexed, theme.text.subdued),
right: nearestIndexed(indexed, theme.text.default),
left: nearestIndexed(indexed, theme.text.muted),
right: nearestIndexed(indexed, theme.text.base),
leftShadow: nearestIndexed(indexed, theme.background.raised.base),
},
block: {
text: scrollback(theme.text.default),
muted: scrollback(theme.text.subdued),
text: scrollback(theme.text.base),
muted: scrollback(theme.text.muted),
syntax,
diffRemoved: scrollback(theme.diff.text.removed),
diffAddedBg: scrollback(theme.diff.background.added),
@@ -298,7 +298,7 @@ export async function resolveRunTheme(
: ansiPalette
return {
...map(theme, indexed, generateSyntax(theme), name === "system" && resolved !== undefined),
background: RGBA.defaultBackground(colors?.defaultBackground ?? theme.background.default),
background: RGBA.defaultBackground(colors?.defaultBackground ?? theme.background.base),
}
}
+2 -2
View File
@@ -126,7 +126,7 @@ function UpdateNotification(props: { width: number }) {
const exit = useExit()
const theme = useTheme()
const [hovered, setHovered] = createSignal(false)
const backdrop = () => (hovered() ? theme.background.action.primary.hovered : theme.background.default)
const backdrop = () => (hovered() ? theme.background.action.primary.hovered : theme.background.base)
createEffect(() => {
update.notification()
setHovered(false)
@@ -154,7 +154,7 @@ function UpdateNotification(props: { width: number }) {
update.open?.("notification")
}}
>
<FadeInText fg={theme.text.subdued} backdrop={backdrop()}>
<FadeInText fg={theme.text.muted} backdrop={backdrop()}>
<Show when={!remote}>
<span style={{ fg: theme.text.action.primary.selected }}>
{state.type === "installed" ? "/exit" : "/update"}
@@ -113,7 +113,7 @@ export function Composer(props: ComposerProps) {
<box
{...SplitBorder}
border={["left"]}
borderColor={theme.border.default}
borderColor={theme.border.base}
backgroundColor={theme.background.raised.base}
paddingLeft={1}
paddingRight={2}
@@ -125,7 +125,7 @@ export function Composer(props: ComposerProps) {
<Show
when={tabList().length > 1}
fallback={
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
{tabList()[0]?.label ?? ""}
</text>
}
@@ -136,7 +136,7 @@ export function Composer(props: ComposerProps) {
const isActive = createMemo(() => store.active === t.id)
return (
<text
fg={isActive() ? theme.text.default : theme.text.subdued}
fg={isActive() ? theme.text.base : theme.text.muted}
attributes={isActive() ? TextAttributes.BOLD : undefined}
>
{t.label}
@@ -146,7 +146,7 @@ export function Composer(props: ComposerProps) {
</For>
</box>
</Show>
<text fg={theme.text.subdued} onMouseUp={close}>
<text fg={theme.text.muted} onMouseUp={close}>
esc
</text>
</box>
@@ -159,19 +159,19 @@ export function Composer(props: ComposerProps) {
<For each={footerHints()}>
{(hint) => (
<text>
<span style={{ fg: theme.text.default }}>
<span style={{ fg: theme.text.base }}>
<b>{hint.label}</b>{" "}
</span>
<span style={{ fg: theme.text.subdued }}>{hint.shortcut}</span>
<span style={{ fg: theme.text.muted }}>{hint.shortcut}</span>
</text>
)}
</For>
<Show when={tabList().length > 1}>
<text>
<span style={{ fg: theme.text.default }}>
<span style={{ fg: theme.text.base }}>
<b>tabs</b>{" "}
</span>
<span style={{ fg: theme.text.subdued }}>←/→</span>
<span style={{ fg: theme.text.muted }}>←/→</span>
</text>
</Show>
</box>
@@ -113,7 +113,7 @@ export function ShellTab(props: { sessionID: string }) {
return (
<Show when={composer.active("shell")}>
<scrollbox scrollbarOptions={{ visible: false }} maxHeight={5} ref={(r: ScrollBoxRenderable) => (scroll = r)}>
<Show when={entries().length > 0} fallback={<text fg={theme.text.subdued}> No shell commands</text>}>
<Show when={entries().length > 0} fallback={<text fg={theme.text.muted}> No shell commands</text>}>
<For each={entries()}>
{(shell, index) => {
const active = createMemo(() => index() === store.selected)
@@ -123,7 +123,7 @@ export function ShellTab(props: { sessionID: string }) {
paddingLeft={1}
paddingRight={1}
backgroundColor={
active() ? theme.background.action.primary.focused : theme.background.action.primary.default
active() ? theme.background.action.primary.focused : theme.background.action.primary.base
}
onMouseMove={() => setStore("selected", index())}
onMouseUp={() => {
@@ -132,7 +132,7 @@ export function ShellTab(props: { sessionID: string }) {
}}
>
<text
fg={active() ? theme.text.action.primary.focused : theme.text.action.primary.default}
fg={active() ? theme.text.action.primary.focused : theme.text.action.primary.base}
attributes={active() ? TextAttributes.BOLD : undefined}
wrapMode="none"
>
@@ -194,7 +194,7 @@ export function SubagentsTab(props: { sessionID: string }) {
<scrollbox scrollbarOptions={{ visible: false }} maxHeight={5} ref={(r: ScrollBoxRenderable) => (scroll = r)}>
<Show
when={entries().length > 0}
fallback={<text fg={theme.text.subdued}> No {store.active ? "active" : "inactive"} subagents</text>}
fallback={<text fg={theme.text.muted}> No {store.active ? "active" : "inactive"} subagents</text>}
>
<For each={entries()}>
{(entry, index) => {
@@ -213,7 +213,7 @@ export function SubagentsTab(props: { sessionID: string }) {
? theme.background.action.primary.focused
: entry.current
? theme.background.action.primary.selected
: theme.background.action.primary.default
: theme.background.action.primary.base
}
onMouseMove={() => setStore("selected", index())}
onMouseUp={() => {
@@ -228,7 +228,7 @@ export function SubagentsTab(props: { sessionID: string }) {
? theme.text.action.primary.focused
: entry.current
? theme.text.action.primary.selected
: theme.text.action.primary.default
: theme.text.action.primary.base
}
attributes={active() ? TextAttributes.BOLD : undefined}
wrapMode="none"
@@ -238,7 +238,7 @@ export function SubagentsTab(props: { sessionID: string }) {
</text>
</box>
<Show when={status()}>
<text fg={active() ? theme.text.action.primary.focused : theme.text.subdued} wrapMode="none">
<text fg={active() ? theme.text.action.primary.focused : theme.text.muted} wrapMode="none">
{status()}
</text>
</Show>
@@ -82,7 +82,7 @@ export function TerminalsTab(props: { sessionID: string; visibleTerminalID?: str
? theme.background.action.primary.focused
: current()
? theme.background.action.primary.selected
: theme.background.action.primary.default
: theme.background.action.primary.base
}
onMouseMove={() => setSelected(index())}
onMouseUp={() => {
@@ -96,7 +96,7 @@ export function TerminalsTab(props: { sessionID: string; visibleTerminalID?: str
? theme.text.action.primary.focused
: current()
? theme.text.action.primary.selected
: theme.text.action.primary.default
: theme.text.action.primary.base
}
attributes={active() ? TextAttributes.BOLD : undefined}
wrapMode="none"
@@ -102,11 +102,11 @@ export function DialogExecute(props: { part: SessionMessageAssistantTool }) {
return (
<box paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}>
<box flexDirection="row" gap={2}>
<text fg={theme.text.default} attributes={TextAttributes.BOLD} flexGrow={1}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD} flexGrow={1}>
execute
</text>
<text fg={failed() ? theme.text.feedback.error.default : theme.text.subdued}>{status()}</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={failed() ? theme.text.feedback.error.base : theme.text.muted}>{status()}</text>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
@@ -119,22 +119,22 @@ export function DialogExecute(props: { part: SessionMessageAssistantTool }) {
>
<box gap={1}>
<box>
<text fg={theme.text.subdued} attributes={TextAttributes.BOLD}>
<text fg={theme.text.muted} attributes={TextAttributes.BOLD}>
Code
</text>
<Show when={code()} fallback={<text fg={theme.text.subdued}>Waiting for code…</text>}>
<Show when={code()} fallback={<text fg={theme.text.muted}>Waiting for code…</text>}>
{(value) => <GutteredCode content={value()} filetype="typescript" digits={digits()} blocks={blocks} />}
</Show>
</box>
<box>
<text fg={theme.text.subdued} attributes={TextAttributes.BOLD}>
<text fg={theme.text.muted} attributes={TextAttributes.BOLD}>
Output
</text>
<Show
when={highlighted()}
fallback={
<text
fg={text() ? (failed() ? theme.text.feedback.error.default : theme.text.default) : theme.text.subdued}
fg={text() ? (failed() ? theme.text.feedback.error.base : theme.text.base) : theme.text.muted}
wrapMode="word"
>
{text() ?? (props.part.state.status === "completed" ? "No output" : "Waiting for output…")}
@@ -147,7 +147,7 @@ export function DialogExecute(props: { part: SessionMessageAssistantTool }) {
<Show when={body().rest}>
{(rest) => (
<box paddingLeft={digits() + 1}>
<text fg={failed() ? theme.text.feedback.error.default : theme.text.default} wrapMode="word">
<text fg={failed() ? theme.text.feedback.error.base : theme.text.base} wrapMode="word">
{rest()}
</text>
</box>
@@ -160,20 +160,20 @@ export function DialogExecute(props: { part: SessionMessageAssistantTool }) {
</box>
</scrollbox>
<box flexDirection="row" gap={3} flexWrap="wrap">
<text fg={theme.text.subdued}>↑/↓ ←/→ scroll</text>
<text fg={theme.text.muted}>↑/↓ ←/→ scroll</text>
<text onMouseUp={() => copy("code")}>
<span style={{ fg: copied() === "code" ? theme.text.feedback.success.default : theme.text.default }}>
<span style={{ fg: copied() === "code" ? theme.text.feedback.success.base : theme.text.base }}>
<b>{copied() === "code" ? "✓ copied" : "c"}</b>
</span>
<span style={{ fg: theme.text.subdued }}>{copied() === "code" ? "" : " copy code"}</span>
<span style={{ fg: theme.text.muted }}>{copied() === "code" ? "" : " copy code"}</span>
</text>
<text onMouseUp={() => copy("output")}>
<span style={{ fg: copied() === "output" ? theme.text.feedback.success.default : theme.text.default }}>
<span style={{ fg: copied() === "output" ? theme.text.feedback.success.base : theme.text.base }}>
<b>{copied() === "output" ? "✓ copied" : "o"}</b>
</span>
<span style={{ fg: theme.text.subdued }}>{copied() === "output" ? "" : " copy output"}</span>
<span style={{ fg: theme.text.muted }}>{copied() === "output" ? "" : " copy output"}</span>
</text>
<text fg={theme.text.subdued}>esc back</text>
<text fg={theme.text.muted}>esc back</text>
</box>
</box>
)
@@ -235,7 +235,7 @@ function GutteredCode(props: {
return (
<box flexDirection="row" gap={1} width="100%">
<text fg={theme.text.subdued} flexShrink={0} width={props.digits}>
<text fg={theme.text.muted} flexShrink={0} width={props.digits}>
{gutter()}
</text>
<box flexGrow={1} flexShrink={1} minWidth={0}>
@@ -244,7 +244,7 @@ function GutteredCode(props: {
width="100%"
conceal={false}
wrapMode="none"
fg={theme.text.default}
fg={theme.text.base}
filetype={props.filetype}
syntaxStyle={syntax()}
content={props.content}
+45 -45
View File
@@ -775,20 +775,20 @@ export function FormPrompt(props: { form: FormWithLocation }) {
>
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
<box paddingLeft={1}>
<text fg={theme.text.subdued}>{props.form.title}</text>
<text fg={theme.text.muted}>{props.form.title}</text>
</box>
<Show when={message()}>
<box paddingLeft={1}>
<text fg={theme.text.default}>{message()}</text>
<text fg={theme.text.base}>{message()}</text>
</box>
</Show>
<Show when={!single() && !tabbed()}>
<box flexDirection="row" gap={3} paddingLeft={1}>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
{confirm() ? "Review" : `Field ${Math.min(store.tab, fields().length - 1) + 1} of ${fields().length}`}
</text>
<Show when={fields().length > 0}>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
· {answered()}/{fields().length} completed
</text>
</Show>
@@ -801,10 +801,10 @@ export function FormPrompt(props: { form: FormWithLocation }) {
const isTab = () => index() === store.tab
const color = () =>
isTab()
? theme.text.default
? theme.text.base
: tabHover() === index()
? theme.text.formfield.focused
: theme.text.subdued
: theme.text.muted
return (
<box
paddingRight={2}
@@ -847,10 +847,10 @@ export function FormPrompt(props: { form: FormWithLocation }) {
<text
fg={
confirm()
? theme.text.default
? theme.text.base
: tabHover() === "confirm"
? theme.text.formfield.focused
: theme.text.subdued
: theme.text.muted
}
attributes={confirm() ? TextAttributes.BOLD : undefined}
>
@@ -864,13 +864,13 @@ export function FormPrompt(props: { form: FormWithLocation }) {
{(external) => (
<box paddingLeft={1} gap={1}>
<Show when={external().title}>
<text fg={theme.text.default}>{external().title}</text>
<text fg={theme.text.base}>{external().title}</text>
</Show>
<Show when={external().description}>
<text fg={theme.text.subdued}>{external().description}</text>
<text fg={theme.text.muted}>{external().description}</text>
</Show>
<text
fg={theme.text.action.primary.default}
fg={theme.text.action.primary.base}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
openExternal()
@@ -879,7 +879,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
{external().url}
</text>
<text
fg={store.answers[external().key] === true ? theme.text.feedback.success.default : theme.text.subdued}
fg={store.answers[external().key] === true ? theme.text.feedback.success.base : theme.text.muted}
>
{store.answers[external().key] === true
? "✓ Acknowledged"
@@ -894,7 +894,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
<Show when={!confirm() && answerField()}>
<box paddingLeft={1} gap={1}>
<box>
<text fg={theme.text.default}>{answerField()!.description ?? formLabel(answerField()!)}</text>
<text fg={theme.text.base}>{answerField()!.description ?? formLabel(answerField()!)}</text>
</box>
<Show when={textual() ? answerField()!.key : undefined} keyed>
<box paddingLeft={1}>
@@ -913,12 +913,12 @@ export function FormPrompt(props: { form: FormWithLocation }) {
input() || formDisplayValue(answerField()!, store.answers[answerField()!.key], "(none)")
}
placeholder={placeholder()}
placeholderColor={theme.text.subdued}
placeholderColor={theme.text.muted}
minHeight={1}
maxHeight={6}
textColor={theme.text.default}
focusedTextColor={theme.text.default}
cursorColor={theme.text.default}
textColor={theme.text.base}
focusedTextColor={theme.text.base}
cursorColor={theme.text.base}
/>
</box>
</Show>
@@ -947,7 +947,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
paddingRight={1}
>
<text
fg={active() ? theme.text.formfield.focused : theme.text.subdued}
fg={active() ? theme.text.formfield.focused : theme.text.muted}
>{`${i() + 1}.`}</text>
</box>
<box
@@ -963,13 +963,13 @@ export function FormPrompt(props: { form: FormWithLocation }) {
? theme.text.formfield.focused
: picked()
? theme.text.formfield.selected
: theme.text.subdued
: theme.text.muted
}
>
[{picked() ? "✓" : " "}]
</text>
</Show>
<text fg={active() ? theme.text.formfield.focused : theme.text.formfield.default}>
<text fg={active() ? theme.text.formfield.focused : theme.text.formfield.base}>
{row.label}
</text>
</box>
@@ -979,7 +979,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
</box>
<Show when={row.description}>
<box paddingLeft={multi() ? 7 : 3}>
<text fg={theme.text.subdued}>{row.description}</text>
<text fg={theme.text.muted}>{row.description}</text>
</box>
</Show>
</box>
@@ -1000,7 +1000,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
backgroundColor={other() ? theme.background.formfield.focused : theme.background.raised.base}
paddingRight={1}
>
<text fg={other() ? theme.text.formfield.focused : theme.text.subdued}>
<text fg={other() ? theme.text.formfield.focused : theme.text.muted}>
{`${rows().length + 1}.`}
</text>
</box>
@@ -1018,7 +1018,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
? theme.text.formfield.focused
: customChecked()
? theme.text.formfield.selected
: theme.text.subdued
: theme.text.muted
}
>
[{customChecked() ? "✓" : " "}]
@@ -1028,7 +1028,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
when={store.editing}
fallback={
<>
<text fg={other() ? theme.text.formfield.focused : theme.text.formfield.default}>
<text fg={other() ? theme.text.formfield.focused : theme.text.formfield.base}>
{input() || "Type your own answer"}
</text>
<Show when={!multi() && customPicked()}>
@@ -1052,7 +1052,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
}}
initialValue={input()}
placeholder="Type your own answer"
placeholderColor={theme.text.subdued}
placeholderColor={theme.text.muted}
minHeight={1}
maxHeight={6}
textColor={theme.text.formfield.focused}
@@ -1093,12 +1093,12 @@ export function FormPrompt(props: { form: FormWithLocation }) {
return (
<box paddingLeft={1}>
<text>
<span style={{ fg: theme.text.subdued }}>{truncate(formLabel(item), 40)}:</span>{" "}
<span style={{ fg: theme.text.muted }}>{truncate(formLabel(item), 40)}:</span>{" "}
<span
style={{
fg: acknowledged()
? theme.text.feedback.success.default
: theme.text.feedback.error.default,
? theme.text.feedback.success.base
: theme.text.feedback.error.base,
}}
>
{acknowledged() ? "Acknowledged" : "(acknowledgement required)"}
@@ -1114,15 +1114,15 @@ export function FormPrompt(props: { form: FormWithLocation }) {
return (
<box paddingLeft={1}>
<text>
<span style={{ fg: theme.text.subdued }}>{truncate(formLabel(item), 40)}:</span>{" "}
<span style={{ fg: theme.text.muted }}>{truncate(formLabel(item), 40)}:</span>{" "}
<span
style={{
fg:
invalid() || missing()
? theme.text.feedback.error.default
? theme.text.feedback.error.base
: answered()
? theme.text.default
: theme.text.subdued,
? theme.text.base
: theme.text.muted,
}}
>
{invalid() ?? (answered() ? value() : missing() ? "(required)" : "(not answered)")}
@@ -1147,41 +1147,41 @@ export function FormPrompt(props: { form: FormWithLocation }) {
>
<box flexDirection="row" gap={2}>
<Show when={!single()}>
<text fg={theme.text.default}>
{"⇆"} <span style={{ fg: theme.text.subdued }}>tab</span>
<text fg={theme.text.base}>
{"⇆"} <span style={{ fg: theme.text.muted }}>tab</span>
</text>
</Show>
<Show when={!confirm() && !textual() && !externalField() && !store.editing}>
<text fg={theme.text.default}>
{"↑↓"} <span style={{ fg: theme.text.subdued }}>select</span>
<text fg={theme.text.base}>
{"↑↓"} <span style={{ fg: theme.text.muted }}>select</span>
</text>
</Show>
<Show when={confirm() && reviewContentHeight() > reviewMaxHeight()}>
<text fg={theme.text.default}>
{"↑↓"} <span style={{ fg: theme.text.subdued }}>scroll</span>
<text fg={theme.text.base}>
{"↑↓"} <span style={{ fg: theme.text.muted }}>scroll</span>
</text>
</Show>
<text
fg={theme.text.default}
fg={theme.text.base}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
if (confirm()) submit()
if (externalField()) acknowledgeExternal()
}}
>
enter <span style={{ fg: theme.text.subdued }}>{actionLabel()}</span>
enter <span style={{ fg: theme.text.muted }}>{actionLabel()}</span>
</text>
<Show when={externalField()}>
<text fg={theme.text.default} onMouseUp={copyExternal}>
c <span style={{ fg: theme.text.subdued }}>copy</span>
<text fg={theme.text.base} onMouseUp={copyExternal}>
c <span style={{ fg: theme.text.muted }}>copy</span>
</text>
</Show>
<text fg={theme.text.default} onMouseUp={cancel}>
esc <span style={{ fg: theme.text.subdued }}>{store.editing && !textual() ? "close" : "dismiss"}</span>
<text fg={theme.text.base} onMouseUp={cancel}>
esc <span style={{ fg: theme.text.muted }}>{store.editing && !textual() ? "close" : "dismiss"}</span>
</text>
</box>
<Show when={store.error}>
<text fg={theme.text.feedback.error.default}>{store.error}</text>
<text fg={theme.text.feedback.error.base}>{store.error}</text>
</Show>
</box>
</box>
+98 -98
View File
@@ -1293,7 +1293,7 @@ export function Session(props: {
visible: showScrollbar(),
trackOptions: {
backgroundColor: theme.decrease(theme.background.raised.base),
foregroundColor: theme.border.default,
foregroundColor: theme.border.base,
},
}}
stickyScroll={!navigationMessage()}
@@ -1330,7 +1330,7 @@ export function Session(props: {
</box>
<box height={1} flexShrink={0} flexDirection="row" justifyContent="flex-end">
<Show when={firstJump()}>
<text fg={theme.text.feedback.info.default}>Loading session history…</text>
<text fg={theme.text.feedback.info.base}>Loading session history…</text>
</Show>
<Show when={!firstJump() && awayFromBottom()}>
<box
@@ -1341,7 +1341,7 @@ export function Session(props: {
onMouseUp={toBottom}
>
<text
fg={latestHovered() ? theme.text.action.secondary.hovered : theme.text.action.secondary.default}
fg={latestHovered() ? theme.text.action.secondary.hovered : theme.text.action.secondary.base}
>
Jump to latest ↓
</text>
@@ -1550,7 +1550,7 @@ function TurnTokenUsage(props: {
setExpanded((value) => !value)
}}
>
<text fg={hover() ? theme.text.default : theme.text.subdued} wrapMode="none">
<text fg={hover() ? theme.text.base : theme.text.muted} wrapMode="none">
<span>{expanded() ? "- " : "+ "}</span>
<span style={{ attributes: TextAttributes.BOLD }}>Tokens</span>
<span>
@@ -1558,7 +1558,7 @@ function TurnTokenUsage(props: {
new · {summary().cached.toLocaleString()} cached · {summary().total.toLocaleString()} total
</span>
<Show when={summary().reuseDrops > 0}>
<span style={{ fg: theme.text.feedback.warning.default }}>
<span style={{ fg: theme.text.feedback.warning.base }}>
{" "}
· ! {summary().reuseDrops} likely cache {summary().reuseDrops === 1 ? "bust" : "busts"}
</span>
@@ -1567,7 +1567,7 @@ function TurnTokenUsage(props: {
</box>
<Show when={expanded()}>
<box paddingLeft={INLINE_TOOL_ICON_WIDTH}>
<text fg={theme.text.subdued} attributes={TextAttributes.ITALIC}>
<text fg={theme.text.muted} attributes={TextAttributes.ITALIC}>
{"Step".padEnd(columns().step + 2)}
{"New".padStart(columns().newTokens)}
{" "}
@@ -1579,7 +1579,7 @@ function TurnTokenUsage(props: {
<For each={steps()}>
{(item) => (
<box paddingLeft={INLINE_TOOL_ICON_WIDTH} flexDirection="column">
<text fg={verbose() && item.finish === "tool-call" ? undefined : theme.text.subdued}>
<text fg={verbose() && item.finish === "tool-call" ? undefined : theme.text.muted}>
{item.finish.padEnd(columns().step + 2)}
<span style={{ attributes: TextAttributes.BOLD }}>
{item.newTokens.toLocaleString().padStart(columns().newTokens)}
@@ -1591,7 +1591,7 @@ function TurnTokenUsage(props: {
</text>
<TurnTokenToolCalls tools={item.tools} />
<Show when={item.reuseDrop !== undefined}>
<text fg={theme.text.feedback.warning.default}>
<text fg={theme.text.feedback.warning.base}>
! Likely cache bust: {item.reuseDrop?.toLocaleString()} fewer cached tokens than the previous step
</text>
</Show>
@@ -1613,10 +1613,10 @@ function TurnTokenToolCalls(props: { tools: SessionMessageAssistantTool[] }) {
<For each={props.tools}>
{(tool) => (
<box flexDirection="row">
<text width={nameWidth()} flexShrink={0} fg={theme.text.subdued} attributes={TextAttributes.BOLD}>
<text width={nameWidth()} flexShrink={0} fg={theme.text.muted} attributes={TextAttributes.BOLD}>
{tool.name}
</text>
<text fg={theme.text.subdued} attributes={TextAttributes.DIM} wrapMode="word" flexGrow={1} minWidth={0}>
<text fg={theme.text.muted} attributes={TextAttributes.DIM} wrapMode="word" flexGrow={1} minWidth={0}>
{turnTokenToolSummary(tool)}
</text>
</box>
@@ -1664,8 +1664,8 @@ function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) {
<Show when={visible() && shortcut()}>
{(value) => (
<box marginTop={1} paddingLeft={3} flexShrink={0}>
<text fg={theme.text.subdued}>
Press <span style={{ fg: theme.text.default }}>{value()}</span> to move running work to the background
<text fg={theme.text.muted}>
Press <span style={{ fg: theme.text.base }}>{value()}</span> to move running work to the background
</text>
</box>
)}
@@ -1747,7 +1747,7 @@ function SessionReasoningGroupView(props: {
const ctx = use()
const theme = useTheme()
const { currentSyntax: syntax } = useThemes()
const thinkingSyntax = createSyntaxStyleMemo(() => generateThinkingSyntax(syntax(), theme.text.subdued))
const thinkingSyntax = createSyntaxStyleMemo(() => generateThinkingSyntax(syntax(), theme.text.muted))
const renderer = useRenderer()
const [expanded, setExpanded] = createSignal(false)
const [hover, setHover] = createSignal(false)
@@ -1787,13 +1787,13 @@ function SessionReasoningGroupView(props: {
icon={expanded() ? "-" : "+"}
color={
!props.completed
? theme.text.default
? theme.text.base
: hover() || expanded()
? theme.text.feedback.warning.default
? theme.text.feedback.warning.base
: RGBA.fromValues(
theme.text.feedback.warning.default.r,
theme.text.feedback.warning.default.g,
theme.text.feedback.warning.default.b,
theme.text.feedback.warning.base.r,
theme.text.feedback.warning.base.g,
theme.text.feedback.warning.base.b,
0.6,
)
}
@@ -1846,7 +1846,7 @@ function SessionReasoningGroupView(props: {
syntaxStyle={thinkingSyntax()}
content={content()}
conceal={ctx.markdownMode() === "rendered"}
fg={theme.text.subdued}
fg={theme.text.muted}
/>
</box>
</box>
@@ -1907,7 +1907,7 @@ function SessionGroupView(props: {
<Show when={grouped().length > 0}>
<InlineToolRow
icon={completed() ? "→" : "✱"}
color={hover() ? theme.text.default : theme.text.subdued}
color={hover() ? theme.text.base : theme.text.muted}
complete={completed()}
pending={label()}
spinner={!completed()}
@@ -1956,26 +1956,26 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) {
<>
<Show when={props.message.error && !interrupted() && !props.message.retry}>
<box paddingLeft={3}>
<text fg={theme.text.feedback.error.default}>Error: {errorMessage(props.message.error)}</text>
<text fg={theme.text.feedback.error.base}>Error: {errorMessage(props.message.error)}</text>
</box>
</Show>
<AssistantRetry retry={props.message.retry} />
<box paddingLeft={3} marginTop={props.message.retry || (props.message.error && !interrupted()) ? 1 : 0}>
<text>
<span style={{ fg: props.message.error ? theme.text.subdued : local.agent.color(props.message.agent) }}>
<span style={{ fg: props.message.error ? theme.text.muted : local.agent.color(props.message.agent) }}>
{Locale.titlecase(props.message.agent)}
</span>
<Show when={ctx.terminal.width >= 28}>
<span style={{ fg: theme.text.subdued }}> · {model()}</span>
<span style={{ fg: theme.text.muted }}> · {model()}</span>
</Show>
<Show when={duration() && (ctx.terminal.width < 28 || ctx.terminal.width >= 36)}>
<span style={{ fg: theme.text.subdued }}> · {Locale.duration(duration())}</span>
<span style={{ fg: theme.text.muted }}> · {Locale.duration(duration())}</span>
</Show>
<Show when={config.data.session.tps && tokensPerSecond()}>
{(value) => <span style={{ fg: theme.text.subdued }}> · {value().toFixed(1)} tok/s</span>}
{(value) => <span style={{ fg: theme.text.muted }}> · {value().toFixed(1)} tok/s</span>}
</Show>
<Show when={interrupted()}>
<span style={{ fg: theme.text.subdued }}> · interrupted</span>
<span style={{ fg: theme.text.muted }}> · interrupted</span>
</Show>
</text>
</box>
@@ -1990,8 +1990,8 @@ function SessionSwitchMessageV2(props: { message: SessionMessageInfo }) {
return (
<box paddingLeft={3}>
<text>
<span style={{ fg: theme.text.subdued }}>↳ Moved to </span>
<span style={{ fg: theme.text.feedback.info.default }}>{props.message.location.directory}</span>
<span style={{ fg: theme.text.muted }}>↳ Moved to </span>
<span style={{ fg: theme.text.feedback.info.base }}>{props.message.location.directory}</span>
</text>
</box>
)
@@ -2008,7 +2008,7 @@ function SessionSwitchMessageV2(props: { message: SessionMessageInfo }) {
}
return (
<box paddingLeft={3}>
<text fg={theme.text.subdued}>{text()}</text>
<text fg={theme.text.muted}>{text()}</text>
</box>
)
}
@@ -2039,16 +2039,16 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
const heading = () => `${state() === "completed" ? "↳" : "!"} ${actor()} ${status()}`
const suffix = () => Locale.truncateWidth(` · ${description()}`, Math.max(0, ctx.width - 3 - stringWidth(heading())))
const color = () => {
if (state() === "error") return theme.text.feedback.error.default
if (state() === "cancelled") return theme.text.feedback.warning.default
if (hover() && childID()) return theme.text.default
return theme.text.feedback.info.default
if (state() === "error") return theme.text.feedback.error.base
if (state() === "cancelled") return theme.text.feedback.warning.base
if (hover() && childID()) return theme.text.base
return theme.text.feedback.info.base
}
return (
<Show
when={completion()}
fallback={
<InlineToolRow icon="◈" color={theme.text.subdued} pending="Notice" complete={true}>
<InlineToolRow icon="◈" color={theme.text.muted} pending="Notice" complete={true}>
{text()}
</InlineToolRow>
}
@@ -2065,7 +2065,7 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
>
<text wrapMode="none">
<span style={{ fg: color() }}>{heading()}</span>
<span style={{ fg: theme.text.subdued }}>{suffix()}</span>
<span style={{ fg: theme.text.muted }}>{suffix()}</span>
</text>
</box>
</Show>
@@ -2075,7 +2075,7 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
function SessionSkillMessage(props: { message: Extract<SessionMessageInfo, { type: "skill" }> }) {
const theme = useTheme()
return (
<InlineToolRow icon="→" color={theme.text.subdued} pending="Skill" complete={true}>
<InlineToolRow icon="→" color={theme.text.muted} pending="Skill" complete={true}>
Skill {props.message.name}
</InlineToolRow>
)
@@ -2091,7 +2091,7 @@ function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type:
const text = () =>
props.message.status === "failed" ? (cancelled() ? "" : props.message.error.message) : props.message.summary
const content = createMemo(() => text().trim())
const color = () => (status() === "failed" && !cancelled() ? theme.text.feedback.error.default : theme.text.subdued)
const color = () => (status() === "failed" && !cancelled() ? theme.text.feedback.error.base : theme.text.muted)
// Usage of the compaction request itself; the resulting context size only shows on the next assistant step.
const usage = () => {
if (props.message.status === "running" || !props.message.tokens) return
@@ -2141,7 +2141,7 @@ function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type:
tableOptions={{ style: "grid", cellPaddingX: 1 }}
conceal={ctx.markdownMode() === "rendered"}
fg={theme.markdown.text}
bg={theme.background.default}
bg={theme.background.base}
/>
</box>
</Show>
@@ -2153,12 +2153,12 @@ function CompactionQueued() {
const theme = useTheme()
return (
<box flexDirection="row" alignItems="center">
<box border={["top"]} borderColor={theme.border.default} flexGrow={1} />
<box border={["top"]} borderColor={theme.border.base} flexGrow={1} />
<box flexDirection="row" gap={1} paddingLeft={1} paddingRight={1}>
<text fg={theme.text.subdued}>◇</text>
<text fg={theme.text.subdued}>Compaction queued</text>
<text fg={theme.text.muted}>◇</text>
<text fg={theme.text.muted}>Compaction queued</text>
</box>
<box border={["top"]} borderColor={theme.border.default} flexGrow={1} />
<box border={["top"]} borderColor={theme.border.base} flexGrow={1} />
</box>
)
}
@@ -2212,7 +2212,7 @@ function RevertMessage(props: {
paddingLeft={2}
backgroundColor={hover() ? theme.decrease(theme.background.raised.base) : theme.background.raised.base}
>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
{props.count} message{props.count === 1 ? "" : "s"} reverted
</text>
<Show when={props.files.length > 0}>
@@ -2220,7 +2220,7 @@ function RevertMessage(props: {
<For each={props.files}>
{(file) => (
<box flexDirection="row" gap={1} flexShrink={0}>
<text fg={theme.text.subdued}>{statusLabel(file.status)}</text>
<text fg={theme.text.muted}>{statusLabel(file.status)}</text>
<FilePath
value={file.file}
maxWidth={Math.max(
@@ -2230,7 +2230,7 @@ function RevertMessage(props: {
(file.additions > 0 ? stringWidth(`+${file.additions}`) + 1 : 0) -
(file.deletions > 0 ? stringWidth(`-${file.deletions}`) + 1 : 0),
)}
fg={theme.text.default}
fg={theme.text.base}
/>
<Show when={file.additions > 0}>
<text fg={theme.diff.text.added}>+{file.additions}</text>
@@ -2243,8 +2243,8 @@ function RevertMessage(props: {
</For>
</box>
</Show>
<text fg={theme.text.subdued}>
<span style={{ fg: theme.text.default }}>{redoKey()}</span> or /redo to restore
<text fg={theme.text.muted}>
<span style={{ fg: theme.text.base }}>{redoKey()}</span> or /redo to restore
</text>
</box>
</box>
@@ -2299,7 +2299,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
<Show when={props.message.text.trim() || files().length || skills().length}>
<box
border={["left"]}
borderColor={delivery() ? theme.border.default : color()}
borderColor={delivery() ? theme.border.base : color()}
customBorderChars={SplitBorder.customBorderChars}
backgroundColor={theme.background.raised.base}
>
@@ -2342,12 +2342,12 @@ function UserMessage(props: { message: SessionMessageUser }) {
backgroundColor={hover() ? theme.decrease(theme.background.raised.base) : theme.background.raised.base}
flexShrink={0}
>
<text fg={theme.text.default}>{props.message.text}</text>
<text fg={theme.text.base}>{props.message.text}</text>
<Show when={skills().length}>
<box flexDirection="row" paddingTop={1} gap={1} flexWrap="wrap">
<For each={skills()}>
{(skill) => (
<text fg={theme.text.default}>
<text fg={theme.text.base}>
<span
style={{
bg: theme.hue.accent[mode() === "light" ? 300 : 200],
@@ -2357,7 +2357,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
>
{" skill "}
</span>
<span style={{ bg: theme.decrease(theme.background.raised.base), fg: theme.text.subdued }}>
<span style={{ bg: theme.decrease(theme.background.raised.base), fg: theme.text.muted }}>
{` ${skill.name} `}
</span>
</text>
@@ -2371,7 +2371,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
{(file) => {
const label = file.mime === "application/x-directory" ? "dir" : "file"
return (
<text fg={theme.text.default}>
<text fg={theme.text.base}>
<span
style={{
bg: theme.hue.accent[mode() === "light" ? 300 : 200],
@@ -2381,7 +2381,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
>
{` ${label} `}
</span>
<span style={{ bg: theme.decrease(theme.background.raised.base), fg: theme.text.subdued }}>
<span style={{ bg: theme.decrease(theme.background.raised.base), fg: theme.text.muted }}>
{" "}
{file.name ?? (file.source.type === "uri" ? file.source.uri : "attachment")}{" "}
</span>
@@ -2405,7 +2405,7 @@ function QueuedPromptDock(props: { prompts: { id: string; text: string }[]; onOp
return (
<box
border={["left"]}
borderColor={theme.border.default}
borderColor={theme.border.base}
customBorderChars={SplitBorder.customBorderChars}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
@@ -2420,8 +2420,8 @@ function QueuedPromptDock(props: { prompts: { id: string; text: string }[]; onOp
backgroundColor={hover() ? theme.decrease(theme.background.raised.base) : theme.background.raised.base}
flexDirection="row"
>
<text fg={theme.text.subdued} wrapMode="none" truncate flexGrow={1} flexShrink={1} minWidth={0}>
<span style={{ fg: theme.text.default }}>{props.prompts.length} queued</span>
<text fg={theme.text.muted} wrapMode="none" truncate flexGrow={1} flexShrink={1} minWidth={0}>
<span style={{ fg: theme.text.base }}>{props.prompts.length} queued</span>
<Show when={next()}>{(text) => <> · {text()}</>}</Show>
</text>
</box>
@@ -2446,7 +2446,7 @@ function AssistantRetry(props: { retry: SessionMessageAssistant["retry"] }) {
<Show when={props.retry}>
{(retry) => (
<box paddingLeft={3}>
<text fg={theme.text.feedback.warning.default}>
<text fg={theme.text.feedback.warning.base}>
⚠ {seconds() > 0 ? `Retrying in ${seconds()}s` : "Retry due"} · attempt {retry().attempt} ·{" "}
{retry().error.message}
</text>
@@ -2642,10 +2642,10 @@ function GenericTool(props: ToolProps) {
<For each={input()}>
{([key, value]) => (
<box flexDirection="row">
<text flexShrink={0} fg={theme.text.subdued}>
<text flexShrink={0} fg={theme.text.muted}>
{key}:{" "}
</text>
<text flexGrow={1} wrapMode="word" fg={theme.text.default}>
<text flexGrow={1} wrapMode="word" fg={theme.text.base}>
{typeof value === "string" ? value : JSON.stringify(value, null, 2)}
</text>
</box>
@@ -2654,10 +2654,10 @@ function GenericTool(props: ToolProps) {
<Show when={output()}>
{(value) => (
<box flexDirection="row">
<text flexShrink={0} fg={theme.text.subdued}>
<text flexShrink={0} fg={theme.text.muted}>
output:{" "}
</text>
<text flexGrow={1} fg={theme.text.default} wrapMode="word">
<text flexGrow={1} fg={theme.text.base} wrapMode="word">
{value()}
</text>
</box>
@@ -2721,10 +2721,10 @@ function InlineTool(props: {
const clickable = createMemo(() => Boolean(props.onClick || failed()))
const fg = createMemo(() => {
if (props.color) return props.color
if (permission()) return theme.text.feedback.warning.default
if (failed()) return theme.text.feedback.error.default
if (hover() && props.onClick) return theme.text.default
return theme.text.subdued
if (permission()) return theme.text.feedback.warning.base
if (failed()) return theme.text.feedback.error.base
if (hover() && props.onClick) return theme.text.base
return theme.text.muted
})
return (
@@ -2732,7 +2732,7 @@ function InlineTool(props: {
icon={props.icon}
iconColor={props.iconColor}
color={fg()}
errorColor={theme.text.feedback.error.default}
errorColor={theme.text.feedback.error.base}
failed={failed()}
denied={Boolean(denied())}
error={error()}
@@ -2760,9 +2760,9 @@ function InlineTool(props: {
function StatusBadge(props: { children: string; raised?: boolean }) {
const theme = useTheme()
const background = () => (props.raised ? theme.background.raised.base : theme.background.default)
const background = () => (props.raised ? theme.background.raised.base : theme.background.base)
return (
<text flexShrink={0} bg={theme.decrease(background())} fg={theme.text.subdued}>
<text flexShrink={0} bg={theme.decrease(background())} fg={theme.text.muted}>
{" "}
{props.children}{" "}
</text>
@@ -2801,7 +2801,7 @@ function BlockTool(props: BlockToolProps) {
gap={1}
backgroundColor={hover() ? theme.decrease(background()) : background()}
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.background.default}
borderColor={theme.background.base}
onMouseOver={() => props.onClick && setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => {
@@ -2818,13 +2818,13 @@ function BlockTool(props: BlockToolProps) {
when={props.spinner}
fallback={
<text
fg={permission() ? theme.text.feedback.warning.default : (props.headerColor ?? theme.text.subdued)}
fg={permission() ? theme.text.feedback.warning.base : (props.headerColor ?? theme.text.muted)}
>
{title()}
</text>
}
>
<Spinner color={permission() ? theme.text.feedback.warning.default : theme.text.subdued}>
<Spinner color={permission() ? theme.text.feedback.warning.base : theme.text.muted}>
{title().replace(/^# /, "")}
</Spinner>
</Show>
@@ -2839,27 +2839,27 @@ function BlockTool(props: BlockToolProps) {
fallback={
<text
flexShrink={0}
fg={permission() ? theme.text.feedback.warning.default : (props.headerColor ?? theme.text.subdued)}
fg={permission() ? theme.text.feedback.warning.base : (props.headerColor ?? theme.text.muted)}
>
{path().label}
</text>
}
>
<Spinner color={permission() ? theme.text.feedback.warning.default : theme.text.subdued}>
<Spinner color={permission() ? theme.text.feedback.warning.base : theme.text.muted}>
{path().label.replace(/^# /, "")}
</Spinner>
</Show>
<FilePath
value={path().value}
maxWidth={Math.max(2, ctx.width - 4 - stringWidth(path().label) - (props.spinner ? 2 : 0))}
fg={permission() ? theme.text.feedback.warning.default : (props.headerColor ?? theme.text.subdued)}
fg={permission() ? theme.text.feedback.warning.base : (props.headerColor ?? theme.text.muted)}
/>
</box>
)}
</Show>
{props.children}
<Show when={error()}>
<text fg={props.errorColor ?? theme.text.feedback.error.default}>{error()}</text>
<text fg={props.errorColor ?? theme.text.feedback.error.base}>{error()}</text>
</Show>
</box>
)
@@ -2899,7 +2899,7 @@ function ShellDisplay(props: {
// A Session can move while its shell is still running in the original Location.
const location = data.shell.get(props.shellID ?? "")?.location ?? data.session.get(ctx.sessionID)?.location
const permission = useToolPermission(() => props.part)
const color = createMemo(() => (permission() ? theme.text.feedback.warning.default : theme.text.default))
const color = createMemo(() => (permission() ? theme.text.feedback.warning.base : theme.text.base))
const backgroundRunning = createMemo(() => {
const id = props.shellID
return Boolean(id && data.shell.get(id))
@@ -3000,7 +3000,7 @@ function ShellDisplay(props: {
isRunning() || props.status === "streaming" ? (
<Spinner color={color()}>Writing command…</Spinner>
) : (
<text fg={theme.text.subdued}>Writing command…</text>
<text fg={theme.text.muted}>Writing command…</text>
)
}
>
@@ -3008,7 +3008,7 @@ function ShellDisplay(props: {
when={isRunning()}
fallback={
<text
fg={theme.text.default}
fg={theme.text.base}
wrapMode={expanded() ? "word" : "char"}
maxHeight={expanded() ? undefined : 2}
>
@@ -3030,7 +3030,7 @@ function ShellDisplay(props: {
</box>
</Show>
<Show when={limitedOutput()}>
<text fg={theme.text.subdued}>{limitedOutput()}</text>
<text fg={theme.text.muted}>{limitedOutput()}</text>
</Show>
</Show>
<Show when={props.background}>
@@ -3056,10 +3056,10 @@ function Write(props: ToolProps) {
path={{ label: "# Wrote", value: pathFormatter.format(stringValue(props.input.path)) }}
part={props.part}
>
<line_number fg={theme.text.subdued} minWidth={3} paddingRight={1}>
<line_number fg={theme.text.muted} minWidth={3} paddingRight={1}>
<code
conceal={false}
fg={theme.text.default}
fg={theme.text.base}
filetype={filetype(stringValue(props.input.path))}
syntaxStyle={syntax()}
content={code()}
@@ -3114,7 +3114,7 @@ function Read(props: ToolProps) {
<For each={loaded()}>
{(filepath) => (
<box paddingLeft={3}>
<text paddingLeft={3} fg={theme.text.subdued}>
<text paddingLeft={3} fg={theme.text.muted}>
↳ Loaded {pathFormatter.format(filepath)}
</text>
</box>
@@ -3234,11 +3234,11 @@ function ExecuteCallView(props: { call: Accessor<ExecuteCall> }) {
const [hover, setHover] = createSignal(false)
const input = createMemo(() => Object.entries(props.call().input ?? {}))
const expandable = createMemo(() => input().length > 0)
const expandedColor = createMemo(() => theme.decrease(theme.text.subdued))
const expandedColor = createMemo(() => theme.decrease(theme.text.muted))
const color = createMemo(() => {
if (props.call().status === "error") return theme.text.feedback.error.default
if (hover()) return theme.text.default
return expanded() ? expandedColor() : theme.text.subdued
if (props.call().status === "error") return theme.text.feedback.error.base
if (hover()) return theme.text.base
return expanded() ? expandedColor() : theme.text.muted
})
return (
@@ -3264,10 +3264,10 @@ function ExecuteCallView(props: { call: Accessor<ExecuteCall> }) {
<For each={input()}>
{([key, value]) => (
<box flexDirection="row">
<text flexShrink={0} fg={theme.text.subdued}>
<text flexShrink={0} fg={theme.text.muted}>
{key}:{" "}
</text>
<text flexGrow={1} wrapMode="word" fg={theme.text.default}>
<text flexGrow={1} wrapMode="word" fg={theme.text.base}>
{typeof value === "string" ? value : JSON.stringify(value, null, 2)}
</text>
</box>
@@ -3295,7 +3295,7 @@ function Execute(props: ToolProps) {
<>
<InlineTool
icon={hasRuntimeError() ? "✗" : props.part.state.status === "completed" ? "✓" : "│"}
color={hasRuntimeError() ? theme.text.feedback.error.default : undefined}
color={hasRuntimeError() ? theme.text.feedback.error.base : undefined}
spinner={isLoading()}
pending="execute"
complete={true}
@@ -3309,7 +3309,7 @@ function Execute(props: ToolProps) {
<box paddingLeft={3}>
<For each={outputPreview().split("\n")}>
{(line, index) => (
<text paddingLeft={3} fg={theme.text.feedback.error.default}>
<text paddingLeft={3} fg={theme.text.feedback.error.base}>
{index() === 0 ? "↳ " : " "}
{line}
</text>
@@ -3353,7 +3353,7 @@ function Edit(props: ToolProps) {
showLineNumbers={true}
width="100%"
wrapMode={ctx.diffWrapMode()}
fg={theme.text.default}
fg={theme.text.base}
addedBg={theme.diff.background.added}
removedBg={theme.diff.background.removed}
contextBg={theme.diff.background.context}
@@ -3443,7 +3443,7 @@ function ApplyPatch(props: ToolProps) {
showLineNumbers={true}
width="100%"
wrapMode={ctx.diffWrapMode()}
fg={theme.text.default}
fg={theme.text.base}
addedBg={theme.diff.background.added}
removedBg={theme.diff.background.removed}
contextBg={theme.diff.background.context}
@@ -3475,7 +3475,7 @@ function ApplyPatch(props: ToolProps) {
<FilePath
value={file.resource}
maxWidth={Math.max(2, ctx.width - 3)}
fg={file.type === "delete" ? theme.diff.text.removed : theme.text.subdued}
fg={file.type === "delete" ? theme.diff.text.removed : theme.text.muted}
/>
</BlockTool>
)}
@@ -3497,8 +3497,8 @@ function ApplyPatch(props: ToolProps) {
}
part={props.part}
spinner={props.part.state.status === "streaming" || props.part.state.status === "running"}
headerColor={props.part.state.status === "error" ? theme.text.feedback.error.default : undefined}
errorColor={props.part.state.status === "error" ? theme.text.subdued : undefined}
headerColor={props.part.state.status === "error" ? theme.text.feedback.error.base : undefined}
errorColor={props.part.state.status === "error" ? theme.text.muted : undefined}
/>
</Match>
</Switch>
@@ -3524,8 +3524,8 @@ function Question(props: ToolProps) {
<For each={questions()}>
{(q, i) => (
<box flexDirection="column">
<text fg={theme.text.subdued}>{q.question}</text>
<text fg={theme.text.default}>{format(answers()?.[i()])}</text>
<text fg={theme.text.muted}>{q.question}</text>
<text fg={theme.text.base}>{format(answers()?.[i()])}</text>
</box>
)}
</For>
@@ -3566,7 +3566,7 @@ function Diagnostics(props: { diagnostics: unknown; filePath: string }) {
<box>
<For each={errors()}>
{(diagnostic) => (
<text fg={theme.text.feedback.error.default}>
<text fg={theme.text.feedback.error.base}>
Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}] {diagnostic.message}
</text>
)}
@@ -25,8 +25,8 @@ export function SessionLocationUnavailable(props: { directory: string; onMove: (
title="Session location unavailable"
body={
<box paddingLeft={1} gap={1}>
<text fg={theme.text.subdued}>{directory()}</text>
<text fg={theme.text.default}>Choose another directory to continue this session.</text>
<text fg={theme.text.muted}>{directory()}</text>
<text fg={theme.text.base}>Choose another directory to continue this session.</text>
</box>
}
options={{ move: "Choose directory" }}
@@ -24,7 +24,7 @@ export function ReasoningPart(props: {
}) {
const theme = useTheme()
const { currentSyntax: syntax } = useThemes()
const thinkingSyntax = createSyntaxStyleMemo(() => generateThinkingSyntax(syntax(), theme.text.subdued))
const thinkingSyntax = createSyntaxStyleMemo(() => generateThinkingSyntax(syntax(), theme.text.muted))
const ctx = use()
// Collapsed by default in hide mode: a single line throughout, so the
// layout never shifts. Click to open the full markdown block, click to close.
@@ -52,7 +52,7 @@ export function ReasoningPart(props: {
<box
border={!inMinimal() || expanded() ? ["left"] : undefined}
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.decrease(theme.background.default)}
borderColor={theme.decrease(theme.background.base)}
paddingLeft={!inMinimal() || expanded() ? 1 : 0}
>
<box onMouseUp={toggle}>
@@ -70,7 +70,7 @@ export function ReasoningPart(props: {
<box
border={["left"]}
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.decrease(theme.background.default)}
borderColor={theme.decrease(theme.background.base)}
paddingLeft={inMinimal() ? 3 : 1}
>
<code
@@ -80,7 +80,7 @@ export function ReasoningPart(props: {
syntaxStyle={thinkingSyntax()}
content={content()}
conceal={ctx.markdownMode() === "rendered"}
fg={theme.text.subdued}
fg={theme.text.muted}
/>
</box>
</box>
@@ -106,12 +106,12 @@ function ReasoningHeader(props: {
const fg = () =>
props.open
? RGBA.fromValues(
theme.text.feedback.warning.default.r,
theme.text.feedback.warning.default.g,
theme.text.feedback.warning.default.b,
theme.text.feedback.warning.base.r,
theme.text.feedback.warning.base.g,
theme.text.feedback.warning.base.b,
0.6,
)
: theme.text.feedback.warning.default
: theme.text.feedback.warning.base
return (
<Switch>
@@ -166,7 +166,7 @@ export function TextPart(props: {
tableOptions={{ style: "grid", cellPaddingX: 1 }}
conceal={ctx.markdownMode() === "rendered"}
fg={theme.markdown.text}
bg={theme.background.default}
bg={theme.background.base}
/>
</box>
</Show>
+37 -37
View File
@@ -47,8 +47,8 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
scrollAcceleration={scrollAcceleration()}
verticalScrollbarOptions={{
trackOptions: {
backgroundColor: theme.background.default,
foregroundColor: theme.scrollbar.default,
backgroundColor: theme.background.base,
foregroundColor: theme.scrollbar.base,
},
}}
>
@@ -61,7 +61,7 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
showLineNumbers={true}
width="100%"
wrapMode="word"
fg={theme.text.default}
fg={theme.text.base}
addedBg={theme.diff.background.added}
removedBg={theme.diff.background.removed}
contextBg={theme.diff.background.context}
@@ -79,7 +79,7 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
when={props.patch}
fallback={
<box paddingLeft={1}>
<text fg={theme.text.subdued}>No diff provided</text>
<text fg={theme.text.muted}>No diff provided</text>
</box>
}
>
@@ -89,8 +89,8 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
scrollAcceleration={scrollAcceleration()}
verticalScrollbarOptions={{
trackOptions: {
backgroundColor: theme.background.default,
foregroundColor: theme.scrollbar.default,
backgroundColor: theme.background.base,
foregroundColor: theme.scrollbar.base,
},
}}
>
@@ -100,7 +100,7 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
streaming={true}
syntaxStyle={syntax()}
content={patch()}
fg={theme.text.subdued}
fg={theme.text.muted}
/>
</scrollbox>
)}
@@ -171,9 +171,9 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
) : props.request.action === "external_directory" ? (
<Show when={current.lines.length > 0}>
<box paddingLeft={1} gap={1}>
<text fg={theme.text.subdued}>Patterns</text>
<text fg={theme.text.muted}>Patterns</text>
<box>
<For each={current.lines}>{(line) => <text fg={theme.text.default}>{line}</text>}</For>
<For each={current.lines}>{(line) => <text fg={theme.text.base}>{line}</text>}</For>
</box>
</box>
</Show>
@@ -186,8 +186,8 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
props.request.action === "shell" ||
props.request.action === "subagent" ||
props.request.action === "task"
? theme.text.default
: theme.text.subdued
? theme.text.base
: theme.text.muted
}
>
{line}
@@ -200,15 +200,15 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
const header = () => (
<box flexDirection="column" gap={0}>
<box flexDirection="row" gap={1} flexShrink={0}>
<text fg={theme.text.feedback.warning.default}>{"△"}</text>
<text fg={theme.text.default}>Permission required</text>
<text fg={theme.text.feedback.warning.base}>{"△"}</text>
<text fg={theme.text.base}>Permission required</text>
</box>
<Show when={props.request.action !== "shell" && current.title}>
<box flexDirection="row" gap={1} paddingLeft={2} flexShrink={0}>
<text fg={theme.text.subdued} flexShrink={0}>
<text fg={theme.text.muted} flexShrink={0}>
{current.icon}
</text>
<text fg={theme.text.default}>{current.title}</text>
<text fg={theme.text.base}>{current.title}</text>
</box>
</Show>
</box>
@@ -224,7 +224,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
<Show when={option === "always"} fallback={presentationBody()}>
<box paddingLeft={1} gap={1}>
<For each={permissionAlwaysLines(props.request)}>
{(line, index) => <text fg={index() === 0 ? theme.text.subdued : theme.text.default}>{line}</text>}
{(line, index) => <text fg={index() === 0 ? theme.text.muted : theme.text.base}>{line}</text>}
</For>
</box>
</Show>
@@ -316,16 +316,16 @@ function RejectPrompt(props: {
}))}
backgroundColor={theme.background.raised.base}
border={["left"]}
borderColor={theme.text.feedback.error.default}
borderColor={theme.text.feedback.error.base}
customBorderChars={SplitBorder.customBorderChars}
>
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
<box flexDirection="row" gap={1} paddingLeft={1}>
<text fg={theme.text.feedback.error.default}>{"△"}</text>
<text fg={theme.text.default}>Reject permission</text>
<text fg={theme.text.feedback.error.base}>{"△"}</text>
<text fg={theme.text.base}>Reject permission</text>
</box>
<box paddingLeft={1}>
<text fg={theme.text.subdued}>Tell OpenCode what to do differently</text>
<text fg={theme.text.muted}>Tell OpenCode what to do differently</text>
</box>
</box>
<box
@@ -354,9 +354,9 @@ function RejectPrompt(props: {
val.traits = { status: "REJECT" }
}}
focused={enabled()}
textColor={theme.text.default}
focusedTextColor={theme.text.default}
cursorColor={theme.text.default}
textColor={theme.text.base}
focusedTextColor={theme.text.base}
cursorColor={theme.text.base}
cursorStyle={config.cursor}
/>
<box
@@ -380,8 +380,8 @@ function RejectPrompt(props: {
}))}
onMouseUp={() => props.onConfirm(input.plainText)}
>
<text fg={theme.text.default}>
enter <span style={{ fg: theme.text.subdued }}>confirm</span>
<text fg={theme.text.base}>
enter <span style={{ fg: theme.text.muted }}>confirm</span>
</text>
</box>
<box
@@ -394,8 +394,8 @@ function RejectPrompt(props: {
}))}
onMouseUp={props.onCancel}
>
<text fg={theme.text.default}>
esc <span style={{ fg: theme.text.subdued }}>cancel</span>
<text fg={theme.text.base}>
esc <span style={{ fg: theme.text.muted }}>cancel</span>
</text>
</box>
</box>
@@ -527,8 +527,8 @@ export function SessionQuestion<const T extends Record<string, string>>(props: {
when={props.header}
fallback={
<box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}>
<text fg={theme.text.feedback.warning.default}>{"△"}</text>
<text fg={theme.text.default}>{props.title}</text>
<text fg={theme.text.feedback.warning.base}>{"△"}</text>
<text fg={theme.text.base}>{props.title}</text>
</box>
}
>
@@ -578,7 +578,7 @@ export function SessionQuestion<const T extends Record<string, string>>(props: {
backgroundColor={
option === store.selected
? theme.background.action.primary.focused
: theme.background.action.primary.default
: theme.background.action.primary.base
}
onMouseMove={() => setStore("selected", option)}
onMouseUp={() => {
@@ -587,7 +587,7 @@ export function SessionQuestion<const T extends Record<string, string>>(props: {
}}
>
<text
fg={option === store.selected ? theme.text.action.primary.focused : theme.text.action.primary.default}
fg={option === store.selected ? theme.text.action.primary.focused : theme.text.action.primary.base}
>
{props.options[option]}
</text>
@@ -597,17 +597,17 @@ export function SessionQuestion<const T extends Record<string, string>>(props: {
</box>
<box flexDirection="row" gap={2} flexShrink={0}>
<Show when={props.fullscreen}>
<text fg={theme.text.default}>
{shortcuts.get("permission.prompt.fullscreen")} <span style={{ fg: theme.text.subdued }}>{hint()}</span>
<text fg={theme.text.base}>
{shortcuts.get("permission.prompt.fullscreen")} <span style={{ fg: theme.text.muted }}>{hint()}</span>
</text>
</Show>
<Show when={keys.length > 1}>
<text fg={theme.text.default}>
{"⇆"} <span style={{ fg: theme.text.subdued }}>select</span>
<text fg={theme.text.base}>
{"⇆"} <span style={{ fg: theme.text.muted }}>select</span>
</text>
</Show>
<text fg={theme.text.default}>
enter <span style={{ fg: theme.text.subdued }}>confirm</span>
<text fg={theme.text.base}>
enter <span style={{ fg: theme.text.muted }}>confirm</span>
</text>
</box>
</box>
+2 -2
View File
@@ -31,7 +31,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
>
<box flexShrink={0} paddingRight={2} paddingBottom={1}>
<title_shimmer
fg={theme.text.default}
fg={theme.text.base}
rename={{
pending: data.session.title.pending(props.sessionID),
title: withTimestampedFallback(session()),
@@ -62,7 +62,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
height: "100%",
trackOptions: {
backgroundColor: theme.background.raised.base,
foregroundColor: theme.scrollbar.default,
foregroundColor: theme.scrollbar.base,
},
}}
>
+3 -3
View File
@@ -30,15 +30,15 @@ export function DialogAlert(props: DialogAlertProps) {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
{props.title}
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingBottom={1}>
<text fg={theme.text.subdued}>{props.message}</text>
<text fg={theme.text.muted}>{props.message}</text>
</box>
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
<box
+4 -4
View File
@@ -58,15 +58,15 @@ export function DialogConfirm(props: DialogConfirmProps) {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
{props.title}
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingBottom={1}>
<text fg={theme.text.subdued}>{props.message}</text>
<text fg={theme.text.muted}>{props.message}</text>
</box>
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
<For each={["cancel", "confirm"] as const}>
@@ -81,7 +81,7 @@ export function DialogConfirm(props: DialogConfirmProps) {
dialog.clear()
}}
>
<text fg={key === store.active ? theme.text.action.primary.focused : theme.text.subdued}>
<text fg={key === store.active ? theme.text.action.primary.focused : theme.text.muted}>
{Locale.titlecase(props.label?.[key] ?? key)}
</text>
</box>
+17 -17
View File
@@ -85,15 +85,15 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
Export session
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box flexDirection="row" gap={1}>
<text fg={theme.text.default}>Export as:</text>
<text fg={theme.text.base}>Export as:</text>
<box flexDirection="row" gap={1}>
<For each={["markdown", "json"] as const}>
{(format) => (
@@ -105,7 +105,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.background.formfield.focused
: store.format === format
? theme.background.formfield.selected
: theme.background.formfield.default
: theme.background.formfield.base
}
onMouseUp={() => selectFormat(format)}
>
@@ -115,7 +115,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.text.formfield.focused
: store.format === format
? theme.text.formfield.selected
: theme.text.formfield.default
: theme.text.formfield.base
}
>
{store.format === format ? "◉" : "○"} {format === "markdown" ? "Markdown" : "JSON"}
@@ -134,7 +134,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.background.formfield.focused
: store.thinking
? theme.background.formfield.selected
: theme.background.formfield.default
: theme.background.formfield.base
}
onMouseUp={() => {
setStore("active", "thinking")
@@ -147,7 +147,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.text.formfield.focused
: store.thinking
? theme.text.formfield.selected
: theme.text.formfield.default
: theme.text.formfield.base
}
>
{store.thinking ? "[x]" : "[ ]"}
@@ -158,7 +158,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.text.formfield.focused
: store.thinking
? theme.text.formfield.selected
: theme.text.formfield.default
: theme.text.formfield.base
}
>
Include thinking
@@ -172,7 +172,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.background.formfield.focused
: store.tools
? theme.background.formfield.selected
: theme.background.formfield.default
: theme.background.formfield.base
}
onMouseUp={() => {
setStore("active", "tools")
@@ -185,7 +185,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.text.formfield.focused
: store.tools
? theme.text.formfield.selected
: theme.text.formfield.default
: theme.text.formfield.base
}
>
{store.tools ? "[x]" : "[ ]"}
@@ -196,7 +196,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.text.formfield.focused
: store.tools
? theme.text.formfield.selected
: theme.text.formfield.default
: theme.text.formfield.base
}
>
Include tools
@@ -212,7 +212,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.background.formfield.focused
: store.sanitize
? theme.background.formfield.selected
: theme.background.formfield.default
: theme.background.formfield.base
}
onMouseUp={() => {
setStore("active", "sanitize")
@@ -225,7 +225,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.text.formfield.focused
: store.sanitize
? theme.text.formfield.selected
: theme.text.formfield.default
: theme.text.formfield.base
}
>
{store.sanitize ? "[x]" : "[ ]"}
@@ -236,7 +236,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
? theme.text.formfield.focused
: store.sanitize
? theme.text.formfield.selected
: theme.text.formfield.default
: theme.text.formfield.base
}
>
Sanitize sensitive data
@@ -250,7 +250,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
backgroundColor={overlayTheme.background.raised.high}
onMouseUp={() => confirm("copy")}
>
<text fg={overlayTheme.text.default}>Copy</text>
<text fg={overlayTheme.text.base}>Copy</text>
</box>
<box
paddingLeft={4}
@@ -258,11 +258,11 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
backgroundColor={
store.active === "export"
? theme.background.action.primary.focused
: theme.background.action.primary.default
: theme.background.action.primary.base
}
onMouseUp={() => confirm("export")}
>
<text fg={store.active === "export" ? theme.text.action.primary.focused : theme.text.action.primary.default}>
<text fg={store.active === "export" ? theme.text.action.primary.focused : theme.text.action.primary.base}>
Export
</text>
</box>
+3 -3
View File
@@ -27,15 +27,15 @@ export function DialogExportResult(props: { path: string; onClose?: () => void }
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
Session exported
</text>
<text fg={theme.text.subdued} onMouseUp={close}>
<text fg={theme.text.muted} onMouseUp={close}>
esc
</text>
</box>
<box>
<text fg={theme.text.default}>{props.path}</text>
<text fg={theme.text.base}>{props.path}</text>
</box>
<box flexDirection="row" justifyContent="flex-end" gap={1} paddingBottom={1}>
<box
+3 -3
View File
@@ -19,15 +19,15 @@ export function DialogHelp() {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
Help
</text>
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
<text fg={theme.text.muted} onMouseUp={() => dialog.clear()}>
esc/enter
</text>
</box>
<box paddingBottom={1}>
<text fg={theme.text.subdued}>
<text fg={theme.text.muted}>
Press {shortcuts.get("command.palette.show")} to see all available actions and commands in any context.
</text>
</box>
+10 -10
View File
@@ -98,11 +98,11 @@ export function DialogPrompt(props: DialogPromptProps) {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
{props.title}
</text>
<text
fg={theme.text.subdued}
fg={theme.text.muted}
onMouseUp={() => {
if (!props.busy) (props.onCancel ?? dialog.clear)()
}}
@@ -121,21 +121,21 @@ export function DialogPrompt(props: DialogPromptProps) {
}}
initialValue={props.value}
placeholder={props.placeholder ?? "Enter text"}
placeholderColor={theme.text.subdued}
textColor={props.busy ? theme.text.formfield.disabled : theme.text.formfield.default}
focusedTextColor={props.busy ? theme.text.formfield.disabled : theme.text.formfield.default}
cursorColor={props.busy ? theme.background.formfield.disabled : theme.text.default}
placeholderColor={theme.text.muted}
textColor={props.busy ? theme.text.formfield.disabled : theme.text.formfield.base}
focusedTextColor={props.busy ? theme.text.formfield.disabled : theme.text.formfield.base}
cursorColor={props.busy ? theme.background.formfield.disabled : theme.text.base}
cursorStyle={config.cursor}
/>
<Show when={props.busy}>
<Spinner color={theme.text.subdued}>{props.busyText ?? "Working…"}</Spinner>
<Spinner color={theme.text.muted}>{props.busyText ?? "Working…"}</Spinner>
</Show>
</box>
<box paddingBottom={1} gap={1} flexDirection="row">
<Show when={!props.busy} fallback={<text fg={theme.text.subdued}>processing…</text>}>
<Show when={!props.busy} fallback={<text fg={theme.text.muted}>processing…</text>}>
<Show when={shortcuts.get("dialog.prompt.submit")}>
<text fg={theme.text.default}>
{shortcuts.get("dialog.prompt.submit")} <span style={{ fg: theme.text.subdued }}>submit</span>
<text fg={theme.text.base}>
{shortcuts.get("dialog.prompt.submit")} <span style={{ fg: theme.text.muted }}>submit</span>
</text>
</Show>
</Show>
+15 -15
View File
@@ -594,10 +594,10 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
if (!isActionItem(action.item))
return (
<text>
<span style={{ fg: theme.text.default }}>
<span style={{ fg: theme.text.base }}>
<b>{action.item.title}</b>{" "}
</span>
<span style={{ fg: theme.text.subdued }}>{action.item.label}</span>
<span style={{ fg: theme.text.muted }}>{action.item.label}</span>
</text>
)
const item = action.item
@@ -615,7 +615,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
? theme.text.action.primary.disabled
: active()
? theme.text.action.primary.focused
: theme.text.default
: theme.text.base
}
attributes={active() ? TextAttributes.BOLD : undefined}
>
@@ -627,7 +627,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
? theme.text.action.primary.disabled
: active()
? theme.text.action.primary.focused
: theme.text.subdued
: theme.text.muted
}
>
{" " + item.label}
@@ -641,11 +641,11 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
<box paddingLeft={4} paddingRight={4}>
<box flexDirection="row" justifyContent="space-between">
{props.titleView ?? (
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
{props.title}
</text>
)}
<text fg={theme.text.subdued} onMouseUp={() => (props.onCancel ?? dialog.clear)()}>
<text fg={theme.text.muted} onMouseUp={() => (props.onCancel ?? dialog.clear)()}>
esc
</text>
</box>
@@ -673,7 +673,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
}, 1)
}}
placeholder={props.placeholder ?? "Search"}
placeholderColor={theme.text.subdued}
placeholderColor={theme.text.muted}
/>
</box>
</Show>
@@ -687,14 +687,14 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
fallback={
props.emptyView ?? (
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No items available</text>
<text fg={theme.text.muted}>No items available</text>
</box>
)
}
>
{props.noMatchView ?? (
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.text.subdued}>No results found</text>
<text fg={theme.text.muted}>No results found</text>
</box>
)}
</Show>
@@ -791,7 +791,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
{(detail) => (
<box paddingLeft={3} paddingRight={3}>
<text
fg={option.detailsColor ?? theme.text.subdued}
fg={option.detailsColor ?? theme.text.muted}
wrapMode={option.detailsWrap ? "word" : "none"}
>
{option.detailsWrap
@@ -844,9 +844,9 @@ function Option(props: {
const theme = useTheme().surface("dialog")
const text = createMemo(() => {
if (props.active && !props.muted) return props.activeColor ?? theme.text.action.primary.focused
if (props.muted && (props.active || props.current)) return theme.text.subdued
if (props.muted && (props.active || props.current)) return theme.text.muted
if (props.current) return theme.text.formfield.selected
return theme.text.default
return theme.text.base
})
return (
@@ -876,7 +876,7 @@ function Option(props: {
? Locale.truncateLeft(props.title, props.titleWidth ?? 61)
: Locale.truncate(props.title, props.titleWidth ?? 61))}
<Show when={props.description}>
<span style={{ fg: props.active && !props.muted ? text() : theme.text.subdued }}>
<span style={{ fg: props.active && !props.muted ? text() : theme.text.muted }}>
{" " + props.description}
</span>
</Show>
@@ -888,8 +888,8 @@ function Option(props: {
props.active && !props.muted
? text()
: props.muted && (props.active || props.current)
? theme.text.subdued
: (props.footerColor ?? theme.text.subdued)
? theme.text.muted
: (props.footerColor ?? theme.text.muted)
}
>
{props.footer}
+1 -1
View File
@@ -63,7 +63,7 @@ export function Dialog(
}}
width={dialogWidth(props.size ?? "medium")}
maxWidth={dimensions().width - 2}
backgroundColor={theme.background.default}
backgroundColor={theme.background.base}
paddingTop={1}
>
{props.children}
+6 -6
View File
@@ -36,7 +36,7 @@ function ToastSurface(props: {
marginLeft={2}
wrapMode="none"
attributes={hovered() && props.toast.action ? TextAttributes.BOLD : undefined}
fg={hovered() ? theme.text.action.primary.default : theme.text.subdued}
fg={hovered() ? theme.text.action.primary.base : theme.text.muted}
>
{props.toast.action ? `› ${props.toast.action.label}` : "x"}
</text>
@@ -50,7 +50,7 @@ function ToastSurface(props: {
maxWidth={Math.min(60, dimensions().width - 6)}
justifyContent="center"
alignItems="flex-start"
borderColor={theme.text.feedback[props.toast.variant].default}
borderColor={theme.text.feedback[props.toast.variant].base}
border={["left", "right"]}
customBorderChars={SplitBorder.customBorderChars}
onMouseOver={() => hover(true)}
@@ -72,7 +72,7 @@ function ToastSurface(props: {
when={props.toast.title}
fallback={
<box flexDirection="row" width="100%">
<text fg={theme.text.default} wrapMode="word" flexGrow={1}>
<text fg={theme.text.base} wrapMode="word" flexGrow={1}>
{props.toast.message}
</text>
{affordance()}
@@ -80,18 +80,18 @@ function ToastSurface(props: {
}
>
<box flexDirection="row" width="100%" marginBottom={1}>
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
<text attributes={TextAttributes.BOLD} fg={theme.text.base}>
{props.toast.title}
</text>
<box flexGrow={1} />
{affordance()}
</box>
<text fg={theme.text.default} wrapMode="word" width="100%">
<text fg={theme.text.base} wrapMode="word" width="100%">
{props.toast.message}
</text>
</Show>
<Show when={props.pending}>
<text fg={theme.text.subdued} marginTop={1}>
<text fg={theme.text.muted} marginTop={1}>
+{props.pending} more
</text>
</Show>
@@ -28,7 +28,7 @@ test("uses an available mode while retaining the pinned preference", async () =>
darkOnly.theme.text = "#eeeeee"
const native = {
version: 2,
base: { ...DEFAULT_THEME.base, text: { ...DEFAULT_THEME.base.text, default: "#abcdef" } },
base: { ...DEFAULT_THEME.base, text: { ...DEFAULT_THEME.base.text, base: "#abcdef" } },
dark: { hue: DEFAULT_THEME.dark.hue },
} as const
let themes: ReturnType<typeof useThemes> | undefined
@@ -76,7 +76,7 @@ test("uses an available mode while retaining the pinned preference", async () =>
expect(current().set("native")).toBeTrue()
await wait(() => current().selected === "native")
expect(current().modes()).toEqual(["dark"])
expect(current().current.text.default.equals(RGBA.fromHex("#abcdef"))).toBeTrue()
expect(current().current.text.base.equals(RGBA.fromHex("#abcdef"))).toBeTrue()
} finally {
app.renderer.destroy()
}
@@ -84,8 +84,8 @@ test("uses an available mode while retaining the pinned preference", async () =>
test.each([
["schema", { version: 2, light: { categorical: [] } }],
["partial mode", { version: 2, light: { text: { default: "#ffffff" } } }],
["token reference", { version: 2, light: { text: { default: "$missing" } } }],
["partial mode", { version: 2, light: { text: { base: "#ffffff" } } }],
["token reference", { version: 2, light: { text: { base: "$missing" } } }],
] as const)("falls back to OpenCode when configured V2 theme %s is invalid", async (_label, source) => {
let themes: ReturnType<typeof useThemes> | undefined
let failure: ThemeError | undefined
@@ -131,7 +131,7 @@ test("dialog surfaces are absolute and can be inherited through the theme contex
function ContextProbe() {
contextual = useTheme()
return <text>{contextual.text.default.toString()}</text>
return <text>{contextual.text.base.toString()}</text>
}
function Probe() {
@@ -162,9 +162,9 @@ test("dialog surfaces are absolute and can be inherited through the theme contex
const dialog = theme.surface("dialog")
expect(theme.surface("dialog")).toBe(dialog)
expect(dialog.surface("dialog")).toBe(dialog)
expect(dialog.background.default).toBe(themes.currentTokens().background.raised.base)
expect(contextual.background.default).toBe(dialog.background.default)
expect(contextual.text.default).toBe(dialog.text.default)
expect(dialog.background.base).toBe(themes.currentTokens().background.raised.base)
expect(contextual.background.base).toBe(dialog.background.base)
expect(contextual.text.base).toBe(dialog.text.base)
expect(dialog.decrease(dialog.background.raised.base)).toBe(themes.currentTokens().hue.neutral[600])
} finally {
app.renderer.destroy()
@@ -172,7 +172,7 @@ for (const orientation of ["horizontal", "vertical"] as const) {
.lines.flatMap((line) => line.spans)
.find((span) => span.text.trim() === glyph)?.fg
expect(color?.toInts()).toEqual(
(unread === "error" ? theme.text.feedback.error.default : theme.text.status.unread).toInts(),
(unread === "error" ? theme.text.feedback.error.base : theme.text.status.unread).toInts(),
)
await app.mockMouse.click(1, orientation === "vertical" ? 1 : 0)
await app.renderOnce()
@@ -14,8 +14,8 @@ test("prompt footer separates simultaneous subagent, shell, and usage status", a
location: { directory: "/workspace" },
theme: {
text: {
default: color,
subdued,
base: color,
muted: subdued,
},
},
keymap: {
@@ -70,8 +70,8 @@ test("prompt footer can hide details", async () => {
location: { directory: "/workspace" },
theme: {
text: {
default: color,
subdued: color,
base: color,
muted: color,
},
},
keymap: {
@@ -8,7 +8,7 @@ import { SidebarContext } from "../../src/feature-plugins/sidebar/context"
function context(options?: { cost?: number; tokens?: number }) {
const color = RGBA.fromInts(200, 200, 200)
return {
theme: { text: { default: color, subdued: color } },
theme: { text: { base: color, muted: color } },
data: {
session: {
get: () => ({ location: { directory: "/workspace" } }),
+1 -1
View File
@@ -373,7 +373,7 @@ test("explicit theme refresh reloads custom colors without a palette event", asy
path.join(tmp.path, "themes", "mini-refresh.json"),
JSON.stringify({
version: 2,
base: { ...DEFAULT_THEME.base, text: { ...DEFAULT_THEME.base.text, default: color } },
base: { ...DEFAULT_THEME.base, text: { ...DEFAULT_THEME.base.text, base: color } },
dark: { hue: DEFAULT_THEME.dark.hue },
}),
)
+13 -13
View File
@@ -79,26 +79,26 @@ function rgba(color: unknown) {
function expectFooter(actual: RunTheme, theme: ResolvedTheme) {
const expected = {
text: theme.text.default,
muted: theme.text.subdued,
warning: theme.text.feedback.warning.default,
error: theme.text.feedback.error.default,
actionSecondaryText: theme.text.action.secondary.default,
text: theme.text.base,
muted: theme.text.muted,
warning: theme.text.feedback.warning.base,
error: theme.text.feedback.error.base,
actionSecondaryText: theme.text.action.secondary.base,
actionFocusedBg: theme.background.action.primary.focused,
actionFocusedText: theme.text.action.primary.focused,
formfieldText: theme.text.formfield.default,
formfieldText: theme.text.formfield.base,
formfieldFocusedBg: theme.background.formfield.focused,
formfieldFocusedText: theme.text.formfield.focused,
selection: theme.text.formfield.selected,
running: theme.text.status.running,
question: theme.text.status.question,
permission: theme.text.status.permission,
success: theme.text.feedback.success.default,
success: theme.text.feedback.success.base,
link: theme.markdown.link,
shade: theme.background.raised.base,
surface: theme.background.raised.base,
pane: theme.background.raised.high,
border: theme.border.default,
border: theme.border.base,
line: theme.background.raised.high,
}
Object.entries(expected).forEach(([key, color]) => {
@@ -192,20 +192,20 @@ test.each(["light", "dark"] as const)(
...base,
text: {
...base.text,
default: "#123456",
base: "#123456",
action: {
...base.text.action,
primary: { ...base.text.action.primary, $focused: "#56789a" },
},
formfield: {
...base.text.formfield,
default: "#234567",
base: "#234567",
$selected: "#345678",
$focused: "#6789ab",
},
feedback: {
...base.text.feedback,
warning: { ...base.text.feedback.warning, default: "#456789" },
warning: { ...base.text.feedback.warning, base: "#456789" },
},
},
background: {
@@ -239,7 +239,7 @@ test.each(["light", "dark"] as const)(
const expected = resolveThemeDocument(parseTheme(DEFAULT_THEMES.opencode), mode)
for (const source of [
{ version: 2, [mode]: { categorical: [] } },
{ version: 2, [mode]: { text: { default: "$missing" } } },
{ version: 2, [mode]: { text: { base: "$missing" } } },
undefined,
]) {
if (source) await Bun.write(path.join(tmp.path, "themes", "mini-invalid.json"), JSON.stringify(source))
@@ -274,7 +274,7 @@ test.each(["light", "dark"] as const)(
"falls back only for unsupported modes of a %s-only custom theme",
async (mode) => {
const base = selectTheme(DEFAULT_THEME, mode)
const definition = { ...base, text: { ...base.text, default: "#123456" } }
const definition = { ...base, text: { ...base.text, base: "#123456" } }
const { hue, ...tokens } = definition
const source = { version: 2, base: tokens, [mode]: { hue } }
await Bun.write(path.join(tmp.path, "themes", "mini-one-mode.json"), JSON.stringify(source))
+10 -10
View File
@@ -9,7 +9,7 @@ test("provides reactive properties, states, surfaces, and color operations", ()
const theme = createComponentTheme(resolved)
const current = theme.surface("dialog")
expect(theme.text.default).toBe(resolved().text.default)
expect(theme.text.base).toBe(resolved().text.base)
expect(theme.hue.accent[500]).toBe(resolved().hue.accent[500])
expect(theme.hue.interactive[500]).toBe(resolved().hue.interactive[500])
expect(theme.hue.gray[200]).toBe(resolved().hue.gray[200])
@@ -25,8 +25,8 @@ test("provides reactive properties, states, surfaces, and color operations", ()
expect(theme.increase(equivalent, 1)).toBe(equivalent)
const unmatched = RGBA.fromInts(1, 2, 3)
expect(theme.increase(unmatched, 1)).toBe(unmatched)
expect(theme.text.subdued).toBe(resolved().text.subdued)
expect(theme.text.action.primary.default).toBe(resolved().text.action.primary.default)
expect(theme.text.muted).toBe(resolved().text.muted)
expect(theme.text.action.primary.base).toBe(resolved().text.action.primary.base)
expect(theme.text.action.primary.hovered).toBe(resolved().text.action.primary.hovered)
expect(theme.text.action.primary.pressed).toBe(resolved().text.action.primary.pressed)
expect(theme.text.action.primary.selected).toBe(resolved().text.action.primary.selected)
@@ -35,7 +35,7 @@ test("provides reactive properties, states, surfaces, and color operations", ()
expect(theme.background.action.primary.focused).toBe(resolved().background.action.primary.focused)
expect(theme.background.action.primary.pressed).toBe(resolved().background.action.primary.pressed)
expect(theme.background.action.primary.disabled).toBe(resolved().background.action.primary.disabled)
expect(theme.background.action.primary.default).toBe(resolved().background.action.primary.default)
expect(theme.background.action.primary.base).toBe(resolved().background.action.primary.base)
expect(
theme.background.action.primary.state({
disabled: true,
@@ -56,7 +56,7 @@ test("provides reactive properties, states, surfaces, and color operations", ()
)
expect(theme.background.action.primary.state({ hovered: true })).toBe(theme.background.action.primary.hovered)
expect(theme.background.action.primary.state({ disabled: false, hovered: false })).toBe(
theme.background.action.primary.default,
theme.background.action.primary.base,
)
expect(theme.text.formfield.state({ focused: true, selected: true })).toBe(theme.text.formfield.focused)
expect(theme.background.action.destructive.disabled).toBe(resolved().background.action.destructive.disabled)
@@ -66,21 +66,21 @@ test("provides reactive properties, states, surfaces, and color operations", ()
expect(theme.background.formfield.disabled).toBe(resolved().background.formfield.disabled)
expect(theme.background.raised.base).toBe(resolved().background.raised.base)
expect(theme.background.raised.high).toBe(resolved().background.raised.high)
expect(theme.scrollbar.default).toBe(resolved().scrollbar.default)
expect(theme.scrollbar.base).toBe(resolved().scrollbar.base)
expect(theme.diff.text.added).toBe(resolved().diff.text.added)
expect(theme.surface("dialog")).toBe(current)
expect(current.surface("dialog")).toBe(current)
expect(current.categorical.map((scale) => scale[500])).toEqual(resolved().categorical.map((scale) => scale[500]))
expect(current.text.default).toBe(resolved().surface("dialog").text.default)
expect(current.background.default).toBe(resolved().background.raised.base)
expect(current.text.base).toBe(resolved().surface("dialog").text.base)
expect(current.background.base).toBe(resolved().background.raised.base)
expect(current.background.action.primary.focused).toBe(resolved().surface("dialog").background.action.primary.focused)
expect(current.background.action.primary.hovered).toBe(resolved().background.raised.high)
expect(current.background.formfield.selected).toBe(resolved().surface("dialog").background.formfield.selected)
setResolved(resolveTheme(selectTheme(DEFAULT_THEME, "dark")))
expect(current.text.default).toBe(resolved().surface("dialog").text.default)
expect(current.background.default).toBe(resolved().background.raised.base)
expect(current.text.base).toBe(resolved().surface("dialog").text.base)
expect(current.background.base).toBe(resolved().background.raised.base)
expect(current.decrease(current.background.raised.base, 1)).toBe(resolved().hue.neutral[600])
expect(current.decrease(current.background.raised.base)).toBe(resolved().hue.neutral[600])
})
+32 -32
View File
@@ -40,8 +40,8 @@ test("resolves complete one-mode documents in the available mode", () => {
const resolvedLight = resolveSource(complete("light"), "dark")
const resolvedDark = resolveSource(complete("dark"), "light")
expect(resolvedLight.background.default.equals(resolveTheme(light).background.default)).toBeTrue()
expect(resolvedDark.background.default.equals(resolveTheme(dark).background.default)).toBeTrue()
expect(resolvedLight.background.base.equals(resolveTheme(light).background.base)).toBeTrue()
expect(resolvedDark.background.base.equals(resolveTheme(dark).background.base)).toBeTrue()
expect(resolvedLight.categorical.length).toBeGreaterThan(0)
expect(resolvedDark.categorical.length).toBeGreaterThan(0)
})
@@ -97,23 +97,23 @@ test("resolves independent definitions and hue aliases", () => {
expect(lightTheme.surface("dialog").increase(lightTheme.hue.red[100])).toBe(lightTheme.hue.red[200])
expect(lightTheme.decrease(lightTheme.hue.red[200])).toBe(lightTheme.hue.red[100])
expect(darkTheme.decrease(darkTheme.hue.red[200])).toBe(darkTheme.hue.red[100])
expect(lightTheme.text.default).toBeInstanceOf(RGBA)
expect(darkTheme.background.default).toBeInstanceOf(RGBA)
expect(lightTheme.text.base).toBeInstanceOf(RGBA)
expect(darkTheme.background.base).toBeInstanceOf(RGBA)
expect(lightTheme.background.raised.base).toBe(lightTheme.hue.neutral[700])
expect(lightTheme.background.raised.high).toBe(lightTheme.hue.neutral[600])
expect(lightTheme.syntax.keyword).toBeInstanceOf(RGBA)
expect(lightTheme.text.action.primary.default).toBe(lightTheme.hue.neutral[800])
expect(lightTheme.text.action.primary.base).toBe(lightTheme.hue.neutral[800])
// Surfaces re-resolve the palette after applying their theme-provided overrides.
const dialog = lightTheme.surface("dialog")
expect(dialog.background.default).toBe(lightTheme.background.raised.base)
expect(dialog.background.formfield.default).toBe(lightTheme.background.raised.base)
expect(dialog.background.feedback.error.default).toBe(lightTheme.background.raised.base)
expect(dialog.background.base).toBe(lightTheme.background.raised.base)
expect(dialog.background.formfield.base).toBe(lightTheme.background.raised.base)
expect(dialog.background.feedback.error.base).toBe(lightTheme.background.raised.base)
expect(dialog.background.action.primary.hovered).toBe(lightTheme.background.raised.high)
expect(dialog.background.action.primary.default).toBe(lightTheme.hue.interactive[500])
expect(dialog.background.action.primary.base).toBe(lightTheme.hue.interactive[500])
expect(dialog.background.action.primary.focused).toBe(lightTheme.hue.interactive[500])
expect(dialog.text.action.primary.default).toBe(lightTheme.hue.neutral[900])
expect(dialog.text.action.primary.base).toBe(lightTheme.hue.neutral[900])
expect(dialog.surface("dialog")).toBe(dialog)
expect(darkTheme.surface("dialog").background.default).toBe(darkTheme.background.raised.base)
expect(darkTheme.surface("dialog").background.base).toBe(darkTheme.background.raised.base)
})
test("resolves base hue aliases and rejects circular hue aliases", () => {
@@ -172,7 +172,7 @@ test("resolves complete light and dark definitions independently", () => {
const lightDefinition = override(light, {
text: {
...light.text,
action: { ...light.text.action, secondary: { default: "#123456", $hovered: "#234567" } },
action: { ...light.text.action, secondary: { base: "#123456", $hovered: "#234567" } },
},
})
const { hue, ...base } = lightDefinition
@@ -185,10 +185,10 @@ test("resolves complete light and dark definitions independently", () => {
const lightTheme = resolveSource(document, "light")
const darkTheme = resolveSource(document, "dark")
expect(lightTheme.text.action.secondary.default.toInts()).toEqual([18, 52, 86, 255])
expect(lightTheme.text.action.secondary.base.toInts()).toEqual([18, 52, 86, 255])
expect(lightTheme.text.action.secondary.hovered.toInts()).toEqual([35, 69, 103, 255])
expect(darkTheme.text.action.secondary.default).toBe(darkTheme.text.subdued)
expect(darkTheme.text.action.secondary.hovered).toBe(darkTheme.text.default)
expect(darkTheme.text.action.secondary.base).toBe(darkTheme.text.muted)
expect(darkTheme.text.action.secondary.hovered).toBe(darkTheme.text.base)
})
test("expands structural state fallbacks within a complete theme", () => {
@@ -198,7 +198,7 @@ test("expands structural state fallbacks within a complete theme", () => {
...light.background,
action: {
...light.background.action,
primary: { default: "#123456" },
primary: { base: "#123456" },
},
},
})
@@ -212,8 +212,8 @@ test("resolves matched action variants and states", () => {
expect(theme.text.action.primary.pressed).toBeInstanceOf(RGBA)
expect(theme.text.action.primary.hovered).toBeInstanceOf(RGBA)
expect(theme.text.action.primary.selected).toBeInstanceOf(RGBA)
expect(theme.text.action.secondary.default).toBe(theme.text.subdued)
expect(theme.text.action.secondary.hovered).toBe(theme.text.default)
expect(theme.text.action.secondary.base).toBe(theme.text.muted)
expect(theme.text.action.secondary.hovered).toBe(theme.text.base)
expect(theme.background.action.primary.pressed).toBeInstanceOf(RGBA)
expect(theme.background.action.primary.hovered).toBeInstanceOf(RGBA)
expect(theme.background.action.primary.selected).toBeInstanceOf(RGBA)
@@ -234,56 +234,56 @@ test("resolves dialog surfaces from direct colors", () => {
"light",
)
expect(theme.surface("dialog").background.default.toInts()).toEqual([18, 52, 86, 255])
expect(theme.surface("dialog").background.base.toInts()).toEqual([18, 52, 86, 255])
expect(theme.surface("dialog").background.action.primary.hovered.toInts()).toEqual([35, 69, 103, 255])
})
test("resolves transparent colors", () => {
const theme = resolveSource(complete("light", {
background: { ...light.background, formfield: { default: "transparent" } },
background: { ...light.background, formfield: { base: "transparent" } },
}))
expect(theme.background.formfield.default.toInts()).toEqual([0, 0, 0, 0])
expect(theme.background.formfield.base.toInts()).toEqual([0, 0, 0, 0])
})
test("reports theme decoding failures as native errors", () => {
expect(() =>
resolveSource(
complete("light", { text: { ...light.text, default: "opaque" } }) as never,
complete("light", { text: { ...light.text, base: "opaque" } }) as never,
"light",
"custom",
),
).toThrow('Invalid theme: custom "opaque" is an invalid value')
})
test("surface overrides rewire references and reset action states from their default", () => {
test("surface overrides rewire references and reset action states from their base", () => {
const theme = resolveTheme(
override(light, {
text: {
default: "#111111",
action: { primary: { default: "$text.default", $pressed: "#222222" } },
base: "#111111",
action: { primary: { base: "$text.base", $pressed: "#222222" } },
},
"@dialog": {
text: {
default: "#333333",
action: { primary: { default: "#444444", $focused: "#555555" } },
base: "#333333",
action: { primary: { base: "#444444", $focused: "#555555" } },
},
},
}),
)
const raised = theme.surface("dialog")
expect(raised.text.default.toInts()).toEqual([51, 51, 51, 255])
expect(raised.text.base.toInts()).toEqual([51, 51, 51, 255])
expect(raised.text.action.primary.pressed.toInts()).toEqual([68, 68, 68, 255])
expect(raised.text.action.primary.focused.toInts()).toEqual([85, 85, 85, 255])
})
test("rejects missing and circular references", () => {
expect(() => resolveTheme(override(light, { text: { default: "$missing" } }))).toThrow(
expect(() => resolveTheme(override(light, { text: { base: "$missing" } }))).toThrow(
'Theme reference "$missing" was not found',
)
expect(() =>
resolveTheme(
override(light, {
text: { default: "$text.subdued", subdued: "$text.default" },
text: { base: "$text.muted", muted: "$text.base" },
}),
),
).toThrow("Circular theme reference")
@@ -302,10 +302,10 @@ test("validates complete hues, resolved groups, and hue-only syntax", () => {
resolveTheme(
{
...light,
syntax: { ...light.syntax, keyword: "$text.default" },
syntax: { ...light.syntax, keyword: "$text.base" },
} as unknown as ThemeDefinition,
),
).toThrow("$text.default")
).toThrow("$text.base")
})
function override(base: ThemeDefinition, value: unknown) {
+12 -12
View File
@@ -2,45 +2,45 @@ import { DEFAULT_THEME, selectTheme } from "@opencode/theme/tui"
import type { BackgroundDefinition, TextDefinition, ThemeDefinition, ThemeDocument } from "@opencode/theme/tui"
const text = {
default: "$hue.neutral.900",
subdued: "$hue.neutral.600",
base: "$hue.neutral.900",
muted: "$hue.neutral.600",
action: {
primary: {
default: "$hue.neutral.100",
base: "$hue.neutral.100",
$hovered: "$hue.neutral.200",
$pressed: "$hue.neutral.300",
},
destructive: { default: "$hue.red.100", $disabled: "$hue.neutral.500" },
destructive: { base: "$hue.red.100", $disabled: "$hue.neutral.500" },
},
formfield: { default: "$hue.neutral.600", $selected: "$hue.neutral.100" },
formfield: { base: "$hue.neutral.600", $selected: "$hue.neutral.100" },
feedback: {
error: { default: "$hue.red.700", subdued: "$hue.red.600" },
error: { base: "$hue.red.700", muted: "$hue.red.600" },
},
} satisfies TextDefinition
const background = {
default: "$hue.neutral.100",
base: "$hue.neutral.100",
raised: { base: "$hue.neutral.200", high: "$hue.neutral.300", max: "$hue.neutral.400" },
action: {
primary: {
default: "$hue.interactive.600",
base: "$hue.interactive.600",
$hovered: "$hue.interactive.700",
$pressed: "$hue.interactive.800",
$selected: "$hue.interactive.700",
},
destructive: { default: "$hue.red.600" },
destructive: { base: "$hue.red.600" },
},
formfield: {
default: "$hue.neutral.100",
base: "$hue.neutral.100",
$hovered: "$hue.neutral.200",
$selected: "$hue.interactive.600",
},
feedback: { error: { default: "$hue.red.100" } },
feedback: { error: { base: "$hue.red.100" } },
} satisfies BackgroundDefinition
const definition = {
...selectTheme(DEFAULT_THEME, "light"),
"@dialog": { background: { default: "$background.raised.base" } },
"@dialog": { background: { base: "$background.raised.base" } },
} satisfies ThemeDefinition
export const document = {
+20 -20
View File
@@ -19,36 +19,36 @@ test("migrates resolved V1 modes into V2 tokens", () => {
expect(migrated.dark.categorical?.length).toBeGreaterThan(0)
expect(migrated.light.hue?.accent).toMatch(/^\$hue\.[^.]+$/)
expect(migrated.light.hue?.interactive).toMatch(/^\$hue\.[^.]+$/)
expect(migrated.base.text?.default).toBe("$hue.neutral.200")
expect(migrated.base.text?.subdued).toBe("$hue.neutral.400")
expect(migrated.base.background?.action?.primary?.default).toBe("transparent")
expect(migrated.base.background?.default).toBe("$hue.neutral.800")
expect(migrated.base.text?.base).toBe("$hue.neutral.200")
expect(migrated.base.text?.muted).toBe("$hue.neutral.400")
expect(migrated.base.background?.action?.primary?.base).toBe("transparent")
expect(migrated.base.background?.base).toBe("$hue.neutral.800")
expect(migrated.base.background?.raised?.base).toBe("$hue.neutral.700")
expect(migrated.base.background?.raised?.high).toBe("$hue.neutral.600")
expect(migrated.dark.background?.default).toBe("$hue.neutral.800")
expect(migrated.dark.background?.base).toBe("$hue.neutral.800")
expect(migrated.dark.background?.raised?.base).toBe("$hue.neutral.700")
expect(migrated.dark.background?.raised?.high).toBe("$hue.neutral.600")
expect(migrated.base.text?.action?.primary?.default).toBe("$text.default")
expect(migrated.base.text?.action?.secondary?.default).toBe("$text.subdued")
expect(migrated.base.text?.action?.secondary?.$hovered).toBe("$text.default")
expect(migrated.base.text?.action?.primary?.base).toBe("$text.base")
expect(migrated.base.text?.action?.secondary?.base).toBe("$text.muted")
expect(migrated.base.text?.action?.secondary?.$hovered).toBe("$text.base")
expect(migrated.base.background?.action?.primary?.$selected).toBe("transparent")
expect(resolved.background.raised.base.toInts()).toEqual(legacy.backgroundPanel.toInts())
expect(resolved.background.raised.high.toInts()).toEqual(legacy.backgroundElement.toInts())
expect(resolved.background.formfield.selected.toInts()).toEqual(legacy.background.toInts())
expect(resolved.background.formfield.focused.toInts()).toEqual(legacy.background.toInts())
expect(resolved.text.formfield.default.toInts()).toEqual(legacy.text.toInts())
expect(resolved.text.formfield.base.toInts()).toEqual(legacy.text.toInts())
expect(resolved.text.formfield.selected.toInts()).toEqual(legacy.primary.toInts())
expect(resolved.text.formfield.focused.toInts()).toEqual(legacy.primary.toInts())
expect(resolved.hue.accent[200].toInts()).toEqual(legacy.accent.toInts())
expect(resolved.hue.interactive[200].toInts()).toEqual(legacy.primary.toInts())
expect(resolved.background.action.primary.selected.toInts()).toEqual([0, 0, 0, 0])
expect(resolved.text.action.primary.selected.toInts()).toEqual(legacy.primary.toInts())
expect(resolved.text.action.secondary.default.toInts()).toEqual(legacy.textMuted.toInts())
expect(resolved.text.action.secondary.base.toInts()).toEqual(legacy.textMuted.toInts())
expect(resolved.text.action.secondary.hovered.toInts()).toEqual(legacy.text.toInts())
expect(resolved.background.feedback.error.default.toInts()).toEqual(legacy.background.toInts())
expect(resolved.surface("dialog").background.default.toInts()).toEqual(legacy.backgroundPanel.toInts())
expect(resolved.surface("dialog").background.action.primary.default.toInts()).toEqual([0, 0, 0, 0])
expect(resolved.surface("dialog").text.action.primary.default.toInts()).toEqual(legacy.text.toInts())
expect(resolved.background.feedback.error.base.toInts()).toEqual(legacy.background.toInts())
expect(resolved.surface("dialog").background.base.toInts()).toEqual(legacy.backgroundPanel.toInts())
expect(resolved.surface("dialog").background.action.primary.base.toInts()).toEqual([0, 0, 0, 0])
expect(resolved.surface("dialog").text.action.primary.base.toInts()).toEqual(legacy.text.toInts())
})
test("references generated hues from matching token colors", () => {
@@ -61,8 +61,8 @@ test("references generated hues from matching token colors", () => {
const migrated = migrateV1(source)
if (!migrated.light) throw new Error("Expected light mode")
expect(migrated.base.border?.default).toBe("$hue.interactive.200")
expect(migrated.base.scrollbar?.default).toBe("$hue.accent.200")
expect(migrated.base.border?.base).toBe("$hue.interactive.200")
expect(migrated.base.scrollbar?.base).toBe("$hue.accent.200")
expect(migrated.base.syntax?.keyword).toMatch(/^\$hue\.[^.]+\.200$/)
expect(migrated.base.markdown?.emphasis).toBe("#123456")
})
@@ -201,7 +201,7 @@ test("builds and extrapolates gray from V1 surfaces and text without using menus
expect(withBorders.dark?.hue?.gray).toEqual(darkGray)
})
test("uses the default text reference for primary actions on transparent backgrounds", () => {
test("uses the base text reference for primary actions on transparent backgrounds", () => {
const source = structuredClone(DEFAULT_THEMES.opencode)
source.theme.background = "transparent"
source.theme.primary = { light: "#ffffff", dark: "#000000" }
@@ -209,8 +209,8 @@ test("uses the default text reference for primary actions on transparent backgro
const migrated = migrateV1(source)
if (!migrated.light || !migrated.dark) throw new Error("Expected both modes")
expect(migrated.base.text?.action?.primary?.default).toBe("$text.default")
expect(migrated.dark.text?.action?.primary?.default).toBe("$text.default")
expect(migrated.base.text?.action?.primary?.base).toBe("$text.base")
expect(migrated.dark.text?.action?.primary?.base).toBe("$text.base")
})
test("retains V1 circular reference errors", () => {
@@ -225,7 +225,7 @@ test("migrates every built-in V1 theme in its supported modes", () => {
for (const source of Object.values(DEFAULT_THEMES)) {
const migrated = migrateV1(source)
for (const mode of themeModes(migrated)) {
expect(resolveThemeDocument(migrated, mode).text.default).toBeDefined()
expect(resolveThemeDocument(migrated, mode).text.base).toBeDefined()
}
}
})
@@ -214,7 +214,7 @@ console.log(result.ok, result.notification, result.sound, result.skipped)
Use semantic theme tokens with OpenTUI elements and pass `context.renderer` to renderer-specific helpers.
```tsx
const Status = () => <text fg={context.theme.text.default}>Ready</text>
const Status = () => <text fg={context.theme.text.base}>Ready</text>
const renderer = context.renderer
```
@@ -227,7 +227,7 @@ import { usePlugin } from "@opencode/plugin/tui"
function Status() {
const context = usePlugin()
return <text fg={context.theme.text.default}>{context.app.version}</text>
return <text fg={context.theme.text.base}>{context.app.version}</text>
}
```
+15 -15
View File
@@ -70,7 +70,7 @@ Its relevant structure looks like this:
"version": 2,
"base": {
"text": {
"default": "#d8f3ff"
"base": "#d8f3ff"
}
},
"dark": {
@@ -105,7 +105,7 @@ The examples below show only the sections relevant to each concept; they are not
"version": 2,
"base": {
"text": {
"default": "#16324f"
"base": "#16324f"
}
},
"light": {
@@ -113,7 +113,7 @@ The examples below show only the sections relevant to each concept; they are not
},
"dark": {
"hue": {},
"text": { "default": "#d8f3ff" }
"text": { "base": "#d8f3ff" }
}
}
```
@@ -127,11 +127,11 @@ Use a hex color, `transparent`, or a reference to another theme color:
"version": 2,
"base": {
"text": {
"default": "#d8f3ff",
"subdued": "$hue.neutral.400"
"base": "#d8f3ff",
"muted": "$hue.neutral.400"
},
"background": {
"default": "transparent"
"base": "transparent"
}
}
}
@@ -173,7 +173,7 @@ base hue or alias.
## States
Use state keys to change interactive colors. Unspecified states use the nearest default value:
Use state keys to change interactive colors. Unspecified states use the nearest base value:
```json title="themes/ocean.json"
{
@@ -182,7 +182,7 @@ Use state keys to change interactive colors. Unspecified states use the nearest
"text": {
"action": {
"primary": {
"default": "$hue.interactive.400",
"base": "$hue.interactive.400",
"$hovered": "$hue.interactive.300",
"$disabled": "$hue.neutral.600"
}
@@ -209,7 +209,7 @@ palette:
"version": 2,
"base": {
"border": {
"default": "$hue.neutral.700"
"base": "$hue.neutral.700"
},
"syntax": {
"comment": "$hue.neutral.500",
@@ -227,8 +227,8 @@ palette:
| Group | Purpose |
| ------------ | ------------------------------------------------------------- |
| `text` | Default, subdued, action, form-field, status, and feedback text |
| `background` | Default, surfaces, actions, form fields, and feedback fills |
| `text` | Base, muted, action, form-field, status, and feedback text |
| `background` | Base, surfaces, actions, form fields, and feedback fills |
| `border` | Borders |
| `scrollbar` | Scrollbars |
| `diff` | Added, removed, context, highlight, and line-number colors |
@@ -243,14 +243,14 @@ Syntax keys are `comment`, `keyword`, `function`, `variable`, `string`, `number`
Panels, dialogs, menus, and toasts select their depth from `background.raised`. Dialogs additionally support a
contextual `@dialog` surface. The TUI re-resolves the palette after applying `@dialog`, so tokens that reference
`$background.default` follow the dialog background:
`$background.base` follow the dialog background:
```json title="themes/ocean.json"
{
"version": 2,
"base": {
"background": {
"default": "#071521",
"base": "#071521",
"raised": {
"base": "#0c1f2e",
"high": "#102a3c",
@@ -259,12 +259,12 @@ contextual `@dialog` surface. The TUI re-resolves the palette after applying `@d
},
"@dialog": {
"background": {
"default": "$background.raised.base"
"base": "$background.raised.base"
}
}
}
}
```
`@dialog` accepts the same token groups as the base mode. An action's contextual `default` also becomes the fallback
`@dialog` accepts the same token groups as the base mode. An action's contextual `base` also becomes the fallback
for its unspecified states; an explicit state override still wins.