Skip to content

fix(app-shell): Home, the app switcher and launcher blocks offer only the apps /me/apps lists, so a tabPermissions-hidden app draws no tile - #27021

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-8n1lrs
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-8n1lrs

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Fixes #26838

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:首页磁贴、应用切换器菜单和 app:launcher 页面块都按当前用户的 GET /me/apps 结果收窄应用列表,权限集里 tabPermissions: hidden 的应用不再出现。命令面板的应用列表和不带应用段的默认落地也按 /me/apps 收窄。
  • 为什么改:隐藏只在 /me/apps 生效,而这三个入口都读 /meta/app(卡上说切换器已正确,实测它也读 /meta/app),成员仍能看到并点开被管理员隐藏的应用。
  • 风险与回滚:无安全插件时 /me/apps 答 404、或读取失败,一律回退到原来的完整列表;首页多等一次 /me/apps 首读。回滚即 revert 本 PR。
  • 先看:ui/packages/app-shell/src/hooks/useLauncherApps.tsx(收窄与回退规则)、ui/apps/console/src/AppContent.tsx(唯一挂载点)、ui/packages/app-shell/src/console/home/HomePage.tsx(首页等待首读)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/console/home/__tests__/HomePage.launcherHiddenApps-26838.test.tsx,预期 4 个用例通过,被隐藏的 Showcase 不出磁贴。
  • 需要你做的:无,席位落地。

What changes

  • New ui/packages/app-shell/src/hooks/useLauncherApps.tsx:
    • LauncherAppsProvider reads GET /api/v1/me/apps once per signed-in user, and again only when the active /meta/app name set gains a name the held answer was not asked about (an install or a publish mid-session).
    • useLauncherApps(apps) returns filterActiveApps(apps) minus every app the answer was asked about and did not list. The entries stay /meta/app's own objects (labels, icons, _packageId, publish state); /me/apps contributes names only.
    • No provider, a 404 (no security plugin serves the route) or a failed read narrows nothing. A failure other than 404 logs one console.warn; a 404 is not asked again for that user.
    • pending is true only while the first read is out.
  • ui/apps/console/src/AppContent.tsx: ConsolePermissionsScope, the one common ancestor of /home and /apps/:appName/*, mounts LauncherAppsProvider inside MePermissionsProvider, with the same server URL and authenticated fetch. The seat extended the claim's file surface to this file (amendment 6457698059).
  • HomePage: the tiles and the System Settings link come from useLauncherApps; the page's loading gate also waits for the first read, so a hidden tile is never drawn and then removed. The host-app resolution and the recent rows' app labels keep the full active list, because a hidden app still opens.
  • AppSwitcher: the menu lists useLauncherApps(apps). The pill still names a hidden app while the user is in it (its existing fallback over all active apps).
  • app:launcher block (views/app-launcher-renderer.tsx): its openable set is useLauncherApps(apps); it still issues no request of its own.
  • LauncherAppsProvider and useLauncherApps are exported from @object-ui/app-shell.
  • useServedApps(apps) (exported): the /me/apps narrowing alone, no active/hidden filter. CommandPalette lists useServedApps(apps.filter(active !== false)), so its own hidden: true behaviour is unchanged.
  • app-shell console/AppContent.tsx: the default-app landing (a URL naming no or an unknown app) picks from useLauncherApps(apps); the route lookup still finds a hidden app, which keeps opening from its URL.
  • Changeset .changeset/26838-launcher-hidden-tabs.md: @object-ui/app-shell and @object-ui/console patch.

Not changed: no server change, no route guard (a hidden app keeps opening from its URL), default_off untouched, console/appObjectScope.ts untouched.

Why a provider, not a hook that fetches

No @objectstack/client method reads /me/apps, so the read is a direct authenticated fetch. The ui network-escape guard (ui/vitest.setup.network-escape-guard.ts) reds any test whose render reaches a relative fetch, and about 130 test files render HomePage, AppSwitcher, AppHeader or HomeLayout. An explicitly mounted provider, the MePermissionsProvider pattern, keeps those tests unchanged: without it a surface reads the unnarrowed list, exactly as before.

Tests

  • hooks/__tests__/useLauncherApps-26838.test.tsx (8 cases): narrows by the answer and keeps the /meta/app entries; no provider narrows nothing; 404 narrows nothing, silently, and is not re-asked; 500 narrows nothing and warns once; pending only for the first read; an app added after the read passes and triggers one re-read; two consumers share one read; no user, no read.
  • console/home/__tests__/HomePage.launcherHiddenApps-26838.test.tsx (4 cases): /meta/app = [setup, crm, showcase_app] and /me/apps = [setup, crm] draws no Showcase tile; a set hiding Setup removes the System Settings link; the 404 control draws every tile; the loading line holds while the first read is out.
  • layout/__tests__/AppSwitcher.launcherHiddenApps-26838.test.tsx (3 cases): the hidden app is not in the menu; the 404 control lists it; the pill still names it when the user is in it.
  • chrome/CommandPalette.launcherHiddenApps-26838.test.tsx (2 cases): the hidden app is not offered; the 404 control offers it.
  • console/__tests__/AppContent.defaultLandingHiddenApp-26838.test.tsx (3 cases): the declared default crm hidden by /me/apps = [sales] makes an unknown-app URL land on sales; the 404 control keeps crm; a URL naming crm still opens it.
Ablations (one-time; restored, blob equals HEAD each time)

Each leg ran under the lock through node scripts/ablation-replace.mjs (anchor hit 1 -> 0, restored blob equals HEAD, git diff HEAD empty), on commit 9f4f6f31:

  • HomePage.tsx: the tiles from activeApps instead of launcher.apps -> HomePage.launcherHiddenApps-26838: 2 failed / 2 passed (the hidden-tile case and the loading-hold case go red).
  • useLauncherApps.tsx: the narrowing predicate replaced by return true -> hook, Home and switcher files: 7 cases red across the three files.
  • AppSwitcher.tsx: the menu from the old inline active/hidden filter -> AppSwitcher.launcherHiddenApps-26838: 2 failed / 1 passed.
  • Round 2, on bb175bf7: CommandPalette useServedApps(...) -> the plain active filter: 1 failed / 1 passed; app-shell console/AppContent useLauncherApps(apps) -> the inline active/hidden filter: 1 failed / 2 passed.

Acceptance notes

  • CommandPalette and default-app landing: fixed in this PR at the seat's REWORK (6458597628).
  • Card premise correction: the card says the app switcher reads /me/apps and hides the app. On origin/main no file under ui/ read /me/apps; the switcher read /meta/app like Home. Both are fixed here.
  • Observation, not filed: GET /me/apps answers 200 { apps: [] } when its handler throws (the outer catch in current-user-endpoints.ts). This client cannot tell that from "every app is hidden", so on that answer Home draws no app tiles and no System Settings link (only the page frame), the switcher menu lists no app, and the palette offers no app; a hidden-by-throw state lasts until the next read (a new user or a new app name). Read-only inference from source; not reproduced.
  • Home now waits for the first /me/apps read alongside /meta/app; the read starts once the app list has loaded, so it adds one request round trip before the tiles draw on a cold load.

Verification

Round 2 on head 7b20f2d5 (8cc708ab merged origin/main acc9c57d; 7b20f2d5 changes one test file): affected set 308 files / 2514 tests passed on 8cc708ab; dispatch-gates --commands 34/34 exit 0 and --ran 0 NOT-MEASURED on 8cc708ab; app-shell package lint 0 errors; CI on 7b20f2d5 green (43 success, 10 skipped). Round 1 readings below.

Round 1 on head cbe8d370 (origin/main merged in), app-shell closure built first (7m33s under the lock).

  • New tests: 3 files, 15/15 passed.
  • Affected set: every app-shell and console test file that names HomePage, AppSwitcher, AppHeader, HomeLayout, ConsoleLayout, AppContent, the launcher block or the hook, plus all of console/home/__tests__ and layout/__tests__: 284 files, 2359 tests passed (on 9f4f6f31, before the merge of origin/main, which touched no file under these paths).
  • pnpm --filter @object-ui/app-shell run type-check: exit 0. pnpm --filter @object-ui/console run type-check: exit 0 (after --filter '@object-ui/console^...' build).
  • node scripts/pm/dispatch-gates.mjs --commands: 34 derived commands, each exit 0; --ran reconciliation: 34 derived, 34 run, 0 NOT-MEASURED. Includes node scripts/check-sdui-manifest.mjs (exit 0) and pnpm --dir ui exec vitest run scripts/ (exit 0).
  • Also run: pnpm --dir ui run check:spec-symbols, check:new-line-citations, check:test-path-roots: exit 0.
  • CI owns the repo lint and the remaining type-check lanes.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (CI runs the repo lint)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…me/apps answer

Home's tiles, the app switcher menu and the app:launcher block listed
GET /meta/app, which still serves an app a permission set hides
(tabPermissions: hidden); the hide is applied only in GET /me/apps.
A LauncherAppsProvider, mounted in the console's ConsolePermissionsScope,
reads /me/apps once per user and useLauncherApps narrows each surface's
list by its app names. No provider, a 404 or a failed read narrows
nothing.

Claude-Session: https://claude.ai/code/session_01JQwxbYwBbR8jCpWPE5Kosz
Co-authored-by: Claude <noreply@anthropic.com>
… and the switcher

Claude-Session: https://claude.ai/code/session_01JQwxbYwBbR8jCpWPE5Kosz
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

REWORK · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · head cbe8d3706b · 2026-10-28T01:57Z

One patch round, same claim and branch. The diff reviewed (useLauncherApps.tsx, the Home, switcher and launcher-block wiring, the ConsolePermissionsScope mount) is sound: one /me/apps read per user, a 404 or failure falls back to the unnarrowed list, and an answer already held survives a later failure.

  1. Card Expected not met in full. The card says "Every launcher surface reads the same filtered list (/me/apps)." The PR's own Acceptance notes name two launcher surfaces it leaves on the unfiltered list: the command palette's app list (console/home/HomeLayout.tsx passes apps ?? [], and the in-app palette likewise) and the default-app landing (ui/packages/app-shell/src/console/AppContent.tsx launcherApps ≈L340, used ≈L375 when the URL names no app). Both draw from the same hook; with Fixes #26838 the card would close with them still showing a tab-hidden app. Wire both through useLauncherApps, with one test each and an ablation. The claim surface was widened for this (amendment on the card).
  2. Keep as an Acceptance note: the /me/apps 200-empty-on-throw reading. State in the PR body that, with this PR, that server answer would show Home with no tiles, and that it was not reproduced.

Everything else stands; re-run the derived gates on the new head and update the report.

… an app /me/apps hides

The palette's "Switch app" group (Home and in-app) narrows its active apps
by the same /me/apps answer through a new useServedApps, keeping its own
rule that lists hidden apps; AppContent's default-app fallback for an
unresolved segment reads useLauncherApps. One test and one ablation each.

Claude-Session: https://claude.ai/code/session_01JQwxbYwBbR8jCpWPE5Kosz
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl and removed size/l labels Oct 28, 2026
…, not a reassigned outer variable

Lint UI refused the hook test's `seen = useLauncherApps(...)` under
react-hooks/globals; the probe now hands its entries to a vi.fn from an effect.

Claude-Session: https://claude.ai/code/session_01JQwxbYwBbR8jCpWPE5Kosz
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · head 7b20f2d51d · 2026-10-28T02:59Z · supersedes REWORK 6458597628

席位意见:返工补齐了卡上"每个启动入口都读同一份 /me/apps 过滤结果",命令面板和默认落地也收窄;隐藏的应用仍能从 URL 打开,/me/apps 404 或失败时一律不收窄。

  1. Shape: draft, base main; Fixes #26838 now holds, because every launcher surface the card names reads the narrowed list. 15 files, +1169/−18; not governed (check-governed-merges --pr 27021); ≤ 3000 lines.
  2. REWORK point 1 measured: seat read of the round-2 diff (bb175bf7). CommandPalette lists useServedApps(apps.filter(active !== false)), which applies only the /me/apps narrowing, so the palette keeps listing hidden: true apps as before. The app-shell console/AppContent.tsx default landing picks from useLauncherApps(apps), while the route lookup still resolves a hidden app by URL. useLauncherApps and useServedApps share one narrowByAnswer, and its behaviour is unchanged (no provider, no answer, or a 404 narrows nothing).
  3. REWORK point 2: the /me/apps 200-empty-on-throw reading stays an Acceptance note and now says what that answer does to Home, the switcher and the palette. It is not reproduced.
  4. Gate check-runs (pinned to head 7b20f2d51d): 53 runs: 43 success, 10 skipped (the core / docs / dogfood path-filtered jobs a ui/-only diff does not trigger), 0 failed. Dev's reading: 34/34 derived families exit 0 and --ran 0 NOT-MEASURED on 8cc708ab; 7b20f2d5 changes one test file (a Lint UI fix). Affected set: 308 files / 2514 tests.
  5. Accepted deviations: the surface amendments 6457698059 and 6458593027 (the seat's own), and the @object-ui/console changeset (the console is public and ships the app-shell code). The relay refuses an edit to the dev's own report comment, so the round-2 report is in the project file issue-26838/os-dev-report-updated.md. The seat applied its body_amendments to this PR body through the relay (issue_patch, read back identical).
  6. Landing clause: not governed. Every check on the current head is green, so the seat lands it (pr_ready + automerge_enable), or asks the maintainer to click if the classifier refuses.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 40654802071 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test UI (13/16) — 失败步骤: Run the ui tests (shard 13/16)

    × zh-CN: the header reads the served 销售代表, and a save keeps the authored label 1117ms
    × ja-JP: the header reads the label the served row carries 1049ms
    × a set the published list does not hold keeps its draft label 1046ms
    × an edit to the label shows the edit, not the served label 1027ms
    FAIL   dom  packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.servedLabel-26884.test.tsx > PermissionMatrixEditPage — header shows the served label (objectstack#26884) > zh-CN: the he
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    FAIL   dom  packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.servedLabel-26884.test.tsx > PermissionMatrixEditPage — header shows the served label (objectstack#26884) > ja-JP: the he
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    FAIL   dom  packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.servedLabel-26884.test.tsx > PermissionMatrixEditPage — header shows the served label (objectstack#26884) > a set the pub
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    FAIL   dom  packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.servedLabel-26884.test.tsx > PermissionMatrixEditPage — header shows the served label (objectstack#26884) > an edit to th
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 31 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Merge-queue build 40654802071 is red, but the failure does not come from this PR. All four failing cases are in ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.servedLabel-26884.test.tsx. That file is not on main (58668776). It is added by #27019, which sat ahead of this PR in the same speculative stack. This PR (head 7b20f2d5) touches neither that test nor PermissionMatrixEditor.tsx, and its own PR-side CI is green (53 runs, 0 failed).

No fix is needed in this PR, and there is nothing to port from main. The failure belongs to #27019. This PR left the queue with the build, so it has to be re-enqueued. A rerun of the same queue build would rebuild the same stack, so this comment does not request one.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console Home launcher ignores a permission set's tabPermissions: hidden — it reads /api/v1/meta/app while the hide is applied only in /api/v1/me/apps

2 participants