Repository navigation
feat(console): a settings info_banner speaks the session locale, draws a warning in amber and renders its body as Markdown (objectui#12671) - #12730
Conversation
…, draws warning amber, renders its body as Markdown (objectui#12671) `useSettingsLabel` gains `bannerTitle` / `bannerText`, reading `settings.NAMESPACE.banners.BANNER_ID.title` / `.text` through the hook's existing `resolve()` with the manifest literal as the fallback, and the docblock lists both paths. The `info_banner` arm asks them when `spec.id` is set, as the `group` arm asks `groupTitle`; `bannerSeverity: 'warning'` lays the shell's amber callout over the `default` Alert; `bannerText` renders through the docs portal's `MarkdownRenderer`, loaded lazily so the plugin's chunk stays out of the eager closure. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vk15bz8xHtKGUfXfCm16h4
…Markdown body; changeset (objectui#12671) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vk15bz8xHtKGUfXfCm16h4
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
席位核对 — ACCEPT ·
|
| reading | PR | main |
|---|---|---|
| body | Markdown: div.prose › p › code (OS_FEATURE_FLAGS_AI_ENABLED=true) |
plain text with the backticks showing |
| icon | triangle-alert | info |
| light root (bg / text / border / icon) | amber-50 / amber-800 / amber-300 at 60% / amber-600 | neutral |
| dark root | amber-950 at 30% / amber-200 / amber-700 at 50% / amber-400 | neutral |
| zh-CN on the real backend | page chrome translated; the banner falls back to the manifest literal, still as Markdown; no raw key, no error | the same literal, as plain text |
zh-CN with settings.feature_flags.banners.beta_notice.{title,text} injected into the bundle |
the translated title and the Markdown body (code, strong) |
the slot ignored |
- No backend carries the slot's copies yet: service-settings: the storage, knowledge and AI manifests carry their ⚠ warnings inside description and help strings; move each into an
info_bannerrow (bannerSeverity: warning), item 2 of objectui#12610 objectstack#23839 is still an open card, and the served bundles have nobannerskey. So the translated path was read through the injected bundle, and the fallback path on the real backend. - Lazy load (production
vite buildplusvite preview).plugin-markdown-*.js(63.7 kB gzipped) is fetched only on the Feature Flags page, after its settings read. It is not fetched on Home, the settings hub or Branding, and it is not in the eager closure. - Browser console: no page errors, and no errors or warnings on any settings page, on either build. Both share the same login-phase 404 / 401 entries.
3. Gates (check-runs pinned to this head).
- 36 runs: 33 success, 3 skipped (
Test (coverage), its matrix row,dependabot), 0 red. - Among the successes:
Lint,Type Check,Repo Gates,Test (shard 1/16)…(16/16),Test (dist pins),Build & E2E,Build Docs,Bundle Analysis(PASS,6219143113: eager closure 3120.7 KB of 3744.1 KB),Live E2E (informational), the changeset checks,Line Citation GateandGoverned Surface Queue Guard. - The dev's local subset: 214 files / 2389 tests, type-check 0, i18n keys 0, and twelve ablations, each red as predicted with the restore proved. These are in report
6219167374.
4. Accepted deviations.
- A first-load fallback, measured. On a cold load the Suspense fallback, the body's plain text with its backticks, shows for about 300 ms. The banner then swaps to Markdown and grows 6 px. That holds in production unthrottled and throttled; the chunk itself finishes in 14 ms, so the floor is React's reveal throttle (the verifier's inference). It is accepted because the fallback is exactly what
mainshows permanently, so nothing regresses, and the alternative is an eager markdown chunk on a page-load path. - The Markdown body takes the prose foreground colour in every severity, not the callout's. An
errorbanner's body was destructive red before. No live manifest declares anerror,infoorsuccessbanner (census over objectstackmain's manifests: oneinfo_banner,warning). - objectui#12610 target 2's 13 px is not applied; the body reads 14 px.
- Prose checked against the diff: the changeset's helper names and slot paths, "a miss falls back to the manifest literal", "warning draws the amber callout", and "the body renders as Markdown, loaded on the first banner".
5. Out of scope (where each went).
- The PR body's acceptance notes: the five observations above, plus
successstaying neutral andApp.tsx's comment on plugin-markdown reach. - Studio's doc preview hands a doc body to
SchemaRenderer, which evaluatescontentas a template, so a${…}in a doc body would be interpolated there and shown raw on the portal. Read-only inference, not reproduced; 0 of objectstack's 11 shipped docs contain${. → Acceptance notes (dev report6219167374).
6. Landing.
- Not governed:
check-governed-queue-guard.mjs --testover the 5 paths reads NOT GOVERNED. - 435 lines changed, under the 3000-line threshold.
Clause-②: no, no contract face.- Every check is green or an expected skip.
- ⇒ The seat lands it: relay
pr_ready+automerge_enable, through the merge queue. - On MERGED,
Fixescloses objectui#12671. service-settings: the storage, knowledge and AI manifests carry their ⚠ warnings inside description and help strings; move each into aninfo_bannerrow (bannerSeverity: warning), item 2 of objectui#12610 objectstack#23839's PR stays draft until objectstack's.objectui-shacarries this reader (its note6216323521).
Generated by Claude Code
Fixes #12671
Clause-②: no (the console starts reading a slot the platform already declares,
settings.NAMESPACE.banners.BANNER_ID.{title,text}, objectstack-ai/objectstack#23886; it publishes no new export, prop or accepted key)What changes
A settings page's
info_bannerrow:useSettingsLabelgainsbannerTitle(bannerId, fallback)andbannerText(bannerId, fallback). They readsettings.NAMESPACE.banners.BANNER_ID.titleand.textthrough the hook's existingresolve(), with the same namespace discovery, the same exists-first probe and the same manifest-literal fallback asgroupTitle, and the docblock lists both paths. Theinfo_bannerarm ofSettingsFieldasks them whenspec.idis set, exactly as thegrouparm asksgroupTitle. This mirrors the platform readersresolveSettingsBannerTitle/resolveSettingsBannerTextin objectstack'spackages/spec/src/system/i18n-resolver.ts. No other spelling is read: a singularbannerbranch, anoticesbranch, abannerTextleaf and alabelleaf are each pinned as not consulted.bannerSeverity: 'warning'lays the shell's existing amber callout spelling over thedefaultAlert, with the warning triangle in amber as its icon.errorstaysdestructive;info,successand an unset severity keepdefault. No new Alert variant, andpackages/components/src/ui/is untouched. The arm'svariant as anycast is gone: the ternary already has the variant's type.bannerText: z.string().optional().describe('Markdown body (info_banner)')in objectstack'ssettings-manifest.zod.ts). It goes through the docs portal's renderer,MarkdownRendererfrom@object-ui/plugin-markdown(the oneDocPagedraws a package doc with), and its unconditionalrehype-sanitizepass, used as it is. No new dependency: the console already depends on the plugin.The bundle is read untyped through
resolve(), as thegroupshelper reads it, so nothing waits on@objectstack/specnext.3.The renderer, and why it loads lazily
SettingsFieldis in the console's eager closure:AppContentimportsWorkspaceTimezonePrompt, which imports it. A static import of the plugin would put the plugin's chunk, highlighter included, on every page load. So the arm loadsMarkdownRendererthroughReact.lazyoverimport('@object-ui/plugin-markdown'), the specifierDocPageand the registry's lazymarkdownloader already use, and the Suspense fallback is the body's own plain text.Measured with a production console build on this branch (
66556f0) against the same build with the two source files at the basee6175b5. Every dependencydist/is identical between the two, since this diff touches no package. The readings come fromapps/console/dist/eager-closure.json:e6175b5SettingsFieldchunk (eager)useSettingsLabelchunk (eager)indexchunkThe
plugin-markdownchunk is not in the eager list on either build, and the eagerSettingsFieldchunk reaches it only through a dynamicimport().node scripts/check-eager-closure-budget.mjson this branch's build:✅ Console eager closure is 3118.8 KB gzipped across 70 of 2253 chunks (budget: 3744.1 KB, headroom: 625.3 KB), exit 0.Two other renderers were considered and not used:
markdowntype throughSchemaRenderer, whichDocPreviewuses. Rejected becauseSchemaRendererevaluates a node'scontentstring as a${…}template, so the banner text would be interpreted as an expression. Its lazy placeholder is also an untranslatedLoading markdown…line.MarkdownCellRendererfrom@object-ui/fields, which is already eager and would cost zero bytes. Rejected because it is a cell renderer whose props require afield: FieldMetadatait never reads, so the banner would have to forge one.Premise checks on
origin/maine6175b5info_bannerarm drewliteralLabelandspec.bannerTextas literals and mapped onlyerrortodestructive. Thegrouparm askedlabels.groupTitle(spec.id, literalLabel).DocPage'sMarkdownRendereris the one for a trusted body (package-authored text, as a manifest's banner is), so it is the one used. The heavy-module case is avoided by loading it lazily, measured above.Alertprimitive declares onlydefaultanddestructive. The console declares a--warningtoken, but it is a 50%-lightness amber, about 2.1:1 on white, too pale for body text, and only one dot on the login page uses it. The amber callouts across the shell spell it with the amber palette, so the arm uses theborder-amber-300/60 bg-amber-50 text-amber-800spelling with its dark pair (dark:border-amber-700/50 dark:bg-amber-950/30 dark:text-amber-200), asPermissionMatrixEditor's andSuggestedBindingsPanel's notices do. The icon takestext-amber-600/dark:text-amber-400, the shell's warning-glyph amber.resolve()holds. It discovers every top-level namespace in the current language's bundle that has asettingsmember, asksi18n.exists()beforet()(objectui#11801), and returns the fallback on a miss.transformSpecTranslationscarriessettingsthrough untouched, sobannersarrives underappas served. The two helpers call that sameresolve()withbanners.BANNER_ID.title/.text.Tests
New:
apps/console/src/pages/settings/__tests__/SettingsField.infoBanner-12671.test.tsx(16 tests) andapps/console/src/pages/settings/__tests__/useSettingsLabel.banners-12671.test.tsx(4 tests). They pin:settings.NAMESPACE.banners.ID.{title,text}renders the translated pair, and a miss renders the manifest literals;id;warningdraws amber, with tailwind-merge dropping the default colours;errorstays destructive;info,successand unset stay default;Both lazy boundaries are imported at module scope in the arm suite, so no
findByraces the module loader.Ablations (each mutation landed and was restored through objectstack's
scripts/ablation-replace.mjs; every run then readgit diff HEADempty)literalLabelspec.bannerTextbanner.(singular).bodybanner.as an aliasiderrorno longerdestructivet()Runs, all on
66556f0pnpm exec vitest runover the two files givesTest Files 2 passed (2),Tests 20 passed (20).pnpm exec vitest run --maxWorkers=2 apps/console/ packages/app-shell/src/__tests__/select-placeholder-literal-11252.test.ts packages/app-shell/src/console/__tests__/bootRedirectCoverage.test.tsx scripts/__tests__/check-i18n-dead-keys.test.tsgivesTest Files 214 passed (214),Tests 2389 passed (2389).pnpm --filter @object-ui/console type-check, after the console's dependency closure build (turbo run build --filter='@object-ui/console^...', 34 tasks successful): exit 0.tsc --listFilesincludes both new test files and the plugin'sMarkdownImpl.tsx.eslinton the four changed source and test files: 0 errors. The 2 warnings are pre-existinganys outside the arm; the file had 3 before this diff.node scripts/check-changeset-presence.mjs: exit 0, 1 changeset for 1 released package.pnpm check:new-line-citations:0 new citation(s), exit 0.pnpm check:i18n-keys: exit 0.node scripts/check-test-path-roots.mjs: exit 0.node scripts/check-vi-mock-override-shape.mjs: exit 0.node scripts/check-governed-queue-guard.mjs --testover the five paths: NOT GOVERNED.pnpm lint,pnpm check, the test shards, Bundle Analysis) runs in CI.Browser reading
Chromium, on a throwaway preview page served by the console's Vite dev server. The page was not committed.
bgamber-50, text amber-800, a 60% amber-300 border and an amber-600 icon.errorkeeps the destructive title and border;infokeeps the default callout.Acceptance notes
errorthe body used to inherit the destructive red, and it now reads in the foreground colour. Measured in the browser reading above; no live producer declareserrortoday.successstays neutral. This card scopeswarninganderror, sosuccesskeeps the neutral callout.MarkdownRendererhas its own Suspense fallback, a 200 px skeleton. In the production build the plugin's index and itsMarkdownImplshare the oneplugin-markdownchunk, so that window is one resolved import. NOT MEASURED in a production browser session; observed only on the dev server, where the body showed as plain text and then as Markdown.App.tsxcomment. It saysDocPageis "the only console-owned module that reaches@object-ui/plugin-markdown". Read as static reach it still holds, and the plugin stays behind a lazy boundary (measured above).register-plugins.tsalready reached the plugin dynamically before this diff. Not edited: outside this card's file surface.Session:
https://claude.ai/code/session_01Vk15bz8xHtKGUfXfCm16h4(dispatched by thedomain:uiPM seat, seat 3).Generated by Claude Code