diff --git a/ui-opentui/src/boundary/renderer.ts b/ui-opentui/src/boundary/renderer.ts index 1131fd12990..807e4ad02be 100644 --- a/ui-opentui/src/boundary/renderer.ts +++ b/ui-opentui/src/boundary/renderer.ts @@ -12,7 +12,6 @@ import { createCliRenderer, type CliRenderer, type KeyEvent, type Selection } from '@opentui/core' import { Deferred, Effect } from 'effect' -import { DEFAULT_THEME } from '../logic/theme.ts' import { RendererError } from './errors.ts' import { installFfiCoordSafety } from './ffiSafe.ts' import { getLog } from './log.ts' @@ -67,10 +66,10 @@ export const acquireRenderer = Effect.fn('Renderer.acquire')(function* (options: Effect.tryPromise({ try: () => createCliRenderer({ - // Root canvas (design pass): paint the dark room from the first frame - // — true black by default. Skin overrides land reactively via the - // header's theme effect (view/header.tsx setBackgroundColor). - backgroundColor: DEFAULT_THEME.color.bg, + // Root canvas: TRANSPARENT by default — the terminal's own background + // shows through (do not paint a "default dark" canvas; glitch hated + // it). A skin's explicit ui_bg lands reactively via the header's + // theme effect (view/header.tsx setBackgroundColor). // scrollbox clips growing output → no terminal-scrollback corruption (gotcha §8 #2). externalOutputMode: 'passthrough', targetFps: 60, diff --git a/ui-opentui/src/logic/theme.ts b/ui-opentui/src/logic/theme.ts index 8cd63a3c2dd..19ac4d28ee3 100644 --- a/ui-opentui/src/logic/theme.ts +++ b/ui-opentui/src/logic/theme.ts @@ -12,6 +12,12 @@ * * Source of truth for the contract: ui-tui/src/theme.ts (+ GatewaySkin in * ui-tui/src/gatewayTypes.ts). Keep this port in sync if that contract changes. + * + * INTENTIONAL divergences from the Ink port (visual-hierarchy design pass): + * - `muted` is a true NEUTRAL grey (not a darker gold) and no longer borrows + * the skin's `banner_dim` (a banner-gold shade in the stock skin); skins + * override it via the dedicated `ui_muted` key instead. + * - a `bg` token paints the root canvas (true black/white; `ui_bg` override). */ import { FALSE_RE, TRUE_RE } from './env.ts' @@ -22,9 +28,10 @@ export interface ThemeColors { border: string text: string muted: string - /** Root canvas background (design pass: the view is a dark room — true black - * by default; skins may override via `ui_bg`). CSS names are valid OpenTUI - * ColorInput, so the defaults use `black`/`white`, not invented hexes. */ + /** Root canvas background. DEFAULT IS `transparent` — the terminal's own + * background shows through (glitch rejected a painted "default dark" canvas; + * the dark room is the user's terminal, not ours). Skins may opt into a + * painted canvas via `ui_bg`. */ bg: string completionBg: string completionCurrentBg: string @@ -262,7 +269,7 @@ export const DARK_THEME: Theme = { // step, CSS `gray` — no invented hexes. Grey = everything that merely // happened; gold stays earned. muted: '#808080', - bg: 'black', + bg: 'transparent', completionBg: '#1a1a2e', completionCurrentBg: '#333355', completionMetaBg: '#1a1a2e', @@ -308,7 +315,7 @@ export const LIGHT_THEME: Theme = { // same disease as dark: muted was `#7A5A0F` (gold-brown). True neutral — // the statusFg `#333333` family's lighter step, CSS `dimgray`. muted: '#696969', - bg: 'white', + bg: 'transparent', completionBg: '#F5F5F5', completionCurrentBg: mix('#F5F5F5', '#A0651C', 0.25), completionMetaBg: '#F5F5F5', @@ -445,7 +452,13 @@ export function fromSkin( const accent = c('ui_accent') ?? c('banner_accent') ?? d.color.accent const bannerAccent = c('banner_accent') ?? c('banner_title') ?? d.color.accent - const muted = c('banner_dim') ?? d.color.muted + // Design pass (Appendix C precondition): `muted` is the transcript's "merely + // happened" NEUTRAL — it must NOT borrow `banner_dim` (the stock skin's dim + // GOLD banner shade; the Ink engine still uses it for its banner-tinted dim). + // Borrowing it re-golded every dim surface in the live app and made the hero + // color the wallpaper. Skins that want a custom transcript dim ship the + // dedicated `ui_muted` key; everything else gets the theme's true neutral. + const muted = c('ui_muted') ?? d.color.muted const completionBg = c('completion_menu_bg') ?? d.color.completionBg const completionCurrentBg = diff --git a/ui-opentui/src/test/inkBudget.test.tsx b/ui-opentui/src/test/inkBudget.test.tsx index 2e24d4c3810..fa4ddcd1d24 100644 --- a/ui-opentui/src/test/inkBudget.test.tsx +++ b/ui-opentui/src/test/inkBudget.test.tsx @@ -50,9 +50,9 @@ describe('theme — muted is a true neutral, not darker gold (design-pass precon expect(isAchromatic(DARK_THEME.color.sessionBorder)).toBe(true) }) - test('bg token: true black (dark) / white (light) by CSS name', () => { - expect(DARK_THEME.color.bg).toBe('black') - expect(LIGHT_THEME.color.bg).toBe('white') + test('bg token: TRANSPARENT by default — the terminal owns the canvas (glitch decision)', () => { + expect(DARK_THEME.color.bg).toBe('transparent') + expect(LIGHT_THEME.color.bg).toBe('transparent') }) test('fromSkin: skins may override bg via ui_bg; default stays the theme bg', () => { @@ -60,8 +60,14 @@ describe('theme — muted is a true neutral, not darker gold (design-pass precon expect(fromSkin({}, {}).color.bg).toBe(DARK_THEME.color.bg) }) - test('skin mapping intact: banner_dim still re-points muted (skins keep their dim)', () => { - expect(fromSkin({ banner_dim: '#445566' }, {}).color.muted).toBe('#445566') + test('muted no longer borrows banner_dim (the stock skin ships GOLD there — the live re-gold bug)', () => { + // The default Hermes skin sends banner_dim '#B8860B'; borrowing it for + // muted re-golded every dim surface in the live app. It must stay neutral. + expect(fromSkin({ banner_dim: '#B8860B' }, {}).color.muted).toBe(DARK_THEME.color.muted) + }) + + test('skins keep a dedicated transcript-dim override: ui_muted', () => { + expect(fromSkin({ ui_muted: '#445566' }, {}).color.muted).toBe('#445566') }) }) diff --git a/ui-opentui/src/view/header.tsx b/ui-opentui/src/view/header.tsx index a241a97bd17..742d134c953 100644 --- a/ui-opentui/src/view/header.tsx +++ b/ui-opentui/src/view/header.tsx @@ -24,6 +24,9 @@ export function Header(props: { store: SessionStore }) { // Root canvas paint — best-effort (a styling miss must never crash chrome). createEffect(() => { const bg = theme().color.bg + // Default is `transparent` = leave the terminal's background alone; only a + // skin's explicit ui_bg paints the canvas. + if (bg === 'transparent') return try { renderer.setBackgroundColor(bg) } catch {