Repository navigation
feat(shell): 宿主新增 navop.i18n / navop.theme 两条 host 模块 - #384
Merged
Merged
Conversation
扩展页的文案与配色此前没有通道: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 辅助完成,已人工复核。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Shell pages contributed by extensions (
shellViews[].entry/pages[].entry, the QuickJSsurface) 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 paintswith. 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 giveThere was no i18n channel whatsoever: greps over
gpui-kit/crates/shell/srcandcrates/universal-plugins/src/shell_plugin_host/fori18n/locale/set_locale/rust_i18nreturn zero hits, andshellViews[].title/pages[].titleare single-languagestrings. In
navop-extensions, 15 of the 27 extension UI scripts (extensions/**/ui/*.js) carrytheir 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 isdeliberately no message catalogue — the extension owns its strings. This mirrors the existing
driver precedent (
extensions/ipc/*/locales/*.yml+translate_textincrates/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-i18ncatalogue.gpui_component::set_locale()only swaps the host's own catalogue and does not make scriptsre-run, so the settings-tab language dropdown now also calls
universal_plugins::refresh_shell_views(cx). That bumps the revision and asks every mounted scriptview 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::syncpropagates light/dark and the custom accent end to end,and
ScriptView::rendercompares the token snapshot every frame, marking itself dirty when itmoves — so a theme change already re-runs the script. What was missing is coverage.
cx.theme().colorsis gpui-base's closed 18-token set; the names the component library actuallypaints with —
list_active,table_hover,list_even,sidebar_*,tab_*,chart_*— areundefinedin JS, and anundefinedreaching the colour bridge blanks the whole page(
expected a string, got nil, "This view could not be rendered").Widening gpui-base's
ColorTokenswould touch thetoken_names_match_gpui_base_fieldsguard andthe whole UI stack, so this adds a parallel channel projecting
gpui_component::Theme::colorsinstead:
appearance(),names(),colors(),color(name),revision(). Colours come back as#rrggbbaa. The table is built by reflecting over theThemeColorserialization rather than froma 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.mdsays so explicitly: the componenttheme has no
surface/destructive; it haspopover/danger.Declaration surface
A new host module has to be registered in four places, and
ShellViewContribisdeny_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.
module_specifier()/ALLcrates/extension-runtime/src/extension/manifest/contributes/shell.rscrates/extension-runtime/src/extension/manifest/shell_validation.rs:154with_host_modulewiringcrates/universal-plugins/src/shell_plugin_host/policy.rs:111,114manifest.md,script-api.md,SKILL.md)navop-extensions(separate repo)Also worth recording:
shell_page_host::ensure_embeddableis a blacklist — it blocks modulesthat touch a backend connection.
i18nandthemecarry no resource access and are deliberatelynot 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:
import ... from "navop.i18n"— no such modulelocale(): string,available(): string[],revision(): numbercx.theme().colors.list_active→undefined(page blanks)navop.theme.color("list_active")→"#rrggbbaa", ornullwhen unknowncx.theme().colors.surface— 18 names onlycx.theme()still serves the 18 semantic tokens —navop.themeis additiveHow to Test
All with
RUSTFLAGS="-D warnings":Both
cargo check -p mainstates are run becauseShellHostModuleis a shared contract andshell_plugin_host/**only compiles withshell-pluginson.The single failure,
extension_runtime_wasm_contract_tests::db_tree_action_runs_registered_wasm_component,is pre-existing and unrelated: the
watparser is only enabled throughextension-wasm'sdev-dependencies, so running that package alone fails with
magic header not detected. Buildingboth packages together makes all 6 wasm contract tests pass, and CI's
cargo test --allis 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 scriptimport 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_modulesrejects"declares
requires_backend+ emptybackends" at install time. Misclassifying these twowould make pure-UI pages — which is exactly their target case — uninstallable.
session_free_modules_register_under_the_declared_names: the modules really mount under thename
module_specifier()returns, not merely declare it.the_skill_document_lists_every_declared_host_module: reads the neighbour repo'sreferences/manifest.mdand requires both spellings of everyALLentry on one line.changing_the_ui_language_reruns_mounted_extension_views: the refresh call exists and sitsafter
set_localein the dropdown handler (the other order reads the stale language).every_theme_color_field_is_serialized_as_a_colorandcomponent_theme_names_differ_from_the_semantic_palette: the reflected colour table is really#rrggbbaafor 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:i18n.rs,theme.rs) and the guard tests are AI-produced, then reviewedagainst the existing host modules (
context.rs,dev.rs) for style and code organization.ensure_embeddableis a blacklist, not a whitelist; andScriptView::refreshrebuilds theexisting instance's
render()without re-runninginit(). The second one changes what thedocs must tell extension authors — a catalogue picked once in
init()stays stale after alanguage switch — so both are now accurate in the code comments and in
script-api.md.with a fresh
grep/ re-run before being written down, not recalled.rustfmt --check) against aHEADcopy, to separatedeviations this PR introduces from ones the baseline already had.
Checklist
cargo runfor story tests related to the changes.Checklist notes: the
cargo runstory tests were not run — the story gallery does not covershell extension pages, and this PR has no visual change; verification was done with the
cargo check/cargo testcommands 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.