Repository navigation
finding(plugin-view): on the registered object-view path, the grid's row Delete and bulk Delete delete nothing: handleDelete / handleBulkDelete only bump a refresh counter and no code calls dataSource.delete #10383
Description
Activity
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsBlocked-by: #10035
分诊首次定级:
priority:p2·bug·domain:ui·pm:blocked—— 注册的object-view路径上,表格的行「删除」和批量「删除」什么都不删:两个处理函数只刷新计数,没有任何代码调用dataSource.delete;ObjectView.tsx同一区域正被 #10035 的 PR #10378 修改,排在它后面Path:
packages/plugin-view/src/ObjectView.tsx(handleDelete/handleBulkDelete,以及onDelete={operations.delete !== false ? handleDelete : undefined}交给ObjectGrid)Triage: lands in
@object-ui/plugin-view⇒domain:ui,bug,priority:p2,pm:blockedBlocked-by #10035 (findingremoved — graded); rationale: on the registeredobject-viewpath (no host list view),handleDelete/handleBulkDeleteignore their record argument and only bump a refresh key, and neitherObjectView.tsx,ObjectGrid.tsxnorRowActionMenu.tsxcallsdataSource.delete— so a Delete control offered by default (operations.deletedefaults to true) deletes nothing and the row stays after the refresh; in-flight PR objectui#10378 (#10035) edits the grid branch right beside theonDeletehand-off, so it waits for that landing.分诊席 #6015,2026-09-24T21:23Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在 objectui
origin/main8e49a998上核对。本席核对
ObjectView.tsx:const handleDelete = useCallback((_record: Record<string, unknown>) => {和const handleBulkDelete = useCallback((_records: Record<string, unknown>[]) => {,参数都带下划线,没有使用;后面有onDelete={operations.delete !== false ? handleDelete : undefined}。- 三个文件里
dataSource.delete的出现次数:ObjectView.tsx0、ObjectGrid.tsx0、RowActionMenu.tsx0。与卡面一致。 - 卡面的探针(被监视的
dataSource.delete调用 0 次)本席没有重跑。
为什么挂在 #10035 后面
本席用 git 取了在飞 PR 的改动范围:
- PR objectui#10378(finding(app-shell/plugin-view): both
ObjectViewlayers remount the whole view subtree by bumping a Reactkey=after a write — the shape AGENTS.md §5 #8's corollary names verbatim #10035):改动ObjectView.tsx表格分支的第 2341–2400 行附近,紧挨着onDelete的交接处(约第 2403 行)。 - PR objectui#10283:改动第 1209–1300 行附近,在
handleDelete(约第 1337 行)之前。
⇒ 修复会同时动
handleDelete和onDelete的接线,与 #10378 的改动区域重叠。#10035 关闭时本卡解锁;届时再看一眼 #10283 是否已落地,避免与它冲突。定级说明
p2:默认就会出现「删除」按钮,点了没有效果,行还在。用户看得到失败(刷新后行仍在),也没有数据被误删,所以不给 p1。
解锁后的执行要点
- 先测量控制台自己的列表路径是怎么删除的,包括确认弹窗和权限行为。卡面给了两条路:
- 处理函数真的执行删除,并与控制台列表的确认、权限行为一致;
- 或者在这条路径上不提供删除入口。
- 选择与控制台列表一致的那一条。
- 钉住:行删除和批量删除都真的调用了
dataSource.delete,调用后行消失;或者(如果选了第二条路)删除入口在这条路径上不出现。
Generated by Claude Code
- addedbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatand removed
on Sep 24, 2026 objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actions分诊更正:
pm:blocked→pm:queue(priority:p2不变)—— 本席上一条评论把本卡挂在 #10035 后面,理由是 PR #10378 还在改同一块代码;其实本席定级时它已经合并,这个阻塞理由不成立分诊席 #6015,2026-09-24T23:24Z。这是本席对自己上一条评论的更正。
哪里错了
- 上一条评论第一行把本卡挂在 finding(app-shell/plugin-view): both
ObjectViewlayers remount the whole view subtree by bumping a Reactkey=after a write — the shape AGENTS.md §5 #8's corollary names verbatim #10035 上,理由是「PR fix(plugin-view,app-shell): refresh the view in place after a write instead of remounting it (objectui#10035) #10378 正在改ObjectView.tsx同一区域」。 - 实际上 PR fix(plugin-view,app-shell): refresh the view in place after a write instead of remounting it (objectui#10035) #10378 已经合并为
8e49a998(finding(app-shell/plugin-view): bothObjectViewlayers remount the whole view subtree by bumping a Reactkey=after a write — the shape AGENTS.md §5 #8's corollary names verbatim #10035 的释放评论 5822407884 写明),而8e49a998正是本席当时核对所用的 main。本席把一个已合并 PR 的改动当成了在途改动,判断错误。 - finding(app-shell/plugin-view): both
ObjectViewlayers remount the whole view subtree by bumping a Reactkey=after a write — the shape AGENTS.md §5 #8's corollary names verbatim #10035 仍然开着:它按Part of合并,剩下ObjectGrid/ObjectGantt/ObjectChart的刷新输入,现在是pm:queue,没有人认领,也没有在途 PR。 - 它剩下的工作会碰
plugin-view/src/ObjectView.tsx,但那是排队中的卡,不是在途改动。两张排队卡碰同一个文件,由领卡时的文件检查排先后,不需要分诊挂阻塞。
⇒ 上一条评论第一行的阻塞指向就此撤销。转
pm:queue。本席复核(objectui
origin/main961ceaa3)ObjectView.tsx第 1337 行const handleDelete = useCallback((_record: Record<string, unknown>) => {,第 1342 行const handleBulkDelete = useCallback((_records: Record<string, unknown>[]) => {,参数仍然没有使用。- 该文件里仍然没有
dataSource.delete调用。
⇒ 问题照旧,上一条评论的定级和执行要点都不变。
补一句
领卡时,如果 #10035 的剩余部分已经有人在改
plugin-view/src/ObjectView.tsx,按常规文件检查排在它后面即可。
Generated by Claude Code
- 上一条评论第一行把本卡挂在 finding(app-shell/plugin-view): both
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 —
domain:uiexecution seat
Session:session_01BA3nKVUwKQJf8DBxrSVtNC
Branch:claude/issue-10383-object-view-delete-does-nothing
Worktree:objectui-issue-10383
Domain:domain:ui
Seat:domain:ui#1
File surface:packages/plugin-view/src/ObjectView.tsx(handleDelete/handleBulkDelete, the confirm dialog, and theonDelete/onBulkDeletehand-off toObjectGrid),packages/app-shell/src/hooks/useObjectActions.ts(itsdeletehandler anddeleteRecordre-bind to the shared core; behaviour byte-identical), ONE shared delete-core helper in the lowest package both already depend on (@object-ui/reactfirst; its file plus one index export), tests beside each, and the changesets. ⛔ NotObjectGrid.tsx/RowActionMenu.tsx(objectui#10354), ⛔ notuseConsoleActionRuntime.tsxorpackages/core/src/actions/ActionRunner.ts(objectui#10404) (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus(default judgement tier) —priority:p2: a Delete control offered by default that deletes nothing
Clause-②: yes
Thread-read: 5823930334
Serial constraints cleared: open-PR file lists read 2026-09-24T23:39Z ⇒ none touchesplugin-view/src/ObjectView.tsx(objectui#8941 touches onlypackages/plugin-view/package.json). Livepm:dispatchedclaims read at the same time ⇒ none namesplugin-view. objectui#10378 (objectui#10035 part 1) merged as8e49a998. The objectui#10035 remainder ispm:queueand unclaimed; it will touch this file, so it goes after this take. objectui#10354 (seat 4, in flight) holdspackages/plugin-grid/src/ObjectGrid.tsx⇒ ⛔ this take does not edit it; a change needed there is a stop-and-report.Scope
The triage grading (
5822484555, corrected by5823930334, which lifts theBlocked-byand leaves the grade and the execution notes unchanged):- Measure first how the console's own list path deletes, including the confirm dialog and the permission behaviour.
- Pick the fix that matches it: the handlers perform the delete (with that confirm and permission behaviour), or the Delete affordance is not offered on this path.
- Pin it: row Delete and bulk Delete both call
dataSource.deleteand the row is gone afterwards; or, on the second path, the Delete affordance does not appear here.
⇒
Fixes #10383.Amended by the seat (review of PR objectui#10424 at
ea0c5c627)The first head copied the console list's delete steps into plugin-view. That is a second implementation of the same operation, and it already diverged on consent: the ADR-0094 package-owned permission-set reset question and toast. The seat rule for one operation with two diverging implementations: the governed side wins, and the other re-binds to it. ⛔ It is not aligned by copying and not double-written. So the respin moves the console's delete core into one shared helper and binds both paths to it. The helper is a new export, so
Clause-②is now yes, and a contract review runs before landing.domain:uiseat #1 ·session_01BA3nKVUwKQJf8DBxrSVtNC· claim · 2026-09-24T23:40Zobjectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 10383,
"status": "done",
"branch": "claude/issue-10383-object-view-delete-does-nothing",
"pr": "#10424",
"session": "session_01BA3nKVUwKQJf8DBxrSVtNC — mode:subagent, the parent PM seat's id (container CLAUDE_CODE_REMOTE_SESSION_ID cse_01BA3nKVUwKQJf8DBxrSVtNC); matches the claim 5824094305",
"premise_still_valid": true,
"summary": "Premise confirmed on origin/main a707197: ObjectView.handleDelete / handleBulkDelete took an unused record argument and only bumped refreshKey, and ObjectGrid hands the row to onDelete / onBulkDelete without deleting (its own comments say the consumer owns confirm + toast + refresh). A temporary probe through the REAL ObjectGrid reproduced it before any edit: row Delete and bulk Delete each called dataSource.delete 0 times, bulkDelete 0, bulk 0, and the row was still shown. Road 1 was taken because it matches the console list. The console list (app-shell ObjectView, then ListView, then the same ObjectGrid, with handlers from useObjectActions) confirms through ActionConfirmDialog (actionConfirm.* chrome; objectActions.deleteConfirm for a row, console.objectView.bulkDeleteConfirm once for a selection), calls dataSource.delete per record (Promise.allSettled for several; it does NOT use the optional DataSource.bulkDelete), and afterwards refreshes and toasts deleteSuccess / bulkDeleteSuccess / bulkDeletePartial; a failed single delete toasts deleteFailed with the error message and does not refresh. Its handlers read no permission: the affordance gate is ObjectGrid's own (operations.delete, perms.can(object,'delete'), bucket/userActions/API operations, per-record explain), the same component on both paths. The plugin-view handlers now repeat those steps with a local AlertDialog in the same shape and keys, plus toast, both from @object-ui/components. The nine i18n keys already exist in all ten packs; English defaults were added to VIEW_DEFAULT_TRANSLATIONS, byte-identical to en. What did not change: no new dependency, no export or signature change (Clause-② no holds), no edit to ObjectGrid.tsx / RowActionMenu.tsx / app-shell / plugin-list, and the grid remount-to-refresh is untouched. Zone-2 readings: #1 confirmed; #2 reproduced; #3 as above; #4 plugin-view -> components/i18n are existing dependencies and nothing comes from app-shell; #5 DataSource declares optional bulkDelete(resource, ids) and bulk?(), and the loop matches the console; #6 hidden where the principal may not delete, by ObjectGrid, now pinned on this path; #7 only the grid branch receives handleDelete, and the kanban/calendar/gantt/other branches get none, so none shares the no-op. Assignee was os-bill (the PM's) at start and was not touched.",
"tests": "All at head ea0c5c6 unless noted. (1) Pin: packages/plugin-view/src/tests/ObjectView.gridDelete-10383.test.tsx, 7 cases driving the real ObjectGrid through ObjectView against an in-memory data source: row confirm then delete(test_object, r1) and the row is gone; Cancel deletes nothing; a refused delete shows 'Failed to delete Test object' with the message and the row stays; bulk confirm 'Delete 2 selected records? This cannot be undone.' then 2 delete calls, 0 bulkDelete, and both rows are gone; partial failure shows '1 deleted, 1 failed' and still refreshes; a permission pair where the grant shows row and bulk Delete, and no grant shows an open menu with Edit and no Delete and no bulk Delete.pnpm exec vitest runof that file: Tests 7 passed (7). (2) Reverse verification with the fix already committed: objectstack scripts/ablation-replace.mjs in WRAP mode put the two handlers back to their refresh-only bodies (anchor x1 -> x0, blob f011d9f8 -> 566dcc9a). Pre-registered 5 red / 2 green; observed 'Tests 5 failed | 2 passed (7)'. The 2 green are the permission pair, which pins ObjectGrid's gate and not the handlers. Restore proven: blob after restore == HEAD blob f011d9f8 andgit diff HEADempty. There is no dist in the path: the test imports ../ObjectView, and plugin-grid resolves to src through the root vitest alias. (3)turbo run build --filter='@object-ui/plugin-view^...' --concurrency=2: 15/15 successful. (4)pnpm --filter @object-ui/plugin-view run type-check(tsc --noEmit && tsc -p tsconfig.test.json): exit 0 at ea0c5c6; --listFilesOnly shows the new test in the test program (1 hit). (5)pnpm exec vitest run packages/plugin-view/ --maxWorkers=2from root: Test Files 48 passed (48), Tests 440 passed (440), on a tree byte-identical to ea0c5c6. (6) Console registered-path suites plus text readers of the touched sources, one run: Test Files 23 passed (23), Tests 1298 passed (1298). Registered-path suites: StudioDesignSurface.gridRefresh, studio-canvas-preview, StudioDesignSurface.studioCanvasLeaf, rowClickModifierPayloadForward-9462, apps/console timelineAxisRefusalReach-7070, schema-catalog catalog-gallery-render. Text readers: defaults-maps-mirror-en-pack, fallback-placeholder-spelling-3512, objectView-config-keys-retired-4730, raw-key-call-sites-3546, relayRungCensus-7559, titleFieldConvergence, onRowClickArity-9357, types object-kanban-group-by-limit-7322 / object-view-unmirrored-keys-7779 / schema-reference-named-list-view-keys-7923 / tree-view-config-readers-8253 / zod-mirror-parity, column-identity.ratchet, flow-envelope.ratchet, scripts check-handler-key-read-sites / check-lucide-icon-record-names / check-i18n-call-site-keys. The in-package readers objectViewHostSurface and modifierClickNewTab-9806 ran in (5). (7) Gates at ea0c5c6: check:control-bytes OK (8473 files); check:new-line-citations VERDICT 0 new, exit 0; check-changeset-presence 1 released package, 1 changeset, exit 0; check:changeset-claims exit 0, listing 5 pending changesets that name plugin-view/src/ObjectView.tsx (6726, 7070, 7499, 8653, 7779), each re-read and still true because none describes the delete handlers; check:i18n-keys, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:test-path-roots, check:i18n-dead-keys all exit 0. My own control-byte grep over the 3 touched files found 0 hits. (8) eslint --no-inline-config --format json, per-rule counts compared with the base (base content fed through --stdin-filename at the same path): ObjectView.tsx base == head (no-explicit-any 53 warn, only-export-components 4 warn, exhaustive-deps 3 warn); the new test has 0 messages. Governed-surface guard --test: NOT GOVERNED. Lock: the first full-suite attempt returned VERDICT queue-timeout exit 99 (NOT MEASURED; holder was another worktree's app-shell run). The same OS_VERIFY_LOCK_SLOT was re-taken and the suite measured green in (5).",
"mcp_calls": "0 — no MCP GitHub tool was called (reads went through REST GET via the proxy)",
"api_writes": "2 fleet-relay strokes, each = 1 POST /repos/objectstack-ai/objectstack/dispatches executed by the fleet-write run as objectstack-fleet[bot]: (1) pr_create -> POST /repos/objectstack-ai/objectui/pulls (draft forced) = PR 10424, run 36077024661 success; (2) this os-dev-report -> POST /repos//issues/10383/comments. Plus git push of the branch (not REST). Zero label writes, zero PR-body PATCHes.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted in PR Acceptance notes, not filed — ADR-0094 copy is console-only. For a package-owned sys_permission_set row, useObjectActions asks resetPackageSetConfirm and toasts resetPackageSetSuccess; on this path the same row now gets the generic delete copy (before this change the path deleted nothing at all). Porting it would put a system-object special case into a generic plugin or need a shared lower-package helper, so it stays outside this card's diff. Where it applies here: e.g. the Studio object canvas (StudioObjectRecordsCanvas renders the registered object-view) opened on sys_permission_set. Dedupe words: object-view permission set reset copy · ADR-0094 plugin-view delete · resetPackageSetConfirm registered path",
"carrier: 承接者:无 · noted in PR Acceptance notes, not filed — observation, not a filing class: plugin-detail DetailView.handleDelete comments that the ActionProvider onConfirm 'will intercept' window.confirm; nothing intercepts a native window.confirm (comment accuracy, no user-visible failure)",
"conflict noted, not chosen silently: the harness attribution reminder asked for a Co-Authored-By trailer naming a model on commits. The dispatch order says commit trailers are model-free only, and the agent definition says the dispatch contract wins. Commits carry only the model-free Claude-Session trailer. One unpushed commit was amended before its first push to drop a card reference line; no pushed history was rewritten"
]
}objectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actions✅ ACCEPT — PR objectui#10424 at
473c86933· contract review FAIL on the PR body only → fixed by the seat's body patch · entering the merge queuedomain:uiseat #1,session_01BA3nKVUwKQJf8DBxrSVtNC. The claim (amended5824094305) saysClause-②: yes:@object-ui/coregains one export,recordDelete. A review-tier contract review ran against the card, the amended claim and the PR only. Its record is below, verbatim.Round 1 → round 2.
- The record passes every derived judgment (①1–①6) and the semver level (②). Its one FAIL is the PR BODY: it still declared
Clause-②: no, "no new export", "ADR-0094 copy is not ported", and gates atea0c5c627. That item needs no code change, and the head does not move. - The seat patched the body at
473c86933: line 2 now readsClause-②: yes; the fix is described as the shared core; the pins, both ablation legs and the gates are stated at this head; the ADR-0094 note is inverted. - The seat re-read the patched body against the diff and the record, and item 1 is closed. ⇒ PASS.
Why the respin (the seat's review of the first head,
ea0c5c627). The first head copied the console's delete steps intoplugin-view. That is one operation with two implementations, and the copy already diverged on consent (the ADR-0094 reset question). Under the seat's rule — the governed side wins and the other re-binds; ⛔ no copying, ⛔ no double-writing — the console's delete core moved intorecordDeletein@object-ui/core, and both hosts now bind to it. The console's behaviour is byte-identical (its hook test is unchanged).Maintainer veto window (the seat does not hold landing for these; they are named in the round report):
- On the registered
object-viewpath, grid Delete now deletes. For a package-ownedsys_permission_setrow it asks the RESET question and shows the reset toast, the same as the console. packages/core/README.mddoes not documentrecordDelete. objectui AGENTS.md Add automated testing infrastructure and CI/CD workflows #2 read literally asks for it. Standing practice is silence (no sibling action export is documented there), and no gate enforces it, so the seat leaves it to your call. A README paragraph can ride a follow-up.
Contract review
Served-tier:
CONTRACT_REVIEW_TIER
Head-sha:473c86933be38cf4c768aff52acf6d94df51c544Inputs read in full: objectui#10383 body; comments
5822484555(triage grading),5823930334(its correction),5824094305(claim, including the "Amended by the seat" section),5824519657(firstos-dev-report, measured atea0c5c627); PR #10424 body as served by REST at review time (updated_at2026-09-25T01:23Z);git diff origin/main...473c86933over all 9 files (.changeset/10383-object-view-grid-delete-deletes.md,.changeset/10383-record-delete-core.md,packages/app-shell/src/hooks/useObjectActions.ts,packages/core/src/actions/__tests__/recordDelete.test.ts,packages/core/src/actions/index.ts,packages/core/src/actions/recordDelete.ts,packages/plugin-view/src/ObjectView.tsx,packages/plugin-view/src/__tests__/ObjectView.gridDelete-10383.test.tsx,scripts/__tests__/check-i18n-call-site-keys.test.ts) plus the head copies of the three source files and both test files;origin/main:packages/app-shell/src/hooks/useObjectActions.tsin full; the seat rule verbatim from objectstack.claude/skills/pm-dispatch/SKILL.md§升级与决策 (lines 226-227: 「② 一个操作两个实现且行为不一致 ⇒ 带治理的一侧(权限闸、同意、去重、审计)胜出。」「另一侧改绑并删除,不是对齐也不是双写;反向裁只在产品语义明确要求时成立。」); ADR-0094 as it appears in the code (the[ADR-0094]blocks inuseObjectActions.tsonorigin/mainand the module docblock ofrecordDelete.ts) and the delete rows of objectstackdocs/adr/0094-sys-permission-set-pure-projection.md(lines 144-145: delete of an artifact-backed set = overlay tombstone = reset); objectuiAGENTS.md#2 (docs-driven, line 94), the changeset-presence rule (lines 167-169) and the version policy (lines 258-262); objectstackAGENTS.mdlines 1084-1096 (Clause-② declaration, 「yestakes at leastminor」); pm-dispatchreferences/landing-operations.mdlines 5-20 andreferences/contract-review.md; the console callerspackages/app-shell/src/views/ObjectView.tsx2971-2993;packages/core/src/actions/ActionRunner.ts1176-1183, 1236-1272, 1328-1370;packages/plugin-grid/src/ObjectGrid.tsx1504-1530, 1580-1600, 1662-1715, 4003-4050;packages/i18n/src/useSafeTranslation.ts;packages/types/src/data.ts476-482; the head's 43 check-runs via REST.Measurement notes. A scratch worktree at the head under
<scratchpad>/pr-10424/wt(offline install, removed after this record):pnpm exec vitest runover the 9 suites named in ①2-①5 →Test Files 9 passed (9),Tests 211 passed (211);pnpm run type-checkinpackages/core,packages/plugin-view,packages/app-shell→ exit 0 each (against a freshly builtpackages/core/dist, which carriesdist/actions/recordDelete.d.tsre-exported throughdist/actions/index.d.ts);node scripts/check-changeset-presence.mjs→ "6 source file(s) of 3 released package(s) changed, and this change declares 2 changeset(s)", exit 0;check-changeset-no-major/check-changeset-fixed/check-pending-changeset-literals→ OK;node scripts/check-i18n-call-site-keys.mjs→ exit 0 ("Interpolation parity: 2807 call sites … every call site passes exactly the arguments that value has holes for"; the scanner walkspackagesandapps, line 1406, sopackages/coreis in scope). Head check-runs: 43 total, 40success, 3skipped(coverage shards and dependabot), 0 failures; the green set includesType Check,Test (shard 1/8 … 8/8),Test (dist pins),Changeset Declaration,Changeset Bump Policy,Changeset Claim Re-read,Changeset Fixed Group Check,README Export Check,Governed Surface Queue Guard,Line Citation Gate,Lint. The PR body's own "Gates" section and the onlyos-dev-reporton the card were measured at the pre-respin headea0c5c627; nothing on the card measures473c86933, so the readings above are this record's.① Derived judgments
-
The new export
recordDelete. Reachable:packages/core/src/index.ts:52export * from './actions/index.js'→packages/core/src/actions/index.tsgains exactly one line,export * from './recordDelete.js'. Exactly one name:recordDelete.tshas oneexportstatement (line 109,export const recordDelete = Object.freeze({ confirmText, run }));RecordDeleteDeps,RecordDeleteRequest,Translate,Row,PERMISSION_SET_OBJECT,isPackageOwnedare module-private, soexport *publishes one value and nothing else (confirmed on the builtdist/actions/recordDelete.d.ts). Signature minimal:confirmText(deps: Pick<Deps,'objectName'|'t'>, record?)andrun(deps, request);deps=objectName,label,t,dataSource{delete, findOne},toast{success, error},onRefresh?— each one is read byrunand nothing unused is asked for;requestis the runner's own action shape so the console registersrununchanged. Sufficient for both callers: app-shell passes{ objectName, label: objectLabel || objectName, dataSource, t, toast (sonner), onRefresh }(headuseObjectActions.ts:95-99), plugin-view passes{ objectName: schema.objectName, label: objectSchema.label || objectName, dataSource, t: tView, toast (@object-ui/components, a sonner re-export), onRefresh: () => setRefreshKey(+1) }(headObjectView.tsx:2602-2613);DataSource.delete(resource, id, opts?) : Promise<boolean>(types/src/data.ts:478-482) andfindOneare structurally assignable, and both hosts type-check. No new dependency:recordDelete.tsimports onlytype { ActionResult } from './ActionRunner.js';packages/core/package.jsonis untouched (deps: @object-ui/types, @objectstack/formula, @objectstack/spec). Lowest common package, no inverted edge:@object-ui/coreis a direct dependency of both@object-ui/app-shelland@object-ui/plugin-view(theirpackage.jsonat head), sits below@object-ui/react(which itself depends on core; the claim had namedreactfirst — core is lower and already shared), and core imports nothing from either host or from i18n/sonner (the translator and the toast sink are injected). PASS -
Console re-bind — behaviour-identical to
origin/main. Line-by-line againstorigin/main:useObjectActions.ts86-181 and 203-221: therecordsfilter (Array.isArray(params.records) ? filter(r?.id != null) : null), thelength > 1bulk branch (Promise.allSettled(records.map(r => dataSource.delete(objectName, r.id)))— the head adds only anas stringcast, no conversion — thenonRefresh?.()BEFORE the toast,t('objectActions.bulkDeleteSuccess', { count, label, defaultValue: \Deleted ${n} ${label} records` })→{ success: true, reload: true, silent: true }, elset('objectActions.bulkDeletePartial', { succeeded, failed, defaultValue })→{ success: false }with noerrorkey), the id chainparams.recordId ?? params.record?.id ?? records?.[0]?.id ?? request.recordId, thenoRecordIdreturn{ success: false, error: t('objectActions.noRecordId') }, the ADR-0094 block (objectName === 'sys_permission_set',params.record.managed_by !== undefined ? params.record : await dataSource.findOne(objectName, recordId),managed_by === 'package', wrapped in try/catch falling back to the generic copy), the single try (delete→onRefresh→toast.success(reset ? t('objectActions.resetPackageSetSuccess', { defaultValue }) : t('objectActions.deleteSuccess', { label }))→{ success: true, reload: true, silent: true }) and catch (toast.error(t('objectActions.deleteFailed', { label }), { description: err.message })→{ success: false }, no refresh) are the same statements in the same order with the same keys, args and defaultValues;labelis bound toobjectLabel || objectNameexactly where main computed it inline.deleteRecord: main'spackagedSetReset = objectName === 'sys_permission_set' && record?.managed_by === 'package'choosingresetPackageSetConfirm {defaultValue}vsdeleteConfirmisrecordDelete.confirmText({ objectName, t }, record)with the same two calls and the samedefaultValuestring;params: record ? { recordId, record } : { recordId }and the deps[execute, t, objectName]are unchanged; theuseEffectdeps array is unchanged. The handler is still a promise-returning function registered under'delete', and the runner still awaits it and catches a rejection (ActionRunner.ts:1240-1243,1268-1272), so a throw anywhere is converted the same way as before. Truth table (identical in main and head;n= rows withid != null` after the filter):request branch id used ADR-0094 row source (only sys_permission_set)return params.records, n>1bulk allSettled, refresh, one toasteach r.idrawnot consulted {success:true,reload:true,silent:true}/{success:false}params.records, n=1single records[0].idparams.recordabsent →findOnesingle-path returns params.records, n=0single falls through to params.recordId ?? params.record?.id ?? request.recordId— {success:false,error:noRecordId}if noneparams.record(row)single record.idthe row if it carries managed_by, elsefindOnesingle-path returns params.recordId(+ optionalrecord)single params.recordIdas above single-path returns legacy top-level recordIdsingle request.recordIdfindOnesingle-path returns none single undefined— {success:false,error:t('objectActions.noRecordId')}, no toastSingle-path returns: success →
dataSource.delete,onRefresh, success toast,{success:true,reload:true,silent:true}; failure → error toast withdescription: err.message, NO refresh,{success:false}(noerror, sohandlePostExecutionatActionRunner.ts:1359toasts nothing a second time).useObjectActions.test.tsxunchanged:git diff --quiet origin/main 473c86933 -- packages/app-shell/src/hooks/__tests__/useObjectActions.test.tsxexits 0; it mockssonner(line 44) and does not mock@object-ui/core, so it exercises the real core through the hook, and it passes at head (7 cases: toast de-duplication ×3, ADR-0094 reset copy ×3 including thefindOnefallback for{ recordId }only, plus setup). PASS -
plugin-view re-bind.
performDeleteis gone:git grep performDelete 473c86933 -- packages apps scripts→ 0 hits (it existed only atea0c5c627). The one-row question isrecordDelete.confirmText({ objectName: schema.objectName, t: tView }, deleteRequest?.records[0])and Continue callsrecordDelete.run(...)(headObjectView.tsx:2589-2593,2600-2616); the host owns only theactionConfirm.*chrome and the bulk questiontView('console.objectView.bulkDeleteConfirm', { count }). Request shapes match the console's byte for byte: console rowactions.deleteRecord(String(record.id), record)behindif (record?.id != null)→params: { recordId, record }(app-shell/src/views/ObjectView.tsx:2971-2977,useObjectActions.ts:132); plugin-view rowhandleDeleteskipsrecord?.id == nulland sends{ params: { recordId: String(records[0].id), record: records[0] } }. Console bulkvalid = records.filter(r?.id != null); if (valid.length === 0) return; params: { records: valid }with the bulk question overvalid.length(:2980-2993); plugin-viewhandleBulkDeletefilters the same way, returns on empty, and sends{ params: { records } }with the bulk question overrecords.length. A package-ownedsys_permission_setrow now gets the reset question and the reset toast on this path: pinned by the new test's last describe (dialog text = the fullresetPackageSetConfirmsentence;successSpy='Permission set reset to its shipped baseline';findOnenot called because the row was passed), with the environment-owned control. Cancel:AlertDialogCancelcloses via Radix →onOpenChange(false)→setDeleteRequest(prev => ({ ...prev, open: false })), the request is retained (so the description does not blank during the exit animation) andrunis never called; pinned ("Cancel deletes nothing and the row stays"). Double-click guard:AlertDialogActionisAlertDialogPrimitive.Action(components/src/ui/alert-dialog.tsx:109-111), so the first click runsif (!deleteRequest?.open) return;withopen: true, setsopen: false, startsrun, and Radix closes; React flushes that discrete event synchronously, so the still-mounted exit-animating content re-renders with a closure whosedeleteRequest.open === false, and a second click returns at the guard. Judged from the code; the pin suite does not exercise the animation window (non-blocking, see ③).
The dev's two edges. (a) Bulk ids forwarded raw: main's console handler already diddataSource.delete(objectName, r.id)raw on the bulk path, and both hosts stringify only on the row path before the core (String(record.id)/String(records[0].id)); so the core's "ids forwarded exactly as supplied" is the console's existing behaviour, and the row ids come from the same grid on both paths. Not a divergence; does not block. (b) A synchronous throw inside the bulkmaprejectsrun:DataSource.deleteis declaredPromise<boolean>and every in-tree implementation isasync(data-objectstack/src/index.ts:3940) or returnsPromise.reject(...)(plugin-form/src/EmbeddableForm.tsx:329), so no in-tree adapter throws synchronously; on the console the runner's catch converts it exactly as it did for main's inline handler; on plugin-viewvoid recordDelete.run(...)would surface as an unhandled rejection with no toast and no refresh. Unreachable in-tree, same class as main on the governed side, and the hosts share the one core; does not block. A.catchon thevoid run(...)call (or moving the bulkmapinside the try) is a one-line hardening for a follow-up. PASS -
The i18n pin re-point. The tuple
[file, absent, present]moved frompackages/app-shell/src/hooks/useObjectActions.tstopackages/core/src/actions/recordDelete.ts;present("t('objectActions.resetPackageSetSuccess', {") is unchanged and matches head line 200;absent("t('objectActions.resetPackageSetSuccess', {\n label:", 14 spaces) is the indentation of the new file (thedefaultValue:line at 201 sits at 14 spaces, so a reintroducedlabel:in first position is caught, as the 16-space form was in the old file). The test still proves the inertlabelis absent fromresetPackageSetSuccess(not.toContain(absent)) and adds a new sister assertion,expect(recordDeleteCore).toContain("t('objectActions.deleteSuccess', { label })"), beside the existingmarketplace.install.updateTosister; the repo-wideparityOf(repoRoot)assertion in the same file is untouched and green at head, and the mechanized parity gate (check:i18n-keys) coverspackages/core. Strengthened, not weakened. PASS -
No reader of the retired inline copies or of the old refresh-only behaviour.
git grep -n "performDelete\|resetPackageSetConfirm\|resetPackageSetSuccess\|packagedSetReset" 473c86933 -- packages apps scripts(locales excluded) → onlyrecordDelete.ts,recordDelete.test.ts, the re-pointed i18n pin,check-i18n-en-drift.{mjs,test.ts}(key-spelling comments, not readers of the hook) and CHANGELOG prose. No test or script pinshandleDelete = useCallback((_recordor the refresh-only body (git grep "_record: Record<string, unknown>"→ one unrelated TS-error quotation inreact/src/hooks/__tests__/useNavigationOverlay.onRowClickArity-9357.test.tsx:128). The plugin-view suites that readrefreshKey(ObjectView.expandGate,ObjectView.refreshInPlace-10035,ObjectView.refreshSignal) pass at head;check-changeset-claims(the five pending changesets namingObjectView.tsx) exits 0. PASS -
Accept set and public surface. Widens by exactly one value export on
@object-ui/core(recordDelete), reachable from.— theexportsmap and everypackage.jsonare untouched. Nothing narrows:useObjectActions's exportedObjectActionsshape,deleteRecord(recordId, record?), and plugin-view'sObjectViewPropsare unchanged; no authorable key is added or re-read; no i18n key is added (all nineobjectActions.*/actionConfirm.*/console.objectView.bulkDeleteConfirmkeys plusresetPackageSetConfirm/resetPackageSetSuccessexist in all 10 packs; the en values added toVIEW_DEFAULT_TRANSLATIONSare byte-identical toen.ts, anddefaults-maps-mirror-en-packpasses). The permission gate is unchanged and decides both paths: plugin-view handsonDelete/onBulkDeleteonly whenoperations.delete !== false(ObjectView.tsx:2461-2462), andObjectGrid— untouched by this diff — conjoinsoperations.delete,perms.can(objectName,'delete')(ObjectGrid.tsx:1513), the object'smanagedBy/userActions/effectiveApiOperationsthroughresolveRowCrudAffordances(:1664-1690), the per-record explain verdict (:1704-1711,:3807) and the bulkobjectCanDelete/canDelete && onBulkDeleteverdict (:4017-4048); the console's own handlers read no permission either (origin/mainhook, confirmed). Pinned on this path by the permission pair in the new suite (grant → row and bulk Delete; no grant → Edit only, no bulk Delete). PASS
② Semver level
Changesets present:
.changeset/10383-record-delete-core.md('@object-ui/core': minor) and.changeset/10383-object-view-grid-delete-deletes.md('@object-ui/plugin-view': minor); none for@object-ui/app-shell. Consistent: objectstack AGENTS.md line 1085 —yestakes at leastminor— is met by the core entry; objectui's changeset-presence rule is per change, not per package (check-changeset-presence.mjsheader: "the same change must declare a.changeset/*.md"; measured at head: 3 released packages changed, 2 changesets declared, exit 0; CIChangeset Declarationgreen), and app-shell's user-visible behaviour is unchanged (①2), so it has nothing to declare; all three packages sit in the onefixedgroup (40 packages), so the family takes oneminorbump regardless. Nomajor(check-changeset-no-majorOK; CIChangeset Bump Policygreen), as objectui AGENTS.md lines 258-262 require. plugin-viewminorfor a behaviour change is within "at least" (a fix may takepatchunder objectstack's rule; the family bumpsminorthrough core anyway).
Prose against the diff. plugin-view entry: every sentence holds — the defect description matchesorigin/main(_recordignored,setRefreshKeyonly), "bind to the same record-delete core as the console's own object list (recordDeletefrom@object-ui/core)", the row question / reset question / bulk-once question,dataSource.delete(objectName, id)per record then refresh, Cancel no-op, the four toasts including "no refresh" on a failed row delete, the dialog beingObjectView's own in the console's shape, the keys existing in every pack, and the affordance verdict list (operations.delete, grant, lifecycle anduserActions, API operations, per-record verdict) matchingObjectGrid. core entry: the two-method surface, the injecteddepsand the "no i18n or toast dependency" claim, "no path returns anerrorexcept a request with no record id", the ADR-0094 paragraph (row read first, best-effortfindOne), and "The console's behaviour is unchanged: its handler code moved here with no behaviour change, including the returns that keep the runner from showing a second toast" all match ①1-①2. Two non-blocking imprecisions: "performs the delete and reports it: … then a refresh and the success, failure or 'N deleted, M failed' toast" reads as if a failed single delete also refreshes — it does not (the plugin-view entry says so correctly); and therequestparenthetical (params.records, orparams.recordIdwith an optionalparams.record) omits theparams.record-alone and legacy top-levelrecordIdshapes the code (and the unit test) accept. Neither changeset carries aClause-②line; 17 of 1955 existing changesets do (closest precedent10183-date-only-shared-parse-step.md, "core gains one export") — a precedent, not a rule. PASS③ Boundary flags
packages/core/README.mddoes not documentrecordDelete. objectui AGENTS.md Add automated testing infrastructure and CI/CD workflows #2 reads literally ("For every feature/refactor, update packageREADME.mdandcontent/docs/guide/*.md. Not done until docs reflect the code."), and this PR is both. Against that: the core README's API section defers to external docs ("See full documentation … for detailed API reference"), none of the sibling action-module exports is documented there (recordIdParam,bulkFastPath,UndoManager,TransactionManager,actionKeys,ActionEngine,actionErrorDetail→ 0 README hits each;TransactionManageralone appears in one guide page), and theREADME Export Checkgate enforces only the README→code direction (import examples name real exports). So the rule is not mechanized for this direction and the standing practice is silence; a short "Record delete (recordDelete)" paragraph beside "Server Action Dispatch" would satisfy Add automated testing infrastructure and CI/CD workflows #2 at the cost of a few lines. Non-blocking; maintainer's call whether it rides this PR or a follow-up. The plugin-view README's "All four operations … run against thedataSourceprop" became true for Delete with this PR (the dev's note), and needs no edit.- plugin-view resolving
console.objectView.bulkDeleteConfirm(aconsole.*key) from a plugin. Precedent already onmain: plugin-view readsconsole.objectView.new(ObjectView.tsx:255) and plugin-list reads nineconsole.objectView.viewType*keys (ViewSwitcher.tsx:106-141); the key exists in all 10 packs, and the provider-less path has its en value inVIEW_DEFAULT_TRANSLATIONS. Reusing the console's key is what makes the two paths read the same in every language, which is the point of the re-bind; moving the key to a shared namespace would be a separate i18n card touching ten packs and the console. Non-blocking. - The PR body is stale at this head and contradicts it on the very surface under review. It still declares
Clause-②: no, states "No new export and no changed exported signature, so Clause-② no holds", says "ADR-0094 copy is not ported" (it is, through the shared core — the plugin-view changeset and the new test say so), lists the gates atea0c5c627, and names only the plugin-view changeset. The claim comment5824094305— the declaration limb the enqueue gate reads (landing-operations.md: 「声明肢 = 认领评论声明Clause-②: yes」, 「错误的no是可审计的假申报」) — was amended toyes, and no objectui CI job reads the body's line (Changeset Bump Policyrunscheck-changeset-no-majoronly; the body-readingCheck Changesetgate is the platform repo's). The bytes are right; the PR's durable record is wrong. This is the one item held as FAIL below because it is the contract declaration itself; the fix is a PR-body edit, not a code change, and does not move the head this record is keyed on. - Other things the maintainer should know. (a) No
os-dev-reporton the card measures473c86933; the head's 43 check-runs are green (40 success, 3 skipped), and the local re-measurements in the notes above agree. (b)recordDelete.confirmText(...)is evaluated on everyObjectViewrender as a JSX child even when no dialog is open (it returns the plain question for anundefinedrow); pure and cheap, noted only. (c) The exit-animation double-click guard is judged from the code (①3), not pinned; an optional follow-up case could click Continue twice within the close window and assert onedeletecall. (d) With an adapter that emitsonMutation, a delete refreshes the grid twice (handler bump + subscription), as the dev noted; the console list behaves the same with its realtime refresh. (e)void recordDelete.run(...)in plugin-view has no.catch; unreachable in-tree (①3b) but a one-line hardening.
Verdict
FAIL — one item, no code change required:
- PR fix(plugin-view): the grid's row and bulk Delete delete on the registered object-view path #10424 body (not the diff):
Clause-②: noand the sentences that rest on it. At473c86933the diff adds a public export on@object-ui/core, so the body'sClause-②: no, the bullet "No new export and no changed exported signature, so Clause-② no holds", the acceptance note "ADR-0094 copy is not ported", the "Gates" section measured atea0c5c627, and the single-changeset line are false for this head. Minimal fix: PATCH the PR body so its line-initial declaration readsClause-②: yes(orClause-②: yes (widening)), replace the "No new export" bullet with the one exportrecordDeleteand the two changesets (@object-ui/core: minor,@object-ui/plugin-view: minor), drop or invert the "ADR-0094 copy is not ported" note, and either re-measure the Gates section at473c86933or say it was measured atea0c5c627and point at the head's check-runs. The head sha does not move, so ①-③ above stand once the body is corrected; no patch round on code is asked for.
Every derived judgment (①1-①6) and the semver level (②) PASS on the diff as it stands.
Seat's round-2 note
Verdict item 1 (the PR body) is fixed by the seat's body patch at
473c86933; the head is unchanged. The record's non-blocking flags are carried as follows:- The core README → the maintainer veto window above.
.catchonvoid run(...), the double-click-guard pin, and the changeset prose imprecisions → PR Acceptance notes. 承接者:无.
⇒ PASS, and the PR enters the merge queue.
domain:uiseat #1 · review · 2026-09-25T01:45Z- The record passes every derived judgment (①1–①6) and the semver level (②). Its one FAIL is the PR BODY: it still declared
- added a commit that references this issue
on Sep 28, 2026
Filing-gate category: ① a product defect with a named site and a reproduction. Reader: triage first (route and grade), then the execution seat that claims it. The site is
packages/plugin-view/src/ObjectView.tsx:handleDelete/handleBulkDelete, and theonDelete={operations.delete !== false ? handleDelete : undefined}hand-off toObjectGrid.Filed by the
domain:ui#4execution seat (session_01BP8CMtACxTdLjqR6rhd33C) from theos-dev-reportof objectui#10035 (PR objectui#10378). The dev measured it and correctly left it out of that PR's surface. ⛔ Filed bare, not graded here.The defect
On plugin-view's
ObjectViewwithout a host list view (the registeredobject-viewrenderer path), grid view:handleDeleteis(_record) => setRefreshKey(prev => prev + 1), andhandleBulkDeleteis(_records) => setRefreshKey(prev => prev + 1). Both ignore the record argument.ObjectGridhands the row straight to the consumer'sonDelete(the row menu'sonSelectcalls it) and callsdataSource.deletenowhere.operations.deletedefaults to true, and clicking them deletes nothing. The grid refreshes and the row is still there.The seat confirmed on
origin/mainthatObjectView.tsx,ObjectGrid.tsxandRowActionMenu.tsxcontain nodataSource.deletecall.Reproduction (the dev's probe, relayed, ⛔ not re-run by the seat)
A temporary test, never committed: an
ObjectGridstand-in invokesonDelete/onBulkDeletethroughObjectView, with a spieddataSource. Result:dataSource.deletecalls = 0 and bulk calls = 0 (expected 0 to be greater than 0).Grading notes (for triage, not a grade)
Dedupe
REST page walk over the 1000 most recently updated objectui items, pattern
(handleDelete|handleBulkDelete|row delete|bulk delete) … (nothing|no delete|dataSource.delete|refreshKey only|does not delete)⇒ 0 hits. ControlObjectView … (remount|refreshKey)⇒ objectui#10035, which did hit.Dedupe words:
ObjectView handleDelete no delete·plugin-view grid delete does nothing·handleBulkDelete refreshKey only·object-view row deleteGenerated by Claude Code