Merge remote-tracking branch 'origin/master' into worktree/remove-sdk-project-toolchain

This commit is contained in:
Tianyi Cui
2026-08-11 16:54:34 +08:00
18 changed files with 336 additions and 15 deletions

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-10-pre-plugin-theme-bootstrap.md
2026-08-10-pre-plugin-theme-bootstrap.md: 276d564dcef60e5130b4b2d0ef7cbc12eeb8b6d6
2026-08-10-pre-plugin-theme-bootstrap.zh.md: 7162b2d851b50f75b3af6320ad9348383933e50f

View File

@@ -0,0 +1,37 @@
# Agent Note: Pre-Plugin Theme Bootstrap
Status: implemented
English | [中文](2026-08-10-pre-plugin-theme-bootstrap.zh.md)
## Problem
The web shell renders `Loading plugins…` before the browser-side plugin tree activates. The theme tokens are already loaded with the shell styles, but `color-scheme` and `body[data-ds-dark-theme]` are not written until ui-theme's ThemeService and ui-layout's ThemePresenter activate; with a persisted dark preference, the loading page therefore renders first with the light palette and then switches to dark.
`dshClient.immediately` only includes the bundle in first-stage prefetching; it does not cause the plugin to execute before HTML parsing or the shell's initial render. Changing only the client plugin's loading tier cannot close this window.
## Decision
ui-theme's host half transforms each index HTML document through `ctx.httpServer.tapIndex()`, inserting a synchronous inline script immediately after the opening `<body>` tag. The transform registers under an optional `httpServer` injection, so compositions without that service still activate ui-theme and install no transform. When the HTML parser executes the script, the body exists, but the shell's module script and React root have not yet run.
The host half registers the [`ui-theme.preference` settings section](2026-08-06-host-backed-web-preferences.md) when a settings provider exists. For each index response, it embeds that schema-validated built-in preference in the inline script; without a settings provider or active registration, it embeds the `system` default. The browser resolves `system` through `prefers-color-scheme`, falling back to light when `matchMedia` is unavailable. It writes only the two pieces of DOM state that ThemePresenter later owns: `document.documentElement.style.colorScheme` and `body[data-ds-dark-theme]`.
The bootstrap logic recognizes only the built-in `light`, `dark`, and `system` semantics. It registers no listeners and does not resolve third-party themes or token overrides. After the browser-side plugin tree activates, ThemeService remains authoritative for theme state, and ThemePresenter writes the complete resolved result back to the same DOM state and owns subsequent updates and disposal.
## Verification
ui-theme's unit tests cover activation without either optional Host service, the script position, Host-setting precedence, the OS preference, missing `matchMedia`, input without a body, live settings reads, and disposal of the Host registrations with the plugin fiber. A Chromium scenario for the real web composition selects the durable dark preference, holds the plugin bundle request open to keep the loading page observable, then asserts that the index response produces a dark background, the body attribute, and the root element's `color-scheme`. The change does not alter the accessibility tree, so it produces no new page golden.
## Alternatives considered
**Hard-code the logic in `apps/web/index.html`.** This would run at the same point, but static HTML cannot embed the current Host setting and would duplicate the preference resolution and DOM fields owned by ui-theme. The Host transform follows the theme plugin's lifecycle and keeps the application shell unaware of the theme domain.
**Make the ui-theme client bundle synchronous or activate it earlier.** `immediately` controls only prefetching; plugin instantiation still occurs after the shell starts running. Blocking the initial render until ThemeService activates would delay the visible loading and error screens and make the shell depend on the plugin tree it monitors to render failures.
**Rely only on CSS `prefers-color-scheme`.** Media queries cannot read an explicit persisted choice, so a user who selects dark while the operating system uses light would still see a flash.
**Run in `<head>` and add a temporary class to html.** The body does not exist yet, and this would require a set of temporary selectors separate from the final palette attributes. Immediately after `<body>` is the earliest parse position that can write the final DOM fields directly.
## Consequences
The loading page's first frame matches the durable built-in preference and defaults to the OS preference when no settings provider is composed. The index transform reads Host settings for every response, while the inline script contains only the selected built-in value and `system` resolution. Changes to the built-in preference semantics or ThemePresenter DOM fields must update both the script and ThemeService. A custom theme still applies fully only after the browser plugins activate; during the loading interval, the page uses the light or dark base palette to which that theme resolves.

View File

@@ -0,0 +1,37 @@
# Agent Note: 插件激活前的主题引导
Status: implemented
[English](2026-08-10-pre-plugin-theme-bootstrap.md) | 中文
## Problem
Web 壳在浏览器侧插件树激活前呈现 `Loading plugins…`。主题 token 已随壳样式加载,但 `color-scheme``body[data-ds-dark-theme]` 要等 ui-theme 的 ThemeService 与 ui-layout 的 ThemePresenter 激活后才写入;持久化偏好为深色时,加载页因此先按浅色调色板绘制,再切为深色。
`dshClient.immediately` 只把 bundle 纳入第一阶段预取,不会让插件在 HTML 解析或壳首次渲染前执行。仅调整客户端插件的加载档位无法关闭这段时间窗口。
## Decision
ui-theme 的主机侧通过 `ctx.httpServer.tapIndex()` 转换每份 index HTML`<body>` 起始标签后紧接一段同步内联脚本。该转换通过可选的 `httpServer` 注入注册,因此不含该服务的组合仍会激活 ui-theme但不会安装转换。HTML 解析器执行该脚本时body 已存在,而壳的模块脚本与 React 根节点尚未运行。
settings provider 存在时,主机侧会注册 [`ui-theme.preference` settings 分节](2026-08-06-host-backed-web-preferences.md)。它为每份 index 响应把经过 schema 校验的内建偏好嵌入内联脚本;不存在 settings provider 或有效注册时则嵌入默认值 `system`。浏览器通过 `prefers-color-scheme` 解析 `system`,不支持 `matchMedia` 时回退为浅色。脚本只写 ThemePresenter 后续拥有的两项 DOM 状态:`document.documentElement.style.colorScheme``body[data-ds-dark-theme]`
引导逻辑只认识内建的 `light``dark``system` 语义,不注册监听器,也不解析第三方主题或 token 覆盖。浏览器侧插件树激活后ThemeService 仍是主题状态的权威来源ThemePresenter 会把完整解析结果重新写入同一组 DOM 状态并负责后续更新与释放。
## Verification
ui-theme 的单元测试覆盖不含任一可选 Host 服务时的激活、脚本位置、Host 设置优先级、系统偏好、缺少 `matchMedia`、不含 body 的输入、实时读取 settings以及 Host 注册随插件 fiber 一同释放。真实 Web 组合的 Chromium 场景会选择持久化深色偏好并拦住插件 bundle 请求,使加载页保持可观察,再断言 index 响应产生了深色背景、body 属性和根元素 `color-scheme`。该变化不改变可访问性树,因此不产生新的页面 golden。
## Alternatives considered
**把逻辑固定写进 `apps/web/index.html`。** 这样能在相同时机执行,但静态 HTML 无法嵌入当前 Host 设置,还会复制 ui-theme 拥有的偏好解析和 DOM 字段Host 转换会跟随主题插件的生命周期,并让应用壳无需了解主题领域。
**让 ui-theme 客户端 bundle 同步或更早激活。** `immediately` 只控制预取,插件实例化仍发生在壳开始运行之后;把首次渲染阻塞到 ThemeService 激活会延后可见的加载与报错界面,也会让壳的故障呈现依赖被它监测的插件树。
**只依赖 `prefers-color-scheme` 的 CSS。** 媒体查询无法读取显式持久化选择,因此操作系统为浅色而用户选择深色时仍会闪烁。
**在 `<head>` 中执行并给 html 添加临时类。** body 此时尚不存在,还需要一套与正式调色板属性不同的临时选择器。紧接 `<body>` 是能够直接写正式 DOM 字段的最早解析位置。
## Consequences
加载页首帧与持久化内建偏好一致;未组合 settings provider 时则默认采用系统偏好。index 转换会为每份响应读取 Host settings而内联脚本只包含选定的内建值与 `system` 解析逻辑。内建偏好语义或 ThemePresenter DOM 字段变化时,必须同时更新脚本与 ThemeService。自定义主题仍会在浏览器插件激活后才完整应用加载期间页面使用该主题解析后的浅色或深色基础调色板。