opentui(v6): ink-budget follow-up — transparent root canvas; muted stops borrowing banner_dim

This commit is contained in:
alt-glitch 2026-06-11 00:16:00 +05:30
parent a089614451
commit 408789d909
4 changed files with 37 additions and 16 deletions

View file

@ -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,

View file

@ -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 =

View file

@ -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')
})
})

View file

@ -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 {