fix(tui): unify attention indicators with unread accent (#45741)

Use the unread accent color for question and permission status indicators by default, preserving semantic tokens and explicit theme overrides.
This commit is contained in:
Kit Langton 2026-08-27 17:15:44 -04:00 committed by GitHub
parent 39416a0d95
commit f607ca4c72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 39 additions and 8 deletions

View file

@ -0,0 +1,5 @@
---
"@opencode-ai/theme": patch
---
Use the unread accent color by default for question and permission status indicators, while preserving explicit theme overrides.

View file

@ -123,8 +123,8 @@ export const DEFAULT_THEME = {
},
status: {
running: "$hue.interactive.800",
question: "$text.feedback.info.default",
permission: "$text.feedback.warning.default",
question: "$text.status.unread",
permission: "$text.status.unread",
unread: "$hue.accent.800",
},
feedback: {
@ -344,8 +344,8 @@ export const DEFAULT_THEME = {
},
status: {
running: "$hue.interactive.200",
question: "$text.feedback.info.default",
permission: "$text.feedback.warning.default",
question: "$text.status.unread",
permission: "$text.status.unread",
unread: "$hue.accent.200",
},
feedback: {

View file

@ -11,13 +11,33 @@ test.each(["light", "dark"] as const)("built-in %s themes resolve status colors"
for (const document of [DEFAULT_THEME, migrateV1(source)]) {
const theme = resolveThemeDocument(document, mode)
expect(theme.text.status.running.equals(theme.hue.interactive[mode === "light" ? 800 : 200])).toBeTrue()
expect(theme.text.status.question.equals(theme.text.feedback.info.default)).toBeTrue()
expect(theme.text.status.permission.equals(theme.text.feedback.warning.default)).toBeTrue()
expect(theme.text.status.question.equals(theme.text.status.unread)).toBeTrue()
expect(theme.text.status.permission.equals(theme.text.status.unread)).toBeTrue()
expect(theme.text.status.unread.equals(theme.hue.accent[mode === "light" ? 800 : 200])).toBeTrue()
expect(theme.contextual.elevated.text.status).toEqual(theme.text.status)
}
})
test.each(["light", "dark"] as const)("custom %s themes inherit the unread attention color", (mode) => {
for (const standalone of [false, true]) {
const theme = resolveThemeDocument(
Schema.decodeUnknownSync(ThemeDocument)({
version: 2,
standalone,
[mode]: {
hue: { ...DEFAULT_THEME[mode].hue, accent: "$hue.purple" },
text: { status: { unread: "#abcdef" } },
},
}),
mode,
)
expect(theme.text.status.unread.equals(RGBA.fromHex("#abcdef"))).toBeTrue()
expect(theme.text.status.question.equals(theme.text.status.unread)).toBeTrue()
expect(theme.text.status.permission.equals(theme.text.status.unread)).toBeTrue()
expect(theme.contextual.elevated.text.status).toEqual(theme.text.status)
}
})
test.each(["light", "dark"] as const)("custom %s themes inherit and override status colors", (mode) => {
for (const standalone of [false, true]) {
const theme = resolveThemeDocument(
@ -27,8 +47,7 @@ test.each(["light", "dark"] as const)("custom %s themes inherit and override sta
[mode]: {
hue: { ...DEFAULT_THEME[mode].hue, interactive: "$hue.purple", accent: "$hue.orange" },
text: {
feedback: { warning: { default: "#654321" } },
status: { question: "#123456" },
status: { question: "#123456", permission: "#654321" },
},
},
}),

View file

@ -124,6 +124,12 @@ for (const orientation of ["horizontal", "vertical"] as const) {
setActive("second")
setStatus({ ...EMPTY_SESSION_TAB_STATUS, busy: true, attention })
await app.renderOnce()
const indicatorColor = () =>
app
.captureSpans()
.lines.flatMap((line) => line.spans)
.find((span) => span.text.trim() === (attention === "question" ? "?" : "!"))?.fg
expect(indicatorColor()?.toInts()).toEqual(theme.text.status[attention].toInts())
const glow = () => {
const colors = app
.captureSpans()
@ -140,6 +146,7 @@ for (const orientation of ["horizontal", "vertical"] as const) {
expect(full).toBeGreaterThan(0)
setActive("first")
await app.renderOnce()
expect(indicatorColor()?.toInts()).toEqual(theme.text.status[attention].toInts())
const dim = glow()
expect(dim).toBeGreaterThan(0)
expect(dim).toBeLessThan(full)