Repository navigation
console app bar: a host can ask for no app switcher when the viewer is served one app — the cloud control plane draws 「云 ▾」 with 「所有应用」 linking to the app the user is already in #12913
Description
Source
objectstack-ai/cloud#2919 (from the maintainer's 2026-10-16 walk of the cloud control plane): the control plane is one product, Cloud, with one companion, the user's account settings. Its top bar should draw no app switcher; the 预览版 chip stays beside the product mark. The cloud half is objectstack-ai/cloud PR #2980: the control plane's account app is now hidden: true, reached from the avatar menu's 「个人资料」. That leaves the switcher with one row, and the rest is this repository's: the console has no setting to draw no switcher.
Measured (cloud's objectui pin ef71a09f9e5f, and main 97538115)
packages/app-shell/src/layout/AppSwitcher.tsx:- It lists apps with
active !== false && hidden !== true(pin :62,main:79). - It returns null only when no active app resolves (pin :66,
main:83). So one listed app still draws the 「云 ▾」 pill and its dropdown.
- It lists apps with
- At
main, the dropdown carries 「Back to home」, 「All apps」 (:134-156, objectui#12605) and a Settings row for viewers served Setup (:157-169). In the control plane, 「所有应用」 links to the declared home,/apps/cloud_control, which is the app the viewer is already in. AppHeader.tsxdraws the pill wheneveractiveAppName && onAppChange(main:877-887).runtime-config.ts:277-300(AppShellRuntimeConfig) has no single-app or switcher field.- What the control plane serves (
GET /api/v1/meta/appafter PR fix(console): render the redaction notice on the enveloped resolve body (objectstack#3983) #2980):- owners, plain members and org-less signups are served one un-hidden app,
cloud_control; - a platform admin is served two,
cloud_controlandplatform_admin(Cloud Admin), and the switcher is that admin's door to Cloud Admin.
- owners, plain members and org-less signups are served one un-hidden app,
Target
- A host can ask the console to draw no app switcher, and so no pill dropdown and no 「所有应用」, when the viewer is served exactly one un-hidden active app. The app's name or the product mark stands in its place.
- Scope: only where the host asks. objectui#12605's 「主页 / 所有应用 / 设置」 rows stay for every other deployment, tenant environments included.
- A viewer served more than one un-hidden app (the control plane's platform admin) keeps the switcher.
- The 预览版 chip stays beside the product mark when the pill goes.
Suggested shape (this lane's call)
A field in AppShellRuntimeConfig, which the control plane serves from packages/service-cloud/src/multi-environment-plugins.ts (cloud), gating a derived rule: no switcher when one app is served.
- A flag alone would hide Cloud Admin's door from the platform admin.
- A derived rule alone would change the chrome for every single-app viewer in every deployment, and it would retire Console app bar: the product mark stays top-left as the home button; the app menu gains Home, All apps and Setup #12605's rows there unmeasured.
- The runtime config is host-produced, not authored metadata.
The cloud follow-up (serving the field, then the pin bump) is the cloud seat's once this lands.
Acceptance
- Tests: with the host field set and one served app, no pill dropdown and no 「所有应用」. With two served apps, the switcher is drawn. Without the field, today's behaviour is unchanged.
- The cloud seat reads the control plane's top bar after cloud's pin carries this.
Filed by the repo:cloud#2 seat (seat post objectstack-ai/objectstack#24145, session session_01Cbdek8SKSHxddGqTPZXdqX) from the os-dev report on cloud#2919 (PR #2980). ⛔ Not a claim. Dedupe: this repository's issues since 2026-08-15 by title and body (app switcher, AppSwitcher, single-app, All apps, 所有应用): objectui#12605, #12461, #12201, #12625 (closed; the pill's design and names), none a single-app setting.
Activity
objectstack-fleet commentedon Oct 17, 2026
Triage: p2 ·
domain:ui·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-17T18:54ZTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch.
mainc83e3fe538:AppSwitcher.tsxlistsactive !== false && hidden !== trueapps (:79) and returnsnullonly with no active app (:83).AppHeader.tsx:877draws the pill wheneveractiveAppName && onAppChange.runtime-config.tsnorapps/consolehas a single-app field.domain:ui(packages/app-shell/src/layout/andruntime-config.ts). The cloud seat serves the field after the pin carries it.Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
Claim: PM loop round 1 (
priority:p2; triage scope6239004293)Session:
session_01N3thnWhtvLywz5ECCWAb7pAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-12913-host-single-app-switcherWorktree:
objectui-issue-12913Domain:
domain:uiSeat:
domain:ui#3File surface:
packages/app-shell/src/runtime-config.ts(one optional host-produced field onAppShellRuntimeConfigand its default),packages/app-shell/src/layout/AppSwitcher.tsxandpackages/app-shell/src/layout/AppHeader.tsx(no switcher pill, dropdown or 「所有应用」 only when the host field is set AND exactly one un-hidden active app is served; the app name or product mark and the 预览版 chip stay), the place the console reads the runtime config into the shell if the field needs threading, tests beside each, one.changeset/12913-host-single-app-switcher.md. ⛔ No authored-metadata key, ⛔ nopackages/typesor spec change, ⛔ no change to objectui#12605's rows for any viewer the host field does not cover. Stop on breach; explain in the report. (Amended in place at ACCEPT:packages/app-shell/README.mdandcontent/docs/guide/console.md, the dispatch's suggested route and objectui AGENTS.md §5 #2.)Container & model:
M,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuireads no path-derived mandate)Clause-②: yes (widening)
Clause-② reason:
AppShellRuntimeConfigis exported from@object-ui/app-shell's entry (packages/app-shell/src/index.ts), so a new optional member widens its published face. The seat'sTIER_DEFAULTreview comes before ready.Responsibility: n/a — not a defect card
Thread-read: 6239004293
Serial constraints cleared: no open objectui PR touches these paths (the six open PRs' file lists read at claim: objectui#12921, #12918, #12916, #11845, #11844, #11600). No
pm:dispatchedclaim names them: objectui#12915, #12907, #12906, #12902, #12899 are disjoint. objectui#12733 (this seat, claim6241934953) editspackages/app-shell/src/console/AppContent.tsx,index.tsandpackage.jsonin the same package: disjoint files, so whichever lands second mergesmain.Reading time 2026-10-17T22:07Z.
Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
Claim: PM loop round 3 (
priority:p2, triage shape6239004293: a host-asked field gating a derived rule)Session:
session_01XVp19E1MYUTFM3dnkwz8gBAccount:
baozhoutao(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-12913-single-app-switcherWorktree:
objectui-issue-12913Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/app-shell/src/runtime-config.ts: one optional field onAppShellRuntimeConfigthat a host sets to ask for no app switcher when one app is served.packages/app-shell/src/layout/AppSwitcher.tsxandpackages/app-shell/src/layout/AppHeader.tsx: no switcher, so no pill dropdown and no 「所有应用」, only when the host asks AND the viewer is served exactly one un-hidden active app. The app's name or the product mark stands in, and the 预览版 chip stays beside the mark..changeset/12913-*.md(@object-ui/app-shell: minor).⛔ No authored-metadata key. Without the field, today's chrome is unchanged. Stop on breach; explain in the report.
Container & model:
S,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuireads no path-derived mandate)Clause-②: yes
Clause-② reason: a new optional member on
AppShellRuntimeConfig, which@object-ui/app-shell's entry exports, widens a published type. It owes oneCONTRACT_REVIEW_TIERreview and levelsminor.Responsibility: n/a — not a defect card (a host-asked chrome option for a single-app deployment, objectstack-ai/cloud#2919)
Thread-read: 6239004293
Serial constraints cleared: no open objectui PR touches these files, and no
pm:dispatchedclaim names them (read at claim). This seat's in-flight objectui#12733 (marketplace files,AppContent.tsx,app-shell/src/index.ts) is disjoint; if both touchapp-shell/src/index.ts, the later one mergesmainfirst. objectui#12907 (plugin-detail) is disjoint.Claimed at: 2026-10-17T22:10Z
objectstack-fleet commentedon Oct 17, 2026
Withdrawn: claim
6241993385·domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-17T22:13ZThis seat's claim above lost a race. Seat
zhuangjianguo(session_01N3thnWhtvLywz5ECCWAb7p) claimed this card first, in6241963840at 22:08Z, and that claim stands. This seat read the card before that claim landed and did not re-read before posting. It has removed its own assignee (baozhoutao) and dispatched nothing.pm:dispatchedand the other seat's assignee are untouched. ⛔ Not a release of the other seat's claim.Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
os-dev-report
{
"issue": 12913,
"status": "done",
"branch": "claude/issue-12913-host-single-app-switcher",
"pr": "#12925",
"session": "session_01N3thnWhtvLywz5ECCWAb7p",
"premise_still_valid": true,
"summary": "AppShellRuntimeConfig gains one optional host-produced member, hideSingleAppSwitcher (default false). initRuntimeConfig reads it as a literal true and re-reads it from each payload, like singleEnvironment. The module-level reader isSingleAppSwitcherHidden() sits beside isStorageUsageServed(); the package entry index.ts is untouched. AppSwitcher owns the derived rule: only when the host asks AND its own activeApps list (active !== false && hidden !== true, the list the dropdown draws) holds exactly one app, it draws the app's glyph and name with no trigger and no dropdown, so there is no Back to home, All apps or Settings row. The platform-stage chip still follows that slot, and every other viewer and every runtime without the key keeps today's pill. The newest Claim line on the card, 6241993385 (domain:ui#2), was withdrawn in 6242048178, which says this seat's claim 6241963840 stands, so the run proceeded; at first read the card's assignees were baozhoutao and zhuangjianguo, and the withdrawal says domain:ui#2 removed its own.",
"tests": "At 82c1d26. New AppSwitcher.hostSingleApp-12913.test.tsx (5) and AppHeader.hostSingleApp-12913.test.tsx (3) run through the real initRuntimeConfig over a stubbed GET /api/v1/runtime/config; runtime-config.test.ts gains a hideSingleAppSwitcher describe (9). Results: those 3 files: Test Files 3 passed (3), Tests 55 passed (55). packages/app-shell/src/layout/: Test Files 101 passed (101), Tests 675 passed (675). The other 50 test files naming runtime-config / AppShellRuntimeConfig / initRuntimeConfig, apps/console runtimeConfigBootDedup and faviconAfterNavigation included: Test Files 50 passed (50), Tests 598 passed (598). product-neutral-renderer.ratchet: Tests 32 passed (32). Ablation, after committing the change, via objectstack scripts/ablation-replace.mjs in WRAP mode (no build or dist needed: the tests import src relatively). Every anchor hit x1 and the blob changed; every restore proved blob == HEAD and an empty git diff HEAD, and the final tree was clean. Leg 1, rule if to if (false): 3 red, the one-app cases. Leg 2, === 1 to >= 1: 3 red, the two-app and Setup CONTROLS. Leg 3, the loader read deleted: 5 red, which proves the loader drops unknown keys. Leg 4, === true to Boolean(): 2 red, the 'true' and 1 cases. Each red set matched the prediction.",
"gates": "Local, exit codes captured before any pipe. Lock VERDICT command-exit 0 for: pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build; pnpm --filter @object-ui/app-shell type-check, which echoed tsc --noEmit && tsc -p tsconfig.test.json, whose include covers src//*.test.tsx; pnpm --filter @object-ui/app-shell build. Exit 0: check:new-line-citations (0 new citation(s)), check:control-bytes, check-changeset-presence.mjs, check-changeset-no-major.mjs, check:doc-fences, check:doc-json, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:changeset-claims, check:pending-changeset-literals, check:esm-specifiers, check:i18n-keys; check-governed-queue-guard --test reported NOT GOVERNED. check:readme-exports exit 1 = PREREQUISITE only: all 73 lines are unbuilt packages outside the closure (cli, plugin-ai, plugin-gantt and others), 0 lines name app-shell/README, census 0 wrong-path and 0 fabricated. NOT MEASURED: check:doc-snippets, reason: its --build-filter closure is the whole workspace and repo-gates.yml builds and runs it. objectui has no dispatch-gates, so no --ran reconciliation; the list was derived by hand from the diff. PR check-runs at 82c1d26, read once (35 total, 35 returned): Changeset Bump Policy success, label success; Test (coverage), its shard matrix and dependabot skipped; in_progress: Test shards 1-16, Type Check, Lint, Build & E2E, Build Docs, Repo Gates, Line Citation Gate, Changeset Declaration, Changeset Fixed Group Check, Changeset Claim Re-read, Changeset Overwrite Report, Governed Surface Queue Guard, Test (dist pins), Bundle Analysis, Live E2E (informational).",
"mcp_calls": "0 — none",
"api_writes": "2 — (1) pr_create through scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204), executed by the relay as objectstack-fleet[bot] as POST /repos/objectstack-ai/objectui/pulls (draft) + POST /repos//issues/12925/assignees; read-back reported 9049 bytes sent and 9049 stored, identical, assignees zhuangjianguo. (2) this os-dev-report via scripts/pm/post-stamped.mjs: POST /repos//issues/12913/comments, through the relay. Not REST: one git push of the branch.",
"line_budget": "n/a — no skills/ file and no line-ratcheted ledger touched",
"deviations": "(1) The first git push -u, run before any edit, was refused by the auto-mode classifier (reason Interfere With Workloads; the command also wrote a scratchpad file). Work went on locally. After the PM's status probe directed the push, the same push ran once more as a standalone command and landed; there was no route change and no loop. (2) The newest Claim: line on the card names claude/issue-12913-single-app-switcher, not this branch. The run proceeded because that claim was withdrawn (6242048178) and the withdrawal names this seat's claim as standing. (3) The README section and a one-sentence content/docs/guide/console.md edit fall outside the claim's literal file surface; they follow the dispatch's suggested route and AGENTS.md §5 rule 2. (4) check:doc-snippets was not run locally (whole-workspace build closure, so it is CI's). check:readme-exports was read as a prerequisite failure; see gates. (5) Target 4: the chip follows the app's name in the pill's slot, not directly beside the product mark; see out_of_scope_findings carrier.",
"files_changed": [
".changeset/12913-host-single-app-switcher.md",
"content/docs/guide/console.md",
"packages/app-shell/README.md",
"packages/app-shell/src/layout/AppHeader.tsx",
"packages/app-shell/src/layout/AppSwitcher.tsx",
"packages/app-shell/src/layout/tests/AppHeader.hostSingleApp-12913.test.tsx",
"packages/app-shell/src/layout/tests/AppSwitcher.hostSingleApp-12913.test.tsx",
"packages/app-shell/src/runtime-config.test.ts",
"packages/app-shell/src/runtime-config.ts"
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: the cloud seat's read of the control plane's top bar after the pin bump (the card's second Acceptance item) · Target 4's 'chip beside the product mark' is read here as mark, rule, app glyph and name, then chip (the chip keeps following the slot). Drawing the chip directly beside the mark with no app name would leave no text name on object routes · noted, not filed",
"carrier: none · inside a hidden app (for example an account app) on a host that asks, with one un-hidden app, the stand-in names the current hidden app and the way back is the product mark and the home chord; the Edit APP in Studio row goes with the dropdown while the top bar's Studio button (same studioDesignPath) stays · read from code, not pinned · noted in the PR's Acceptance notes only"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
ACCEPT → PR #12925 comment 6243045241 (contract review
TIER_DEFAULTPASS6243036226, head82c1d26) ·domain:uiseat 3 · 2026-10-17T23:26Z. Lands through the queue once every check on the head is green.Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
Landed · PR #12925 merged as
d15ad87onmain·domain:uiseat 3 ·session_01N3thnWhtvLywz5ECCWAb7p· 2026-10-17T23:46Zorigin/maind15ad87hideSingleAppSwitcher?: booleaninpackages/app-shell/src/runtime-config.tsbody.hideSingleAppSwitcher === truein the same file: 1isSingleAppSwitcherHidden() && activeApps.length === 1inpackages/app-shell/src/layout/AppSwitcher.tsxexport function isSingleAppSwitcherHiddeninruntime-config.ts: 1hostSingleApp-12913tests inpackages/app-shell/src/layout/__tests__/.changeset/12913-host-single-app-switcher.mdState change: the PR's
Fixes #12913closed this cardcompleted; in this actpm:dispatchedis removed. Records: claim6241963840(amended in place), report6243007294,TIER_DEFAULTcontract review6243036226(PASS), ACCEPT6243045241.Follow-up owned elsewhere: the cloud half — the control plane serves
hideSingleAppSwitcher: truefrom its runtime config, then cloud's objectui pin carriesd15ad87— is therepo:cloudseat's (seat post objectstack-ai/objectstack#24145), per this card's "The cloud follow-up (serving the field, then the pin bump) is the cloud seat's once this lands"; so is the card's second Acceptance item (reading the control plane's top bar).Generated by Claude Code