Skip to content

fix(app-shell): honour all three persist values in ContextSelectors - #3922

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-os5994-persist-enforce
Aug 9, 2026
Merged

fix(app-shell): honour all three persist values in ContextSelectors#3922
yinlianghui merged 1 commit into
mainfrom
claude/issue-os5994-persist-enforce

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixesobjectstack-ai/objectstack#5994

按维护者裁定(enforce,不退役;spec 不改)兑现 AppContextSelectorSchema.persist 的三个取值。spec 对该键的原话是 “Persist selection via URL query, sessionStorage, or not at all” —— 一值一介质。而 packages/app-shell/src/layout/ContextSelectors.tsx 此前把每个选择器同时写进 sessionStorage 和 URL,读取侧再按 URL ?? storage 兜一次:于是 'session''query' 完全无差别,'none' 也只是跳过「从 storage 回灌 URL」那一个 effect,照样两处都写。

行为矩阵(三值 × 写入/恢复)

persist写 URL query写 sessionStorage恢复来源备注
'query'(默认)✅ 只写自己派生的 query 键仅 URL旧版遗留的 objectui-ctx-* 条目既不读也不删,直接忽略
'session'❌ 从不进地址栏objectui-ctx-{app}-{id}仅 sessionStorage同名 query 参数不再遮蔽它
'none'无 —— 组件 state,随 mount 消亡仍照常发布为导航模板变量

读取侧与写入侧同等排他:'session' 选择器不再从 query string 读回,'query' 选择器也不再从 storage 读回 —— 那个 ?? 正是两值同化的根因。想同时要「URL 反映」和「记忆兜底」,应当去 spec 加一个新取值,而不是让渲染器背着作者写第二处存储。

一并删掉的 storage → URL 回灌 effect

它是「两处都写」的配套设施:URL 缺参时用 storage 补回。一值一介质之后已无可桥接之物 —— 'query' 作用域被无参导航丢掉时,由 SelectorControl 的 auto-select-first 重新确立(「不能停在空值上」本来就是该 effect 自述的存在理由);想要超出 URL 的记忆,作者显式声明 'session',代价是不进地址栏。

dev 环境下「两个选择器撞同一个 scope 键」的告警随之只看 'query' 选择器:其余取值根本不碰 URL,再告警就是误报。

与裁定注记不符的一处实况(请复核)

裁定写道「默认仍是 'query',只有显式声明 'session'/'none' 的作者会看到变化」。实测并非如此:Studio 在 framework 仓 packages/platform-objects/src/apps/studio.app.ts显式声明了 persist: 'query',且它是全仓唯一声明 contextSelectors 的应用(objectui 里没有任何元数据声明过 persist)。所以本 PR 唯一影响到的就是它,而变化恰好是「只有你声明的那一处才生效」:

  • ?package= 的读写一切照旧 —— 六个 Studio 读取面、以及约 15 条导航元数据里的 params: { package: '{active_package}' },全不受影响;
  • 不再写 objectui-ctx-studio-active_package;
  • 无参导航落到「首个 project 包」而不是「上次选中的包」(此前靠 storage 回灌)。

第三条是本 PR 唯一的用户可见退化,它是「兑现声明」的直接后果而非附带损伤:作者声明的介质是 URL,URL 里没有,就没有。packages/app-shell/README.md 的 Studio package scope 段落原写着 “repairs missing ?package= … from the last selected package”,已按此改正。

测试

  • 新增 ContextSelectors.persist.test.tsx:三值 × (写 URL / 写 storage / 恢复来源) 全矩阵,外加「persist 省略时套用 spec 的 'query' 默认」「'none' 在 remount 后丢弃」「非 'query' 选择器不再触发撞键告警」。每个用例都断言两个存储而非仅该取值命名的那一个 —— 「写了 URL」只是半句话,「且没碰 storage」才是坏掉的那半句。
  • 反向验证(方向先判后跑):把实现整体退回 origin/main,19 个用例 12 转红、7 保持绿;保持绿的那 7 个断言的事实本来就成立(「从 URL 读回」、scope 键派生、legacy ?package= 兼容钉、两个 'query' 选择器撞键告警、模板变量发布)。三个 'none' 用例也在转红之列 —— 测试文件里记了原因:'none' 的内存值来自新增 state,单独恢复被删的那两条写入并不会让它红,只有整体退回才会。
  • 既有 ContextSelectors.scopeKey.test.tsx逐条重判(不是批量重写):一条断言翻转(默认取值下 storage 必须为空);一条整体替换 —— 原 “re-applies each remembered scope under its own key” 钉的正是被删的回灌桥,而且它本就无法诚实报告:active_env 存的 'prod' 恰好也是它 auto-select 的首个选项,那一半即使什么都没恢复也会绿。替换后的用例改在 persist: 'session' 上钉「两个选择器、两把 storage 键、互不干扰、且都不进 URL」,两个取值都刻意避开首选项。
pnpm exec vitest run packages/app-shell/src/layout/__tests__/ContextSelectors.persist.test.tsx \
packages/app-shell/src/layout/__tests__/ContextSelectors.scopeKey.test.tsx --maxWorkers=2
→ Test Files 2 passed (2) / Tests 19 passed (19) 连跑 5 次稳定
pnpm exec vitest run packages/app-shell/src/layout/__tests__/ --maxWorkers=2
→ Test Files 16 passed (16) / Tests 85 passed (85) (整目录连跑 3 次绿;见下)
pnpm exec turbo run type-check --concurrency=2
→ Tasks: 78 successful, 78 total
pnpm exec eslint 「本 PR 三个改动文件」 → 0 errors

整目录首跑曾有 1 个 waitFor 超时的偶发红(随后连跑 3 次全绿,且本 PR 两个文件单跑 5/5 绿),报告为偶发而非隐瞒:失败栈是 @testing-library 的 real-timer 超时,即 AGENTS.md 记的「满并行下有界窗口被无界模块加载吃掉」那一类。本 PR 的 fixture 未声明 icon,getIcon(undefined) 返回静态导入的兜底图标,断言路径上没有 lazy 边界。

影响面核对

useAppContextSelectors 的消费者只有 AppSidebar / UnifiedSidebar,两者仅取 contextValueselement;全仓 objectui-ctx 的引用除本文件外只在上述两个测试文件里。其余 layout 测试的 fixture 均未声明 contextSelectors,已逐个 grep 确认。

注:objectui#3500 的前半段(per-selector scope 键派生 + Studio ?package= grandfather)已在 origin/main 落地,本 PR 基于其形态,不存在串行冲突。


Generated by Claude Code

`AppContextSelectorSchema.persist` declares one persistence medium per value
("Persist selection via URL query, sessionStorage, or not at all"), but the
shell mirrored every selector into BOTH stores and read them back as
`URL ?? storage`. So `'session'` and `'query'` were indistinguishable, and
`'none'` only opted out of the storage-to-URL re-apply while still writing
both.
Each value now names exactly one medium, on the read side as well as the
write side:
- `'query'` (default) — the URL query key only; storage is neither written
nor read, so a stale `objectui-ctx-*` entry from an older build is ignored
rather than resurrected.
- `'session'` — sessionStorage only; the scope stays out of the address bar,
and a query parameter of the same key no longer shadows it.
- `'none'` — neither store; the pick lives in component state for that mount
and dies with it.
The storage-to-URL repair effect goes with the mirror it depended on: a
`'query'` scope dropped by a param-less nav link is re-established by
`SelectorControl`'s auto-select-first, which was that effect's stated reason
to exist. The dev-only "two selectors share one scope key" warning now
considers `'query'` selectors only, since no other value touches the URL.
Studio declares `persist: 'query'` and is the only live consumer, so its
package scope is what changes: the undeclared sessionStorage mirror is gone,
and a param-less navigation lands on the first project package instead of the
last picked one. The README's Studio package scope section is updated to say
so.
Refs objectstack-ai/objectstack#5994
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercelBot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 9, 2026 4:52am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests labels Aug 9, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-DHrPDYU9.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)482.39KB106.34KB
core (index.js)2.96KB1.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)139.61KB35.99KB
fields (index.js)230.97KB56.74KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)117.21KB30.27KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)236.63KB59.02KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)112.10KB27.10KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)187.63KB49.66KB
plugin-kanban (index.js)48.30KB13.28KB
plugin-list (index.js)105.12KB25.48KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

验收通过(objectui 分片 PM,session session_01GTRjn8xBqp75dk7kFupVRt)objectstack-ai/objectstack#5994(v17)

实物核验:base 99ba5fbd7(origin/main)→ head a2d8d48b8,单提交 5 文件,与报告逐一相符;零 releases/ 触碰。CI 亲读终态:20 检查全 completed、0 失败(coverage/dependabot skipped 为常规)。

技术裁定意见:

  • 读写两侧同等排他是本单的正确形态 —— 只改写侧会让 session 值被共享链接的 query 参数遮蔽,issue 的「无差别」症状只除一半。
  • 删除 storage→URL 回灌桥成立:一值一介质后无可桥接,空白面由 auto-select-first 兜住(即该 effect 自述的存在理由)。
  • scopeKey 测试的断言翻转与用例替换经复核成立,替换用例刻意避开了 auto-select 首选项假绿(原用例存的 prod 恰为首选项,什么都不恢复也绿)。
  • 撞键告警收窄到 query 选择器,消除误报。

⚠️提请维护者注意(裁定影响面与实测相反,veto 窗口开放):裁定注记称「只有显式声明 session/none 的作者会看到变化」,实测唯一声明 contextSelectors 的应用是 Studio,且它显式声明 persist: 'query' —— 本 PR 的唯一用户可见变化恰好落在 Studio:无参导航不再恢复「上次选中的包」,落到首个 project 包(URL 是声明介质,URL 里没有就没有)。这是兑现声明的直接后果,语义裁定本身未被动摇,故按契约机械类验收;若维护者认为 Studio 需要「URL 反映 + 记忆兜底」的混合行为,那是 spec 侧新增取值/字段的问题域(dev 报告 open question,未擅自开单),请在暂停汇报中定夺。

勘误:PM 认领评论把主落点误写为 packages/plugin-app/.../ContextSelectors.tsx,真实路径是 packages/app-shell/src/layout/ContextSelectors.tsx(issue 正文本来是对的);另 objectui#3500 前半段实际已在 main 上,该卡描述待更新。

GraphQL 配额未恢复则 undraft+auto-merge 由 PM 随后补发。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 9, 2026 05:38
@yinlianghui
yinlianghui added this pull request to the merge queueAug 9, 2026
Merged via the queue into main with commit d86b41cAug 9, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-os5994-persist-enforce branch August 9, 2026 05:39
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationtests

Projects

None yet

2 participants

@yinlianghui@claude