Skip to content

fix(app-shell,react,plugin-detail): fold an override-only admin's approval levers into one menu and draw Recall once - #26053

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-udq66w
Oct 26, 2026
Merged

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Fixes #25877

Clause-②: yes (widening)
Metadata-protocol: none

The Clause-② arm is the claim's ruling: one optional member is added to the exported InlineEditContext value (and to InlineEditProvider's props) of @object-ui/react, which widens its public type surface and narrows nothing. The member is optional so that every existing host keeps its behaviour: absent means false, and then the DetailView approval band draws its own Recall exactly as before. A host sets it only when it draws the request's declared Recall itself.

What changes

Placement only. Who may override, what an override records, the override notice each lever shows and the request each lever sends are unchanged.

  • Record page, admin who is not the approver (viewer.can_act: false, viewer.can_override: true): the approval bar above the title is now one line: a Pending badge with "Waiting on" and the pending approvers by name, the levers with no override arm as buttons (Reply, for the submitter), and one amber "Admin override" menu holding every lever the override flag admits (Override Approve, Override Reject, Override Reassign, and Recall, whose gate ORs the flag in too). The folded set is read off each action's own visible gate (actionAdmittedByOverride), not off a name list. The submitter's own Recall stays the plain Recall inside the menu; a non-submitter admin sees Override Recall, as before.
  • Record page, approver: unchanged (foldOverrides folds nothing for a viewer who can act).
  • Recall drawn once: DeclaredActionsBar reports which declared actions it actually draws (new optional onDrawnActionsChange, after the capability gate and each action's visible verdict; folded levers report through a visibility probe while their menu is closed). While it draws approval_recall, the record page sets approvalRecallDrawnByHost, and the DetailView approval band (@object-ui/plugin-detail) keeps its lock badge and tally only. Read off what the bar draws, not off whether it is mounted: before the request definition loads, or on a backend that sends no viewer block (every declared gate fails closed), the bar draws no Recall and the band stays the submitter's lever.
  • The override menu appears only once a folded lever is shown, so a set whose every gate says no leaves no empty menu.
  • scripts/sdui-manifest.record.json re-recorded on the merged tree (node scripts/gen-sdui-manifest-node.mjs); sdui.manifest.json itself is byte-identical.

Files: ui/packages/app-shell/src/views/DeclaredActionsBar.tsx, ui/packages/app-shell/src/views/RecordDetailView.tsx, ui/packages/react/src/context/InlineEditContext.tsx, ui/packages/plugin-detail/src/DetailView.tsx (seat 2's package, held by this card with seat 2's knowledge), tests beside each, root changeset (patch for @object-ui/app-shell, @object-ui/react, @object-ui/plugin-detail).

Verification

All on head df86fb71 (main 39a78b15 merged in):

$ cd ui && pnpm exec vitest run --maxWorkers=2 PATHS   # 101 files under the verify lock
 Test Files  101 passed (101)
      Tests  728 passed (728)
$ pnpm --filter @object-ui/react --filter @object-ui/plugin-detail --filter @object-ui/app-shell type-check
os-verify-lock: VERDICT command-exit 0

The 101 files are every RecordDetailView.*, RecordApprovalsPanel.* and DeclaredActionsBar.* test in app-shell, every DetailView.* test in plugin-detail, and the InlineEditContext / resolveApprovalLock tests in react. They include RecordDetailView.recordOpenRequests-12701, whose control case ("every approvals consumer still shows its state") is what moved the signal from "bar mounted" to "bar draws Recall". New tests:

  • app-shell/src/views/__tests__/DeclaredActionsBar.foldOverrides-25877.test.tsx (8): the fold, the status line, Reply stays a button, menu order and labels, a folded lever still dispatches with its override notice, the approver's bar unchanged, no change without foldOverrides, the drawn-actions report (menu closed, approver set, unmount), no empty menu.
  • plugin-detail/src/__tests__/DetailView.approvalRecallHostDrawn-25877.test.tsx (3): signal on, absent, and toggled.
  • RecordDetailView.approvalRecallGate.test.tsx (+3, page to band seam): the signal is true while the bar draws Recall, false on a backend with no viewer block, false with no approvals API.
  • RecordDetailView.approvalDeclaredActions.test.tsx: the override-admin case now opens the menu to find its three levers.

The first type-check run on the merged tree failed once in metadata-admin/useMonacoFallback.ts (configureBundledMonaco missing from a plugin-editor dist built before #25993 merged); after pnpm --filter '@object-ui/app-shell^...' build all three packages pass. That file is not in this diff.

Reverse verification, each mutation through scripts/ablation-replace.mjs with its on-disk proof and a byte-identical restore to HEAD (tests resolve @object-ui/* to source, so no rebuild leg):

mutation went red
band: drop the approvalRecallDrawnByHost term from the Recall condition 2 of 3 in DetailView.approvalRecallHostDrawn-25877
bar: never fold (false && foldOverrides …) 3 of 8 in DeclaredActionsBar.foldOverrides-25877
bar: always draw the menu trigger "draws no menu when every folded lever is hidden by its own gate"
page: approvalRecallDrawnByHost={false} "tells the band the page draws recall while the decision bar draws it"
page: signal on mount instead of on drawn (&& to ||) "leaves the band its recall when the bar is up but draws none"

Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 46 commands for this diff (10 paths vs merge base 39a78b15); every one exited 0 on the merged tree, including check-sdui-manifest.mjs after the re-record. --ran reconciliation: "46 derived famil(ies) accounted for — 46 run, 0 NOT-MEASURED". After the last commit (changeset wording only) the changeset-reading gates were re-run and exited 0.

Acceptance notes

  • Placement reading. The card says 「标题旁一个「…」菜单」. The menu sits where the bar was, on the line above the title, and is not injected into page:header; the seat's claim records this reading.
  • Status wording. The line reuses existing bundle keys (approvalsInbox.statusPending, approvalsInbox.waitingOn, approvalsInbox.approverNameSeparator, approvalsInbox.viaOverrideChip for the menu's name), so in zh-CN it reads 「待审批」 + 「等待以下审批人」 + names rather than the card's literal 「审批中 · 等待 X」. A dedicated sentence key would touch ui/packages/i18n in ten locales, outside this card's file surface. carrier: none.
  • Menu items keep the menu open while the lever's own dialog runs (preventDefault on select), the same shape as the page header's overflow items in PageHeaderActions.
  • Narrowed local run. ui/packages/app-shell has about 1700 test files; locally only the files named after the touched components ran (listed under Verification). The full package is CI Test Core's.

Generated by Claude Code

…to one menu and draw Recall once

For a viewer who holds no approver slot but may override, the record
page's approval bar becomes one status line (pending, waiting on whom),
the levers without an override arm as buttons, and one marked menu with
every lever the override flag admits. The approver's bar is unchanged.

The record page now tells the DetailView approval band that it draws the
declared Recall itself (new optional InlineEditContext member
approvalRecallDrawnByHost), and the band keeps its lock badge only.

Claude-Session: https://claude.ai/code/session_014N3u93mMkVGhq6DBKXqx4J
Co-authored-by: Claude <noreply@anthropic.com>
…ws one

The band's Recall now steps aside when the decision bar reports that it
draws the request's declared approval_recall, rather than whenever the
bar is mounted. Before the request definition loads, or on a backend
that sends no viewer block (every declared gate fails closed), the bar
draws no Recall and the band stays the submitter's lever.

DeclaredActionsBar reports the declared actions it draws through the new
optional onDrawnActionsChange; folded levers report through a visibility
probe while their menu is closed, and the override menu appears only once
a folded lever is shown.

Claude-Session: https://claude.ai/code/session_014N3u93mMkVGhq6DBKXqx4J
Co-authored-by: Claude <noreply@anthropic.com>
…-udq66w

# Conflicts:
#	scripts/sdui-manifest.record.json
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tooling labels Oct 26, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: df86fb71c821e82b9b583e0383f421c28ab6e107
Local-runs: none

① Derived judgments

  • @object-ui/react InlineEditContextValue.approvalRecallDrawnByHost?: boolean and InlineEditProviderProps.approvalRecallDrawnByHost?: boolean are added. The provider defaults it to false and threads it into the memoised value, with the dependency array updated. This is an additive optional member, so no existing consumer's acceptance changes. Correct as declared: widening.
  • Absent or false keeps the band's prior behaviour. DetailView.tsx adds && !inline?.approvalRecallDrawnByHost in front of the unchanged approvalIsSubmitter !== false gate, so a host that never sets it renders exactly as before. The doc comments mark it placement only, never authorization, as for approvalIsSubmitter. Correct.
  • @object-ui/app-shell DeclaredActionsBar gains the optional foldOverrides and onDrawnActionsChange. Neither is in the exported surface that Clause-② names. A host that passes neither renders the unfolded toolbar as before. The drawn set is reported for both inline buttons and folded levers: folded levers report through DeclaredActionVisibilityProbe while the menu is closed. So RecordDetailView sets approvalRecallDrawnByHost only while the bar actually draws approval_recall, and Recall appears once per page. Before the definition loads, or on a backend with no viewer block, the band remains the lever. Correct.
  • The capability gate (useCapabilityGate) runs before the fold split, and the fold reads each action's own visible gate (actionAdmittedByOverride). An override-only viewer gains no lever and loses none. Correct.

② Semver level

The changeset .changeset/25877-approval-override-menu.md marks @object-ui/app-shell, @object-ui/react and @object-ui/plugin-detail as patch, with Clause-②: yes (widening) and Metadata-protocol: none. That matches the PR body and the claim's ruling (Clause-② yes, additive). It is consistent with an optional member in the fixed release group.

③ Boundary flags

  • The dev raised one flag: the status wording reuses the existing approvalsInbox.statusPending / waitingOn keys rather than a literal 审批中 · 等待 X key. Answered: acceptable for this card. A dedicated key would be an i18n follow-up only if the maintainer asks for that wording.
  • plugin-detail/DetailView.tsx is seat 2's package. The edit is one condition plus a test, and seat 2 was told before dispatch. No open seat 2 card holds that region.
  • PR author objectstack-fleet[bot] (relay write); no other author to name.

Implemented-by: session_014N3u93mMkVGhq6DBKXqx4J
Reviewed-by: session_017iXjUm1rujwDk7f3KT1Pkk

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · seat domain:ui#1 · 2026-10-26T11:23Z

Head df86fb71c821 (read from the PR object).

  • Shape. Draft on main; Fixes #25877; Clause-②: yes (widening), Metadata-protocol: none. The changeset .changeset/25877-approval-override-menu.md is in the root .changeset/ (patch: app-shell, react, plugin-detail).
  • Checks pinned to the head. 61 named check runs. Latest per name: 54 success, 7 skipped, 0 failing, 0 pending.
  • Governed surface. check-governed-merges.mjs --pr 26053 finds 0 of 10 paths governed, with 948 changed lines.
  • Clause-② review. The TIER_DEFAULT contract review is in comment 6414812254 on this PR: PASS.
  • Route A, as ruled. The record page's approval bar folds an override-only viewer's levers into one status line plus one amber menu. The approver's bar is unchanged. The page tells the DetailView band, through the new optional approvalRecallDrawnByHost, to keep its lock badge only while the bar actually draws approval_recall. Folded levers report through a visibility probe, so Recall is drawn once per page. The changeset sentences match the code.
  • sdui record. react and plugin-detail are in the registry closure, so the PR re-records the hash. It lands in the manifest-hash serial queue, which the two seats interleave.

Landing: held for the serial queue. When its turn comes, the card thread merges origin/main and regenerates, and the seat arms auto-merge on that head. A head move that only regenerates or merges main keeps the contract review.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: e37b77231874b0fadf7fcacfcdb2bef131c78cf7
Local-runs: none

① Derived judgments

  • @object-ui/react InlineEditContextValue.approvalRecallDrawnByHost?: boolean and InlineEditProviderProps.approvalRecallDrawnByHost?: boolean are added. The provider defaults it to false and threads it into the memoised value, with the dependency array updated. This is an additive optional member, so no existing consumer's acceptance changes. Correct as declared: widening.
  • Absent or false keeps the band's prior behaviour. DetailView.tsx adds && !inline?.approvalRecallDrawnByHost in front of the unchanged approvalIsSubmitter !== false gate, so a host that never sets it renders exactly as before. The doc comments mark it placement only, never authorization, as for approvalIsSubmitter. Correct.
  • @object-ui/app-shell DeclaredActionsBar gains the optional foldOverrides and onDrawnActionsChange. Neither is in the exported surface that Clause-② names. A host that passes neither renders the unfolded toolbar as before. The drawn set is reported for both inline buttons and folded levers: folded levers report through DeclaredActionVisibilityProbe while the menu is closed. So RecordDetailView sets approvalRecallDrawnByHost only while the bar actually draws approval_recall, and Recall appears once per page. Before the definition loads, or on a backend with no viewer block, the band remains the lever. Correct.
  • The capability gate (useCapabilityGate) runs before the fold split, and the fold reads each action's own visible gate (actionAdmittedByOverride). An override-only viewer gains no lever and loses none. Correct.

② Semver level

The changeset .changeset/25877-approval-override-menu.md marks @object-ui/app-shell, @object-ui/react and @object-ui/plugin-detail as patch, with Clause-②: yes (widening) and Metadata-protocol: none. That matches the PR body and the claim's ruling (Clause-② yes, additive). It is consistent with an optional member in the fixed release group.

③ Boundary flags

  • Re-recorded on a new head: since the earlier review at df86fb71c8, the only commits are the merge a1950c6b of origin/main and e37b7723, which re-records scripts/sdui-manifest.sources/plugin-detail.json and react.json under devx(sdui): one manifest record per ui registry package, so ui PRs on different packages stop conflicting #26120's per-package layout. No source line under review changed, so ① and ② stand as written.

  • The dev raised one flag: the status wording reuses the existing approvalsInbox.statusPending / waitingOn keys rather than a literal 审批中 · 等待 X key. Answered: acceptable for this card. A dedicated key would be an i18n follow-up only if the maintainer asks for that wording.

  • plugin-detail/DetailView.tsx is seat 2's package. The edit is one condition plus a test, and seat 2 was told before dispatch. No open seat 2 card holds that region.

  • PR author objectstack-fleet[bot] (relay write); no other author to name.

Implemented-by: session_014N3u93mMkVGhq6DBKXqx4J
Reviewed-by: session_017iXjUm1rujwDk7f3KT1Pkk

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Test UI (5/16) on head e37b7723 failed with three timeouts, not assertions, all in repo-wide scan tests that this PR does not touch:

  • ui/scripts/__tests__/check-vi-mock-inherit.test.ts: "no @object-ui/i18n factory freezes the surface" and "walked a real population", each Test timed out in 15000ms
  • ui/scripts/__tests__/dollar-dialect-alias-census.test.ts: "is asymmetric, measured by ABLATION…", Test timed out in 15000ms

Why I don't read it as this PR's failure:

  • The same two files were green in Test UI on the previous head df86fb71, whose source diff is identical; since then the branch has only merged main and re-recorded two sdui source hashes.
  • On e37b7723 locally, cd ui && pnpm exec vitest run over both files gives Test Files 2 passed (2), Tests 120 passed (120) in 22s.
  • The shard itself ran 909s.

The one @object-ui/i18n mock this PR adds, in DeclaredActionsBar.foldOverrides-25877.test.tsx, inherits through importOriginal, which is what the sweep checks; locally the sweep passes on it. No fix to port. Re-running the failed jobs once.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat domain:ui#1 · 2026-10-26T15:27Z: the ACCEPT above carries over to head e37b772318. The only changes since the reviewed head are the merge a1950c6b of origin/main and e37b7723, which re-records scripts/sdui-manifest.sources/plugin-detail.json and react.json (#26120 per-package layout). The contract review is re-recorded at this head. Checks on this head are green, 0 failing. Test UI (5/16) failed once on three 15 s timeouts in ui/scripts/__tests__ scan tests that are outside this diff (comment 6419713088). The rerun passed. plugin-detail is next in the per-package order after #25998 (merged). Landing: ready + auto-merge.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 26, 2026 15:28
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit 054652b Oct 26, 2026
97 of 98 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-udq66w branch October 26, 2026 16:06
@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

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

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

    × has no near-spelling for any of them — unlike objectui#6041, nothing here is a rename 14ms
    FAIL   unit  packages/app-shell/src/services/MetadataService.specKeyObjectPayload.test.ts > the instrument > has no near-spelling for any of them — unlike objectui#6041, nothing here is a rename
      ↳ 失败原因: AssertionError: expected 48 to be 47 // Object.is equality
    
  • Test UI (13/16) — 失败步骤: Run the ui tests (shard 13/16)

    × has no on/off flag to rename to — `enable` is a capabilities object, not a boolean 8ms
    FAIL   unit  packages/app-shell/src/services/MetadataService.retiredObjectEnabled.test.ts > objectui#6238 · the schema oracle — both tombstone keys are refused BY NAME > has no on/off flag to rename t
      ↳ 失败原因: AssertionError: expected 48 to be 47 // Object.is equality
    

↳ 失败原因 是判读的关键:超时(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,按失败测试文件聚合):

  • packages/app-shell/src/services/MetadataService.specKeyObjectPayload.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • packages/app-shell/src/services/MetadataService.retiredObjectEnabled.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

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

分诊清单:

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

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

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/l tooling

Projects

None yet

2 participants