Skip to content

feat(shell): 宿主新增 navop.i18n / navop.theme 两条 host 模块 - #384

Merged
feigeCode merged 1 commit into
mainfrom
feat/shell-host-i18n-theme
Oct 12, 2026
Merged

feigeCode merged 1 commit into
mainfrom
feat/shell-host-i18n-theme

Conversation

@feigeCode

Copy link
Copy Markdown
Owner

Description

Shell pages contributed by extensions (shellViews[].entry / pages[].entry, the QuickJS
surface) had two blind spots in the host-module surface: the host gave them no language fact at
all
, and cx.theme() exposed only 18 of the ~100+ colour names the host actually paints
with
. This PR adds two host modules that close both.

No extension is touched. Nothing declares the new modules yet, so runtime behaviour is
unchanged — this is the host-side half only.

navop.i18n — the host had no language fact to give

There was no i18n channel whatsoever: greps over gpui-kit/crates/shell/src and
crates/universal-plugins/src/shell_plugin_host/ for i18n / locale / set_locale /
rust_i18n return zero hits, and shellViews[].title / pages[].title are single-language
strings. In navop-extensions, 15 of the 27 extension UI scripts (extensions/**/ui/*.js) carry
their copy as hard-coded Chinese literals — 472 of them — so a shell page stays Chinese no matter
what language the host is set to.

The module hands over the fact only: locale(), available(), revision(). There is
deliberately no message catalogue — the extension owns its strings. This mirrors the existing
driver precedent (extensions/ipc/*/locales/*.yml + translate_text in
crates/db_view/src/database_view_plugin.rs) rather than making the host resolve extension keys,
which would put extension copy inside the host's rust-i18n catalogue.

gpui_component::set_locale() only swaps the host's own catalogue and does not make scripts
re-run, so the settings-tab language dropdown now also calls
universal_plugins::refresh_shell_views(cx). That bumps the revision and asks every mounted script
view to rebuild. Without it the UI language changes, the extension page silently stays on the old
language, and nothing reports an error — cx.notify() repaints but does not re-run the script.

navop.theme — cx.theme() sees 18 names, the host paints with 100+

Theme already follows the host: apply_appearance → Theme::sync_base →
ScriptView::render/theme_tokens::sync propagates light/dark and the custom accent end to end,
and ScriptView::render compares the token snapshot every frame, marking itself dirty when it
moves — so a theme change already re-runs the script. What was missing is coverage.
cx.theme().colors is gpui-base's closed 18-token set; the names the component library actually
paints with — list_active, table_hover, list_even, sidebar_*, tab_*, chart_* — are
undefined in JS, and an undefined reaching the colour bridge blanks the whole page
(expected a string, got nil, "This view could not be rendered").

Widening gpui-base's ColorTokens would touch the token_names_match_gpui_base_fields guard and
the whole UI stack, so this adds a parallel channel projecting gpui_component::Theme::colors
instead: appearance(), names(), colors(), color(name), revision(). Colours come back as
#rrggbbaa. The table is built by reflecting over the ThemeColor serialization rather than from
a hand-maintained name list — upstream renames and new colours happen without us, and a copied list
is one more thing that drifts.

The two naming schemes are not the same, and script-api.md says so explicitly: the component
theme has no surface / destructive; it has popover / danger.

Declaration surface

A new host module has to be registered in four places, and ShellViewContrib is
deny_unknown_fields, so a mismatch is not a late failure — the extension vanishes at parse time.
That is why the enum, the allowlist and the docs move in one go, and why the host ships before any
extension declares them.

Place File
enum + serde rename + module_specifier() / ALL crates/extension-runtime/src/extension/manifest/contributes/shell.rs
install-time allowlist crates/extension-runtime/src/extension/manifest/shell_validation.rs:154
with_host_module wiring crates/universal-plugins/src/shell_plugin_host/policy.rs:111,114
skill docs (manifest.md, script-api.md, SKILL.md) navop-extensions (separate repo)

Also worth recording: shell_page_host::ensure_embeddable is a blacklist — it blocks modules
that touch a backend connection. i18n and theme carry no resource access and are deliberately
not listed; a comment says so, so nobody "helpfully" adds them and cuts embedded pages off from
their own language and colours.

Screenshot

Text-only / API-surface change — no visual diff, since no extension uses either module yet. The
user-visible contract is the API surface:

Before After
import ... from "navop.i18n" — no such module locale(): string, available(): string[], revision(): number
cx.theme().colors.list_active → undefined (page blanks) navop.theme.color("list_active") → "#rrggbbaa", or null when unknown
cx.theme().colors.surface — 18 names only unchanged; cx.theme() still serves the 18 semantic tokens — navop.theme is additive

How to Test

All with RUSTFLAGS="-D warnings":

cargo check -p main --locked
cargo check -p main --locked --no-default-features --features wasm-components,embedded-webview,windows-native-rdp
cargo test -p universal-plugins --features shell-plugins      # 68 passed; 0 failed
cargo test -p main --bin navop -- changing_the_ui_language    # 1 passed
cargo test -p extension-runtime --lib                         # 217 passed; 1 failed (pre-existing)

Both cargo check -p main states are run because ShellHostModule is a shared contract and
shell_plugin_host/** only compiles with shell-plugins on.

The single failure, extension_runtime_wasm_contract_tests::db_tree_action_runs_registered_wasm_component,
is pre-existing and unrelated: the wat parser is only enabled through extension-wasm's
dev-dependencies, so running that package alone fails with magic header not detected. Building
both packages together makes all 6 wasm contract tests pass, and CI's cargo test --all is green.

New guards — 10 tests — pin the parts that have no compile-time link:

  • every_host_module_specifier_matches_its_manifest_spelling: the manifest spelling and the script
    import specifier are two halves of one chain, in two different crates. Renaming one side neither
    fails to build nor fails to install — the page just white-screens at runtime. module_specifier()
    is a match with no wildcard arm, so adding a variant is a compile error until it is listed.
  • language_and_theme_are_declared_but_never_require_a_backend: validate_modules rejects
    "declares requires_backend + empty backends" at install time. Misclassifying these two
    would make pure-UI pages — which is exactly their target case — uninstallable.
  • session_free_modules_register_under_the_declared_names: the modules really mount under the
    name module_specifier() returns, not merely declare it.
  • the_skill_document_lists_every_declared_host_module: reads the neighbour repo's
    references/manifest.md and requires both spellings of every ALL entry on one line.
  • changing_the_ui_language_reruns_mounted_extension_views: the refresh call exists and sits
    after set_locale in the dropdown handler (the other order reads the stale language).
  • every_theme_color_field_is_serialized_as_a_color and
    component_theme_names_differ_from_the_semantic_palette: the reflected colour table is really
    #rrggbbaa for 100+ fields, and the component palette is not a superset of the semantic one.
  • available_locales_are_the_ones_core_can_resolve, revision_moves_only_when_the_host_says_so,
    locale_is_never_empty.

AI Assistance

AI-generated, human-reviewed. I used AI to draft every file in this PR, then reviewed each one line
by line. Per CONTRIBUTING.md:

  • The two new modules (i18n.rs, theme.rs) and the guard tests are AI-produced, then reviewed
    against the existing host modules (context.rs, dev.rs) for style and code organization.
  • Two earlier conclusions of mine were wrong and were corrected during review:
    ensure_embeddable is a blacklist, not a whitelist; and ScriptView::refresh rebuilds the
    existing instance's render() without re-running init(). The second one changes what the
    docs must tell extension authors — a catalogue picked once in init() stays stale after a
    language switch — so both are now accurate in the code comments and in script-api.md.
  • No blind replacement: every call site and every number quoted in this description was re-derived
    with a fresh grep / re-run before being written down, not recalled.
  • Formatting was checked read-only (rustfmt --check) against a HEAD copy, to separate
    deviations this PR introduces from ones the baseline already had.
  • Nothing generated was merged unreviewed, and none of it is a mechanical AI dump.

Checklist

  • I have read the CONTRIBUTING document and followed the guidelines.
  • Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate.
  • Passed cargo run for story tests related to the changes.
  • Tested macOS, Windows and Linux platforms performance (if the change is platform-specific)

Checklist notes: the cargo run story tests were not run — the story gallery does not cover
shell extension pages, and this PR has no visual change; verification was done with the
cargo check / cargo test commands listed above instead. The change is not platform-specific
(host-side plumbing with no platform code paths), so no per-platform performance run was done.

扩展页的文案与配色此前没有通道:gpui-shell 全树没有 i18n/locale 面,
`cx.theme()` 只给 gpui-base 的 18 个语义色,而宿主界面自己在用的是组件库的
另一套名字(`list_active` / `table_hover` / `sidebar_*` …)—— 脚本读到
`undefined` 会让整页 blank(`expected a string, got nil`)。这里补两条**不碰
资源**的 host 模块,行为对现有扩展零变化(没人声明就没人拿到)。

- navop.i18n:`locale()` / `available()` / `revision()`。**只给语言事实,不带
  词条表** —— 扩展文案不在宿主的 rust-i18n 目录里,宿主替扩展解析会把两边键空间
  搅在一起(先例:IPC driver 的 `locales/*.yml` 由扩展自己带)。
- navop.theme:`appearance/names/colors/color/revision`。色表靠反射
  `ThemeColor` 的序列化结果生成,不手抄那一百多个字段名;取不到的名字返回
  `null`,不像 `cx.theme()` 那样把整页带下去。

声明面四处同步:`ShellHostModule` 枚举(serde rename `i18n`/`theme`)、
`shell_validation::validate_modules`(安装期闸门)、`policy.rs` 装载、
`shell_page_host::ensure_embeddable`(**黑名单**,两个新模块故意不在其中)+
相邻仓文档与守卫。

顺带把"脚本重跑"这件事写准:`ScriptView::refresh` → `rebuild` →
`build_snapshot(&object, …)` → `call_render(object, …)`,重建的是**已有实例的
`render()`,`init()` 不会再跑**。所以切语言后 `locale()` 必须在 `render()` 里读,
否则页面停在旧语言且不报错。这把 `refresh_shell_views` 的契约钉在注释与文档里。

守卫(都带"为什么"):
- `shell.rs`:`module_specifier()`(无通配臂 match ⇒ 加变体必编译失败)与 serde
  rename 逐变体对齐;两个新模块不得 `requires_backend`(否则只声明 i18n 的纯
  界面页会在**安装期**被拒)。
- `shell_plugin_host.rs`:无会话模块真挂在 `module_specifier()` 那个名字上。
- `extension_pages_render_tests.rs`:渲染台子补两个桩;新增**文档守卫**读相邻仓
  `references/manifest.md` 的模块表,要求 `ShellHostModule::ALL` 每个都在同一行
  同时出现两种拼写(用独立复算验过非空转:删行/改坏拼写都会报)。
- `main/src/setting_tab.rs`:语言下拉必须在 `set_locale` 之后叫醒已挂载视图。

验证(`RUSTFLAGS="-D warnings"`):
- `cargo check -p main --locked` ✅ 与 `--no-default-features
  --features wasm-components,embedded-webview,windows-native-rdp` ✅(两态)
- `cargo test -p universal-plugins --features shell-plugins` ✅ 68 passed
- `cargo test -p main --bin navop -- changing_the_ui_language` ✅ 1 passed
- `cargo test -p extension-runtime --lib` 217 passed / 1 failed:
  `db_tree_action_runs_registered_wasm_component` 是**既存**红,与本改动无关 ——
  `wat` 解析器只在 `extension-wasm` 的 dev-dependencies 里开,单包跑必然红
  (`magic header not detected`);并入该包后 6 个 wasm 契约测试全绿,CI 的
  `cargo test --all` 亦绿。

AI 辅助:本改动由 AI 辅助完成,已人工复核。
@feigeCode
feigeCode merged commit cefcc91 into main Oct 12, 2026
8 checks passed
@feigeCode
feigeCode deleted the feat/shell-host-i18n-theme branch October 12, 2026 09:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant