Context
Found while trying to boot the portal light to witness the second direction of #18569's AC-2. It is not #18569 — that one is a Monaco consumer calling the wrong accessor, and it is fixed in PR #18570. This is upstream of it and breaks the body class too.
The Problem
Measured at dev@87e3786673, local portal, one browser session:
localStorage.setItem('portalTheme', 'neo-theme-neo-light')
- reload
apps/portal/view/ViewportController.mjs:193 reads the key and calls setTheme(value, false) on a truthy value, and :364 does me.component.theme = theme. That lands — Portal.view.Viewport#theme reads neo-theme-neo-light afterwards, confirmed through the Neural Link.
Nothing else follows:
| surface |
value |
localStorage.portalTheme |
neo-theme-neo-light |
Portal.view.Viewport#theme |
neo-theme-neo-light |
document.body class |
neo-theme-neo-dark |
every .monaco-editor |
vs-dark |
every descendant's theme config |
null |
So the app renders dark while its Viewport says light. The page is internally inconsistent, and a reader who set a light preference simply does not get it.
The Architectural Reality
container.Base#afterSetTheme:306 propagates to live items and deliberately leaves raw item configs to createItem:
value && this.items?.forEach(item => {
if (!Neo.isString(item) && Neo.typeOf(item) !== 'Object') {
item.theme = value
}
})The storage read is a promise — readLocalStorageItem(...).then(...) — so setTheme lands at an unspecified point in boot. The failure is consistent with the assignment happening while the tree is still raw configs: the forEach skips them, createItem later resolves precedence against a Viewport whose theme did change, and the observed result is that neither the body class nor any descendant carries it.
⚠️ That mechanism is inferred, not measured. What is measured is the table above. I have not instrumented the ordering, and whoever takes this should establish it rather than inherit it — the ticket is the observation, not the cause.
The body class is the second half and may be a different path again: whatever writes neo-theme- onto document.body did not react either.
The Fix
Unknown, deliberately. The shape of the answer depends on which of the two is true — a propagation that ran too early over an empty item list, or a theme assignment that never reaches the body-class writer at all. Both are cheap to separate with one instrumented boot.
Acceptance Criteria
- AC-1 With
portalTheme: neo-theme-neo-light stored, a cold boot renders the portal light: document.body carries neo-theme-neo-light and no surface still reads dark.
- AC-2 ⚠️ The Viewport config and the rendered body class agree, asserted together. They disagree today, and an arm that checks only one of them passes on exactly this defect.
- AC-3 The dark preference and the no-preference default are unchanged.
Neo.config.prefersDarkTheme boots dark today and must keep doing so.
- AC-4 ⚠️ The cause is established before the fix, and stated. This ticket carries an observation and an inferred mechanism; a fix built on the inference without checking it is how the wrong path gets repaired.
- AC-5 A descendant created after the stored theme lands inherits it — that is the case the promise makes possible and the one a fix is most likely to miss.
Out of Scope
#18569 / PR #18570 — Monaco's own accessor, already fixed, and its editors now follow whatever theme the chain actually reports. The live toggle path, which works: driving Viewport#theme on a mounted tree repaints the body and the editors correctly. This is specifically the stored preference at boot.
Avoided Traps
Do not fix this by making container.Base#afterSetTheme stamp raw item configs. Its comment records why it does not — createItem resolves the config's own theme, itemDefaults, the class default and then the container's, and stamping here pre-empts the first three.
Do not assert the fix on Viewport#theme alone. It is already correct today, which is exactly what makes this defect invisible from the worker side.
Related
- #18569 / PR #18570 — the Monaco consumer, fixed; this is upstream of it.
- #18106 / #18111 —
getTheme() walking components, and the null-by-design contract for an inheriting child.
Decision Record impact
none pending AC-4 — if the cause turns out to be the propagation contract rather than the portal's boot sequence, that judgement belongs with whoever measures it.
Sweeps
Live latest-open sweep at 2026-09-09T21:48:43Z plus the filing of #18569 since: nothing equivalent open. All-state search on monaco theme dark editorTheme returned #8845 and #8727, both closed and both the origin of the theme-sync feature rather than this. A2A claim sweep: no open claim on portal theming.
Retrieval Hint: portal stored theme localStorage portalTheme setTheme viewport config body class descendants do not follow boot
Origin Session ID: c4dc8abc-31fa-4ecd-9aef-5209ccfd16c0
Context
Found while trying to boot the portal light to witness the second direction of #18569's AC-2. It is not #18569 — that one is a Monaco consumer calling the wrong accessor, and it is fixed in PR #18570. This is upstream of it and breaks the body class too.
The Problem
Measured at
dev@87e3786673, local portal, one browser session:localStorage.setItem('portalTheme', 'neo-theme-neo-light')apps/portal/view/ViewportController.mjs:193reads the key and callssetTheme(value, false)on a truthy value, and:364doesme.component.theme = theme. That lands —Portal.view.Viewport#themereadsneo-theme-neo-lightafterwards, confirmed through the Neural Link.Nothing else follows:
localStorage.portalThemeneo-theme-neo-lightPortal.view.Viewport#themeneo-theme-neo-lightdocument.bodyclassneo-theme-neo-dark.monaco-editorvs-darkthemeconfignullSo the app renders dark while its Viewport says light. The page is internally inconsistent, and a reader who set a light preference simply does not get it.
The Architectural Reality
container.Base#afterSetTheme:306propagates to live items and deliberately leaves raw item configs tocreateItem:value && this.items?.forEach(item => { if (!Neo.isString(item) && Neo.typeOf(item) !== 'Object') { item.theme = value } })The storage read is a promise —
readLocalStorageItem(...).then(...)— sosetThemelands at an unspecified point in boot. The failure is consistent with the assignment happening while the tree is still raw configs: the forEach skips them,createItemlater resolves precedence against a Viewport whose theme did change, and the observed result is that neither the body class nor any descendant carries it.⚠️ That mechanism is inferred, not measured. What is measured is the table above. I have not instrumented the ordering, and whoever takes this should establish it rather than inherit it — the ticket is the observation, not the cause.
The body class is the second half and may be a different path again: whatever writes
neo-theme-ontodocument.bodydid not react either.The Fix
Unknown, deliberately. The shape of the answer depends on which of the two is true — a propagation that ran too early over an empty item list, or a
themeassignment that never reaches the body-class writer at all. Both are cheap to separate with one instrumented boot.Acceptance Criteria
portalTheme: neo-theme-neo-lightstored, a cold boot renders the portal light:document.bodycarriesneo-theme-neo-lightand no surface still reads dark.Neo.config.prefersDarkThemeboots dark today and must keep doing so.Out of Scope
#18569 / PR #18570 — Monaco's own accessor, already fixed, and its editors now follow whatever theme the chain actually reports. The live toggle path, which works: driving
Viewport#themeon a mounted tree repaints the body and the editors correctly. This is specifically the stored preference at boot.Avoided Traps
Do not fix this by making
container.Base#afterSetThemestamp raw item configs. Its comment records why it does not —createItemresolves the config's own theme,itemDefaults, the class default and then the container's, and stamping here pre-empts the first three.Do not assert the fix on
Viewport#themealone. It is already correct today, which is exactly what makes this defect invisible from the worker side.Related
getTheme()walking components, and the null-by-design contract for an inheriting child.Decision Record impact
nonepending AC-4 — if the cause turns out to be the propagation contract rather than the portal's boot sequence, that judgement belongs with whoever measures it.Sweeps
Live latest-open sweep at 2026-09-09T21:48:43Z plus the filing of #18569 since: nothing equivalent open. All-state search on
monaco theme dark editorThemereturned #8845 and #8727, both closed and both the origin of the theme-sync feature rather than this. A2A claim sweep: no open claim on portal theming.Retrieval Hint:
portal stored theme localStorage portalTheme setTheme viewport config body class descendants do not follow bootOrigin Session ID: c4dc8abc-31fa-4ecd-9aef-5209ccfd16c0