Repository navigation
feat(mobile, app-shell): open the mobile shell's native connection switcher from the header organization button - #29565
Conversation
…oved in the mobile shell The header organization button opens the shell's native switcher when the bridge announces showConnectionSwitcher; useNativeHostRouting answers the shell's connectionRemoved event through event.waitUntil. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jTVYu9vDSx28X2zi7h
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jTVYu9vDSx28X2zi7h
…-g0magq # Conflicts: # ui/packages/mobile/src/__tests__/nativeHost.test.tsx
…Removed answer Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jTVYu9vDSx28X2zi7h
…-g0magq # Conflicts: # ui/packages/mobile/src/__tests__/nativeHost.test.tsx # ui/packages/mobile/src/index.ts # ui/packages/mobile/src/nativeHost.ts
… too In the mobile shell the sheet lists the shell's connections across company servers, so the button no longer waits for a second membership on this server; the read-only organization indicator steps aside so the header draws the organization once. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jTVYu9vDSx28X2zi7h
|
Contract-review-claim: session_01QsR2jV5sBSzCYNEDgpYLJw · head c962ab9 · 2026-11-02T19:19Z Generated by Claude Code |
|
director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw Contract reviewServed-tier: Read-only; nothing was built or run here. CI on ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Must-fix (1):
Should-fix (1): the os-dev report on #29458 names Implemented-by: VERDICT: FAIL Generated by Claude Code |
|
director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw Contract reviewServed-tier: Re-review of the one must-fix in record 6586314740. Read-only; nothing was built or run here. The head is the same commit, so ① (widening list, dependency, ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
No must-fix. No should-fix. Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:ui#5 · zhuangjianguo · session_01QsR2jQjvWkZ7is6GXbC6DB 席位核对 — ACCEPT ·
|
domain:ui#5 · zhuangjianguo · session_01QsR2jQjvWkZ7is6GXbC6DB
Requested by Steedos · project thread
Fixes #29458
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
ui/packages/app-shell/src/layout/WorkspaceSwitcher.tsx(按钮分支,位于单组织判断之前)、ui/packages/app-shell/src/layout/CurrentOrganizationIndicator.tsx(App 内让位)、ui/packages/mobile/src/useNativeHost.ts(connectionRemoved 应答)。pnpm --dir ui exec vitest run packages/app-shell/src/layout/__tests__/WorkspaceSwitcher.nativeConnectionSwitcher.test.tsx packages/mobile/src/__tests__/nativeHost.test.tsx,应全部通过(7 + 64)。What changes
The bridge contract is the mobile repository's
docs/HYBRID-SHELL.md(objectstack-ai/mobile#173, merged at678fbe9). This session cannot read that repository, so the contract below is taken from the card's quoted text:The header organization button is
WorkspaceSwitcher(data-testid="workspace-switcher"), whichAppHeaderdraws at every width (the badge alone belowsm).CurrentOrganizationIndicatordraws no control by design, andOrgBadgeis only the mark, so neither is the button.WorkspaceSwitcher: whennativeHostCan(useNativeHost(), 'showConnectionSwitcher'), the trigger is a plain button that callsnativeShowConnectionSwitcher()and no dropdown is mounted. In the shell, with the capability, the button renders whenever there is an active organization, at any membership count: the native branch sits after the no-active-organization guard and before the single-membership guard.orgListcounts this server's memberships only, while the sheet lists the shell's saved connections across company servers, so a user with one organization on each of two servers has two connections. Without the capability (plain browser, older shell) every rule is unchanged, the single-membership guard included. Afailedanswer from the shell is logged atwarn.CurrentOrganizationIndicatorrenders nothing when the shell announces the capability, so a walled deployment at one membership draws the organization once, on the native button. The name shows at the same widths the indicator showed it (badge always, name fromsmup, the samehidden truncate sm:inlinespan); a test pins that equality.useNativeHostRouting(already mounted by the Console'sNativeShellRouting) listens forobjectstack-native:connectionRemovedwhile the bridge is present and callsevent.waitUntil()with the promise its new optionalonConnectionRemovedreturns. When the option is omitted, as the Console does today, the promise is already resolved. The latest handler is read through a ref, so the listener is installed once per bridge.@object-ui/app-shellnow depends on@object-ui/mobile(which depends only on@object-ui/types; no cycle). Vitest and the Console's Vite config already alias@object-ui/mobiletosrc.Which unregister case holds: no registration exists, so there is nothing to unregister. No Console path registers a device: the client SDK's device methods were removed (its
notificationsnamespace comment says the/notifications/devicesroutes were never built), the server routes are #29041 (pm:blocked), and the registration card is #29042 (pm:blocked). So the Console has no token and no device id. The handler answerswaitUntilwith a resolved promise, andonConnectionRemovedis the seam where #29042 adds the unregister call. No registration is invented here.Widening (
@object-ui/mobile, all new exports):'showConnectionSwitcher'joinsNativeHostMethod, andshowConnectionSwitcher?(): Promise<null>joinsObjectStackNativeas an optional method, the wayscan?is: an older shell does not carry it.nativeShowConnectionSwitcher(): the typed wrapper, sameunavailable | ok | failedanswer as the others.nativeHostCan(host, method): new export. It is the wrappers' own feature-detect (the former privatecallable), so a control can pick its native or web path before it renders.NATIVE_CONNECTION_REMOVED_EVENTand theNativeConnectionRemovedEventtype.UseNativeHostRoutingOptions.onConnectionRemoved.No server change, no metadata key, no new i18n key.
Tests
WorkspaceSwitcher.nativeConnectionSwitcher.test.tsx(7 cases, real Radix dropdown and a real click).showConnectionSwitcheronce, with nomenurole, no organization row and nothing switched. Two controls, with no host and with a host without the capability: the web menu opens with its rows andshowConnectionSwitcheris never called.isolated) deployment,WorkspaceSwitcherandCurrentOrganizationIndicatorrendered side by side asAppHeaderdraws them. With the capability, the native button renders and opens the sheet, the indicator is absent, and the organization name is drawn once. The native button's name span carries the same classes as the indicator's. Two controls, with no host and with a host without the capability: no switcher and the read-only indicator, as today.nativeHost.test.tsxextended:showConnectionSwitcherjoins the unavailable / missing-method / ok wrapper rows;nativeHostCancases; aconnectionRemovedblock. With no unregister step,waitUntilgets a promise that resolves at once. With one, the shell is held until the step settles and the step runs once. The latest handler runs without a second listener. An event withoutwaitUntilis left alone. Outside shell mode no listener is installed. The listener is removed on unmount.Acceptance notes
Verification
Final head
c962ab9e. Its last commit changes three code files againstdec7cd0b:WorkspaceSwitcher.tsx,CurrentOrganizationIndicator.tsxand the new test file. It also updates the changeset text. All CI check-runs onc962ab9econcludedsuccessorskipped: Test UI (1/16 … 16/16), Type Check UI, Lint UI, Build & E2E UI, Live E2E UI, Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard and Check Changeset. Before that, main was merged three times. Two of those merges brought in #29475 and the shell code-scanner card, which had editednativeHost.ts/useNativeHost.ts; their conflicts were import and union lines only.showConnectionSwitcheris optional onObjectStackNative, matchingscan?.Tests, ablation and type-check
On
c962ab9e:pnpm --dir ui exec vitest run --maxWorkers=2 packages/mobile/+WorkspaceSwitcher.nativeConnectionSwitcher,WorkspaceSwitcher,WorkspaceSwitcher.noActiveOrg-12698,WorkspaceSwitcher.organizationNoun-12851,CurrentOrganizationIndicator,CurrentOrganizationIndicator.noActiveOrg-12698: Test Files 15 passed, Tests 168 passed.ablation-replace.mjs, each restored to the HEAD blob withgit diff HEADempty; vitest aliases@object-ui/*tosrc, so no rebuild is involved):if (nativeSwitcher) {toif (nativeSwitcher && false) {inWorkspaceSwitcher.tsx: 3 failed, 4 passed (every capability case red, every control green).if (nativeSwitcher) {toif (nativeSwitcher && orgList.length > 1) {(the old placement after the single-membership guard): 2 failed, 5 passed (the one-membership capability cases).if (nativeSwitcher) return null;toif (nativeSwitcher && false) return null;inCurrentOrganizationIndicator.tsx: 2 failed, 5 passed.turbo run type-check --filter=@object-ui/app-shell: 65/65 tasks.eslinton the three changed files: exit 0.On
33a962e6(the bridge half, unchanged since): the mobile + native-shell console suites 18 files / 181 tests; theconnectionRemovedablation (thewaitUntilguard forced to return) red 3 cases and restored;turbo run type-checkfor mobile + app-shell 66/66;pnpm check:new-line-citations0 new.Derived gates
node scripts/pm/dispatch-gates.mjs --commandsatc962ab9ederived the same 54 families as on33a962e6. Onc962ab9ethe 53 non-vitest families were run with their exit codes recorded before any pipe, pluspnpm check:ci-filter-parityat the seat's request:node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0node scripts/check-adr-0087-registration.mjs --self-test: exit 0node scripts/check-changeset-no-major.mjs --base origin/main: exit 0node scripts/check-closing-keyword-parity.mjs: exit 0node scripts/check-closing-keyword-parity.mjs --self-test: exit 0node scripts/check-comment-mask-corpus.mjs: exit 0node scripts/check-dts-emitted.mjs --self-test: exit 0node scripts/check-empty-changeset.mjs --base origin/main: exit 0node scripts/check-empty-changeset.mjs --self-test: exit 0node scripts/check-osv-exemptions.mjs: exit 0node scripts/check-osv-exemptions.mjs --self-test: exit 0node scripts/check-sdui-manifest.mjs: exit 0node scripts/check-sdui-manifest.mjs --self-test: exit 0node scripts/ci/assert-turbo-remote-cache.mjs --self-test: exit 0node scripts/ci/plan-test-core-shards.mjs --self-test: exit 0node scripts/ci/prune-turbo-cache.mjs --self-test: exit 0node scripts/osv-base-relative.mjs --self-test: exit 0node scripts/pm/release-rehearsal-clone.mjs --self-test: exit 0node scripts/release-pending-publish.mjs --self-test: exit 0pnpm --filter @objectstack/spec run check:duration-unit-keys: exit 0pnpm --filter @objectstack/spec run check:llms-txt: exit 0pnpm check:changeset-fixed: exit 0pnpm check:changeset-gate-self-tests: exit 0pnpm check:closed-ceremony-text: exit 0pnpm check:cross-package-test-inputs: exit 0pnpm check:dispatcher-error-vocabulary: exit 0pnpm check:driver-memory-census: exit 0pnpm check:dts-closure: exit 0pnpm check:dual-build-cjs-loads: exit 3pnpm check:engine-double-contract: exit 0pnpm check:gitlink-declared: exit 0pnpm check:lean-entry-closure: exit 0pnpm check:manifest-repository-directory: exit 0pnpm check:merge-driver: exit 0pnpm check:nul-bytes: exit 0pnpm check:override-consistency: exit 0pnpm check:pm-changeset-deadline-census: exit 0pnpm check:pnpm-flag-spellings: exit 0pnpm check:published-files: exit 0pnpm check:query-options-erasure: exit 0pnpm check:refd-timer-probe: exit 0pnpm check:skill-identifier-liveness: exit 0pnpm check:sourcemap-no-sources-content: exit 0pnpm check:test-source-alias: exit 0pnpm check:tier-file-adoption: exit 0pnpm check:turbo-task-graph: exit 0pnpm check:type-check-coverage: exit 0pnpm check:type-check-debt: exit 0pnpm check:undeclared-dep-imports: exit 0pnpm check:vendor-export-contract-resolve: exit 0pnpm check:watch-hint-literal: exit 0pnpm check:where-matcher: exit 0pnpm check:workspace-manifest-cycles: exit 0pnpm check:ci-filter-parity: exit 0check:dual-build-cjs-loadsexited 3 onc962ab9e(PREREQUISITE NOT MET: the re-created worktree had no built dists). It is NOT MEASURED locally there. It measured exit 0 on33a962e6(115 require entry points across 70 packages), and CI's Lint & Repo Gates onc962ab9eis green.pnpm --dir ui exec vitest run scripts/was not re-run locally onc962ab9e; the CI runs listed above for that head are green. It ran green locally on33a962e6(169 files) and ondec7cd0b(169 files, pluscheck-doc-json-fences.test.ts42/42 after a stale-build rebuild).Generated by Claude Code