Uh oh!
There was an error while loading. Please reload this page.
Console Home honours features.aiStudio for the authoring front door (#5521) - #5575
Merged
Merged
Conversation
…ing front door (objectui#5521)
"Build an app" is withheld when the runtime reports features.aiStudio: false.
Hidden rather than dimmed, because that is the flag's own declared meaning on
both sides of the wire ("the SPA hides the AI authoring affordances" /
"set false to force-hide the authoring UI"). Unknown fails OPEN.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnKContributor
✅ 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
|
This was referenced Aug 21, 2026
os-sales
marked this pull request as ready for review
August 21, 2026 14:43
Uh oh!
There was an error while loading. Please reload this page.
os-sales added a commit
that referenced
this pull request
Aug 21, 2026
… two inline spellings `features.marketplace` had a documented accessor carrying the fail-open doctrine; `features.aiStudio` was read inline at two call sites in two different spellings — `ChatDock` un-chained, `HomePage` optional-chained — so neither reader could cite the doctrine and the un-chained one is a TypeError, not a fail-open, against a snapshot whose `features` is absent. That is the shape PR #5575 measured crashing 29 tests. - `isAiStudioEnabled()` sibling of `isMarketplaceEnabled()`, same docblock treatment. - Both call sites moved onto it; no inline `features.aiStudio` read remains. - The four Home suites' module mocks taught the new export (an explicit factory replaces the whole module, so an unlisted export is `undefined` at the call site). - New coverage: the accessor's fail-open doctrine, and the dock's default body under the partial snapshot the un-chained read could not survive. Part of #5577
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 freeto 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.
Part of #5521
Verified at
a84d94d8e— every gate verdict below was read from a run on thatexact tree, after the final commit.
Part of, notFixes: this card names four cards and I close one. Two werealready closed by work that landed after the card was filed, and the fourth is
carved out by the card itself pending a product ruling on cloud#1452. Details
below — the card should stay open for that fourth row.
⛔ Read this first — the declared file fence names a directory that does not contain the console home page
The dispatch fenced this card to
apps/console/**. The console home pagecomponent is not there. It is:
re-exported to the console as
DefaultHomePage(packages/app-shell/src/index.ts:216),which
apps/console/src/App.tsx:125renders. There is no home page componentanywhere under
apps/console/**, so the fence as literally written isunsatisfiable — no one could close this card inside it.
I proceeded rather than stopping, and the reasons are checkable rather than a
preference:
changeset". I changed exactly that: one component, one new test file, one
changeset. Nothing widened; the shape of the declared surface is honoured
exactly, only the path differs.
home page file, deliberately … Locate the home component yourself and
re-derive; do not trust a path from me here."
path, because the dispatcher's serial-constraint clearance covered
apps/console/**and is void here. No open PR touchespackages/app-shell/src/console/home/: the only open PRs are Pin @objectstack/* to 17.1.0 #5529 (the17.1.0 pin, draft), chore: release packages #5400 (the changesets release PR) and two dependabot
bumps. The prior change to this exact file, Console: OS_CLOUD_URL=off is a disabled state, not a red load failure #5517 (objectui#5504), is merged.
Flagging rather than silently absorbing:
@object-ui/app-shellis a publishedlibrary, not an app, so this is a
minoron a package the whole workspaceconsumes — a materially different risk class from the
apps/consolediff thefence anticipated. #5517 made the identical class of change in the identical
file, so this is the normal route, but the reviewer should know the fence moved.
Premise re-verification: 2 of the card's 4 rows are already fixed
Checked against
origin/mainatac73c24b0, not relayed from the card. Thecard's headline claim — "two of them contradict a flag the server already sends
as false", called "the sharpest form of this bug" — is no longer true. Those
two were closed by #5517 (objectui#5504), which landed after this card was filed
and which the card itself named as an in-flight sibling to sequence behind.
ac73c24b0features.marketplaceunread — "a missed read"marketplaceEnabledfeatures.installLocal/marketplaceunreaddisabled={!canAuthorMetadata || !marketplaceEnabled}+ a visible localized reasonfeatures.aiStudioand/orstudio.accessunreadmanage_metadata, which fails openSo one row survives, and this PR closes it.
The fix
HomePagenow readsfeatures.aiStudio— a flag the server was alreadysending and this page was not consuming. No new authorable config key (the
ruling forbids minting one, and none was needed), no new server surface, no new
i18n copy, and no locale table touched.
Why HIDDEN rather than dimmed
This was left to my judgment with a stated reason required. The reason is the
flag's own declared meaning, on both sides of the wire, rather than a
presentation preference:
RuntimeFeatures.aiStudio(packages/app-shell/src/runtime-config.ts:28):"When false, the SPA hides the AI authoring affordances."
authoring UI."
Honouring what the producer declares its flag to mean is the whole point of
reading it. Substituting a dimmed card would be re-deciding the contract at the
consumer.
It is also the honest answer here, and the distinction is the one #5533/#5557
are about — reporting the wrong kind of answer:
!canAuthorMetadatais a fact about this principal. A dimmed card plus areason line tells an admin something actionable about their own account. That
case correctly stays disabled-with-reason; I did not touch it.
aiStudio: falseis a fact about the deployment. Authoring exists fornobody there — no permission to acquire, no admin to ask. A permanently greyed
front door advertises a room that was never built, which is exactly Marketplace catalog page tells a NON-ADMIN "access denied" on a runtime that has no marketplace at all #5557's
"access denied for a surface that exists for nobody", applied to a card
instead of a page.
Consistent with that, no reason line is rendered in its place.
home.build.noCapabilitysays the account lacks "Manage Metadata"; printingit to an admin who holds it, on a runtime that has no authoring at all, would be
the same misdirection.
What was deliberately NOT swept in
metadata authoring, it answers to its own flags, and Console:
OS_CLOUD_URL=offrenders the marketplace as a red load-failure with wrong guidance, and the home page still promotes the marketplace entry #5504 already ruleddisable-and-explain for it. Its flag means reachability; this one means
force-hide. Two flags, two declared meanings, two presentations. A test pins
that the sibling still stands.
cloud#1452. Not folded in.
HomeAiActions' "Build with AI" — already gated on the live agentcatalog (
buildAvailable), andsurfaceAgent.tsalready downgrades build→askon
aiStudioEnabled: false. Correct as-is./api/v1/meta/*and the 404s are the other halfof the criterion and are not softened by anything here.
studio.access— the half I did NOT do, and why it is a separate cardThe dispatch asked me to reuse
holdsStudioAccessfromapps/console/src/components/studioEntry.ts, per #5563's precedent. Thatimport is impossible in the direction required:
HomePagelives in@object-ui/app-shell, andapps/consoledepends on app-shell, not thereverse. app-shell's manifest lists no console dependency and could not without
a cycle.
The three ways out are all worse than filing it:
against ("one definition of 'is this principal a platform operator'");
@object-ui/permissionsand re-export fromstudioEntry—correct, but touches three packages and is nobody's declared surface here;
It is also less urgent than it looks: the card's table offers
features.aiStudioand/or
studio.access,/studiois already hard-gated fail-closed byStudioRoute+useStudioEntry, and coupling a library's home page to theconsole's route policy is a design question, not a mechanical edit. Filed
with the dependency evidence; the residual it leaves is narrow — a runtime with
aiStudio: truewhose permissions endpoint fails open, where the card stays liveand the click bounces at
StudioRoute.Tests, and how the vacuum is guarded
HomePage.aiStudioDisabled.test.tsx, 7 cases. The acceptance condition is thatsomething does not render, which an empty page reproduces perfectly, so:
tile the strip renders (
app-tile-crm) and the sibling template cover;flag flipped and find the card;
manage_metadata, so the principal half ofthe gate is wide open and only the deployment flag can move the verdict —
without that, every denial would pass for the wrong reason.
An empty render fails the denials' counter-assertions; a gate stuck open fails
the denials; a gate that swept the whole cover fails the sibling assertion. None
of the three can pass alone.
Every case boots the genuine
initRuntimeConfig()over a stubbedGET /api/v1/runtime/configrather than mocking an accessor — mocking it wouldpass against a page that never asks the server anything. The
featuresfixtureis a spread partial, not named booleans, because the fail-open case turns on
absent vs false and a
aiStudio: booleansignature could not express it.A real defect the counter-probes caught
The first draft read
getRuntimeConfig().features.aiStudio. That crashed 29tests across 4 neighbouring suites: four sibling suites mock the module as
getRuntimeConfig: () => ({ branding: … })with nofeatureskey at all, sofeaturesis genuinelyundefinedon real code paths. The shipped read isfeatures?.aiStudio !== false, copied fromisMarketplaceEnabled()'s own bodyrather than invented — both the optional chain and the
!== falseareload-bearing, and an absent flag now fails open.
Ablation
Pure source — and the reason is checkable, not assumed: the root
vitest.config.mtsresolve.aliasmaps every@object-ui/*specifier at thatpackage's
src(including@object-ui/app-shell), and the suite imports../HomePagerelatively. No dist artifact sits between the edit and the run, sothis cannot go falsely green on a stale build.
The prediction was written to disk before the run, naming both the count and
the individual cases, so it was falsifiable in both directions: 2 red / 61
green; more than 2 means the gate sweeps something it should not, fewer than 2
means a case passes vacuously.
The mutation ablates the read itself —
getRuntimeConfig().features?.aiStudio !== false→true, i.e. exactly themissed read this card is about. It was proven on disk before the measurement,
anchored on the text in both directions: the real expression
1 → 0, the mutant0 → 1, withgit diff --statshowing the single-line change. An editor exitcode proves nothing — a zero-hit replace exits 0 with the file untouched — so the
script aborts with readings VOID unless both counts land.
Observed: exactly 2 red / 61 green, and the two are the two named cases —
withholds the "Build an app" cover entirelyandwithholds authoring while keeping objectui#5504's explained marketplace state. Prediction held in bothdirection and count. The script carried
trap … EXIT INT TERM; the tree cameback byte-identical (
git status --shortandgit diff --statboth empty).Gates run locally at
a84d94d8eExit codes captured before any pipe, verdicts quoted from each gate's own
output line.
packages/app-shell/src/console/homevitestTest Files 10 passed (10)·Tests 63 passed (63)@object-ui/app-shell type-checktsc --noEmit && tsc -p tsconfig.test.jsonechoed — not a zero-match pass)0 errors(7 pre-existingno-explicit-anywarnings)check-control-bytes✅ OK (scanned 4630 tracked text file(s); skipped 85 binary)check-changeset-presence✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)check-changeset-no-major✅ No changeset declares a major bump.check-changeset-fixed✅ All workspace packages are in the changeset fixed group.check-i18n-call-site-keys2505 call sites comparedcheck-i18n-en-driftNo en value changed in this range.check-i18n-dead-keysThe i18n gates are green and vacuous by construction: this diff adds no
t()call site and removes none — the card is gated at the JSX, sohome.build.title/.subtitleremain live call sites for every runtime that hasAI Studio. No locale pack is touched.
The test run is a declared narrowing, stated so it is checkable rather than
buried. I ran the
console/homedirectory (10 files / 63 tests), not the whole@object-ui/app-shellpackage. Two reasons, one of them a live incident:git grepforDefaultHomePage|from '.*HomePage'acrosspackages/andapps/returnsexactly four hits outside
console/home/:apps/console/src/App.tsx(therender site) and two console suites that stub it
(
DefaultHomePage: stub('home-page')). Nothing else mounts this component, sono suite outside the directory can observe the change.
shared container's verify lock for 650 s without emitting a single test
marker (still transforming) and had a sibling agent's job queued behind it.
That is the long-holder pathology
os-verify-lock.shexists to surface, for afarm CI runs on every push regardless. I stopped it by task id — never by
process name, which would have taken the sibling's vitest with it — and the
lock passed straight to the queued job.
Lint is a declared narrowing, and it is sound rather than merely cheap: the
population came from ESLint's own config resolution over the changed files (2
files, count read from
--format json), andeslint.config.jsdeclares noprojectServiceand noparserOptions.project— grep forprojectService|parserOptions|project:returns no match — so linting is nottype-aware and a 2-file diff cannot move the verdict of a file it does not
contain. All 6
HomePage.tsxwarnings sit at lines 185–311, ahead of my firstadded block at line 382; the one warning in the new test is the
(app: any)mock signature copied verbatim from the sibling suite. The repo-wide
pnpm lintand
pnpm type-checkfarms are CI's run and are not duplicated here.Scope
Exactly 3 files:
HomePage.tsx, its new test, and a changeset — the declaredshape, at the path the component actually lives at (see the fence note above).
runtime-config.tswas left read-only. Reading the flag inline rather thanadding an
isAiStudioEnabled()sibling toisMarketplaceEnabled()is adeliberate trade recorded in a code comment: the accessor is the better
factoring, but adding it would mean editing four neighbouring suites' module
mocks to teach them the new export. Filed as a follow-up rather than ridden in
here.
Generated by Claude Code