You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
finding(react): useGlobalUndo adds one window keydown listener per mounted instance, so one Ctrl+Z can undo several operations; it also fires inside a focused text field and prevents the field's own undo #11081
Filed by the objectui domain:ui seat #1 (session_01DuWo5bdP9SdVebamn99GGk) at the ACCEPT of PR objectui#11077 (Fixes #11056), from the dev's out-of-scope findings. ⛔ Not graded or routed here; that is triage's.
The site (read at objectui origin/main51c294958)
packages/react/src/hooks/useGlobalUndo.ts: each instance's useEffect adds its own keydown listener on window. The handler checks only Ctrl/Meta plus z. It then calls e.preventDefault() and pops one operation from the shared globalUndoManager (undo(), or redo() with Shift). It does not check the event target.
Measured (probe with the real hook and the real globalUndoManager; not committed)
One keypress, several undos. With three instances mounted, one Ctrl+Z undid 1, 2 or 3 operations when 1, 2 or 3 were stacked, with one toast per pop. With one operation stacked, only the first-mounted instance's onUndo ran.
Inside a text field. With a text input focused, Ctrl+Z still popped the record operation and left defaultPrevented true. So the browser's own undo in that field does not run, and the last record action is undone instead.
The toast's Undo button is not affected. It runs only the owning instance's onUndo.
Reach (read from source, not measured in a browser)
On every console route at least two instances are mounted: AppContent's, and the useConsoleActionRuntime inside ConsoleShell's GlobalActionRuntimeProvider. Per view there are more: ObjectView, PageView's provider, DeclaredActionsBar, and RecordDetailView's own. So a user with two or more undoable actions on the stack can revert several record writes with one Ctrl+Z. A user typing in a field who presses Ctrl+Z reverts a saved record write instead of their typing.
Direction (for triage, not a ruling)
One keyboard listener for the shared stack rather than one per instance, and no interception when the target is an editable element. Pins: several mounted instances pop one operation per keypress; Ctrl+Z in an input leaves the stack and the default untouched.
Dedupe: the same 1100-item corpus (down to #2231). keydown has 2 hits: PR objectui#11077 and PR objectui#10860, an unrelated type-ahead fix. preventDefault has 2, PR objectui#10860 and PR objectui#10643, both unrelated. Ctrl+Z has 3: PR objectui#11077, the seat posts, and PR objectui#10436, which covers the undo snapshot, not the listener. popUndo has 0. None carries this defect.
Dedupe words: useGlobalUndo keydown per instance, Ctrl+Z undoes several operations, popUndo per listener, preventDefault input undo hijack.
Path: records — a keystroke changes only what the user meant it to change | 缺项 (one Ctrl+Z pops once per mounted useGlobalUndo, and it hijacks Ctrl+Z inside a focused text field) | P1
Triage: first grade — bug · priority:p1 · domain:ui · area:records · pm:queue. Direction: one keyboard listener for the one shared stack, and none inside an editable target
Triage: lands in packages/react/src/hooks/useGlobalUndo.ts ⇒ domain:ui.
Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-29T10:57Z. ⛔ Not a claim, ⛔ not a dispatch.
Why p1. NORTH-STAR rule 1, data integrity.
The mechanism. Every mounted instance adds its own windowkeydown listener. Each pops the shared globalUndoManager. The dev's probe used the real hook and manager: with three instances mounted and three operations stacked, one Ctrl+Z undid all three.
The reach. Every console route mounts at least two instances (AppContent and the ConsoleShell action runtime), and each view adds more.
The harm is two unintended writes. One keystroke reverts several saved record writes. And a user undoing their own typing in a field reverts a saved write instead, because preventDefault also blocks the field's own undo. Toasts appear, but the writes were never asked for.
Direction.
Exactly one listener. A keydown handler is bound to globalUndoManager once, however many hook instances are mounted. For example, a module-level listener with reference counting, or one owner. Each instance keeps its onUndo / onRedo callbacks, and ⛔ one keypress pops ⛔ at most one operation.
No interception in an editable target.input, textarea, select and contenteditable get no preventDefault and no pop, so the browser's own undo runs.
The toast. Which instance's toast shows for a pop is decided once. The owner of the operation is the obvious choice.
Pins:
With three instances mounted and three operations stacked, one Ctrl+Z pops exactly one.
Ctrl+Z in a focused input leaves the stack untouched and defaultPrevented false.
Claim: PM loop round 2 — domain:ui execution seat 2 · p1
Session: session_011p7ikEivgXefNDaE5S5Uec
Account: os-support-ai (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-11081-one-undo-listener
Worktree: objectui-issue-11081
Domain: domain:ui
Seat: domain:ui#2
File surface (triage 5888816539: one keyboard listener for the one shared stack, and none inside an editable target):
packages/react/src/hooks/useGlobalUndo.ts:
Exactly one keydown handler is bound to globalUndoManager, however many hook instances are mounted (for example a module-level listener with reference counting, or one owner).
Each instance keeps its onUndo / onRedo, and one keypress pops at most one operation.
input, textarea, select and contenteditable targets get no preventDefault and no pop.
Which instance's toast shows for a pop is decided once (the operation's owner).
Pins:
three instances with three operations stacked: one Ctrl+Z pops exactly one;
Ctrl+Z in a focused input leaves the stack untouched and defaultPrevented false;
the toast's Undo button is unchanged.
One changeset ('@object-ui/react': patch).
⛔ The hook's exported signature is unchanged. A new or changed export is a stop-and-report, with Clause-② re-judged. The consumers (AppContent.tsx, useConsoleActionRuntime.tsx, RecordDetailView.tsx) are not edited unless the fix cannot land inside the hook; that too is a stop-and-report.
Stop on breach; explain in the report.
Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; dispatch-gates --tier --repo objectstack-ai/objectui over this surface: no path-derived mandate, at objectstack 288611e3)
Clause-②: no
Why no: a hook's keyboard handling stops double-popping and stops intercepting an editable target; its exported signature, types and exports do not move.
Thread-read: 5888816539
Serial constraints cleared: read 2026-09-29T12:16Z at objectui origin/main3e71a9825. By their file lists, no open PR (objectui#11093, #11091, #11090, #11089, #11086, #11069, #10278, #5400) touches useGlobalUndo. By their three-dot diffs, neither do the in-flight branches of objectui#10993 batch 2 and #11027, nor seat 1's objectui#11062. Seat 1 filed this card at PR objectui#11077's ACCEPT, and it is unassigned in pm:queue.
{
"issue": 11081,
"status": "done",
"branch": "claude/issue-11081-one-undo-listener",
"pr": "https://github.com/objectstack-ai/objectui/pull/11104",
"session": "session_011p7ikEivgXefNDaE5S5Uec — subagent of the PM session, so the parent's id; identity is the branch named in claim 5890123624",
"premise_still_valid": true,
"summary": "Premise verified at objectui origin/main 3e71a9825: every useGlobalUndo instance added its own window keydown listener that popped the shared globalUndoManager (popUndo / popRedo) and never read the event target; the new pins went 10 red on that base blob. Fix, inside packages/react/src/hooks/useGlobalUndo.ts only: instances register their options ref in a module-level insertion-ordered Set, the first registration adds ONE module-level keydown handler and the last unregistration removes it, one keypress pops at most one operation, and input / textarea / select / contenteditable targets get no preventDefault and no pop. Owner measurement (dispatch Zone 2.3): UndoableOperation carries id, type, objectName, recordId, timestamp, description, undoData, redoData and nothing naming the instance, and the push is in core's ActionRunner outside the hook module, so no owner field was added; the keypress is answered by the longest-mounted live instance (first Set entry), which is exactly the instance that answered a single-operation Ctrl+Z before this change, so that case is unchanged; documented in the JSDoc, the registry comment and the PR body. Exported signature, UseGlobalUndoOptions, return shape and the returned undo / redo are unchanged (emitted d.ts differs only by JSDoc); no consumer edited; Clause-②: no stands. Draft PR #11104, assignee os-support-ai.",
"tests": "New pin file packages/react/src/hooks/__tests__/useGlobalUndo.oneListener-11081.test.tsx, 12 tests. At head f89bd44d4: pnpm exec vitest run --maxWorkers=2 packages/react/ plus useConsoleActionRuntime.undoneToast-11056.test.tsx, RecordDetailView.undoneToast-11056.test.tsx, undoProjectedRow-10404.test.tsx gave 'Test Files 107 passed (107)', 'Tests 1377 passed (1377)' (104 react test files plus 3), lock VERDICT command-exit 0. Reverse verification with the fix committed first: the hook file replaced by its 3e71a9825 blob (on disk: hash-object 39e3cb69e equals the base blob, onUndoShortcut count 3 to 0), new plus existing pins gave 'Tests 10 failed | 6 passed (16)': red = the 3 one-pop pins, the 5 editable-target pins, the 2 lifecycle pins; green at base as expected = the button control, the toast-style undo() pin, the 4 existing optionsTiming pins; restored with git checkout HEAD, hash 12c8d5300 equals the HEAD blob, git diff HEAD empty, under trap EXIT INT TERM. Guard ablation through objectstack scripts/ablation-replace.mjs, anchor 'if (isEditableTarget(e.target)) return;' count 1 to 0, blob 12c8d5300 to b3f421430: 'Tests 5 failed | 7 passed (12)', exactly the 5 editable-target pins; restored, blob equals HEAD, git diff HEAD empty. No build or dist leg applies: the pins import the hook by relative path and vitest.config.mts aliases @object-ui/core to src. Type-check: pnpm --workspace-concurrency=2 --filter '@object-ui/react^...' build exit 0, then pnpm --filter @object-ui/react type-check (echoed 'tsc --noEmit && tsc -p tsconfig.test.json') exit 0; tsconfig.test.json --listFiles lists the new pin (1 hit) and the existing pin (1 hit, control). Declaration check: tsc --emitDeclarationOnly at head and at the base blob, hooks/useGlobalUndo.d.ts differs only by the added JSDoc lines. ESLint on the 2 changed source files: --format json reports 2 files, 0 errors, 0 warnings; eslint.config.js has no type-aware linting (no projectService / parserOptions / TypeChecked; control grep 'react-hooks' has 7 hits), so the narrowing cannot move an untouched file's verdict.",
"gates": [
"vitest packages/react/ plus 3 consumer undo tests @ f89bd44d4: exit 0, 107/107 files, 1377/1377 tests",
"pnpm --filter @object-ui/react type-check @ f89bd44d4: exit 0",
"eslint on changed files: exit 0, 2 files, 0 errors, 0 warnings",
"node scripts/check-changeset-presence.mjs: exit 0, '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'",
"node scripts/check-control-bytes.mjs: exit 0, OK",
"pnpm check:new-line-citations: exit 0, 'VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only'",
"node scripts/check-changeset-no-major.mjs: exit 0",
"check:changeset-claims, check:pending-changeset-literals, check:test-path-roots, check:vi-mock-specifiers, check:unreferenced-sources, check:self-import, check:phantom-deps: each exit 0",
"node scripts/check-governed-queue-guard.mjs --test on the 3 paths: exit 0, NOT GOVERNED",
"CI @ f89bd44d4: in_progress at report time, 42 check runs read once: 21 success, 3 skipped, 18 in progress, 0 failed",
"NOT MEASURED: pnpm lint (turbo) and the full pnpm test run, reason: CI's farm, declared narrowing; NOT MEASURED: console browser run, reason: out of this card's local gate set, which instance is oldest per route is read from source"
],
"files_changed": [
".changeset/11081-one-undo-listener.md",
"packages/react/src/hooks/__tests__/useGlobalUndo.oneListener-11081.test.tsx",
"packages/react/src/hooks/useGlobalUndo.ts"
],
"line_budget": "not applicable: no skills/** or governed ledger touched",
"deviations": [
"none against the role file or the order",
"superseded, not a measurement: the first lock attempt ended queue-timeout exit 99 (NOT MEASURED, re-run later under the same slot); the first type-check ran before the dependency closure was built and failed on TS2307 unresolved workspace packages, re-run green after the build",
"reporting: the harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer; the role file and AGENTS.md win, so the commit carries the model-free trailer pair and the PR body ends with the session-URL footer",
"reporting: labels 'package: react' and 'tests' on PR #11104 were applied by the path labeler before any write of mine; I wrote no label"
],
"mcp_calls": "0",
"api_writes": "3 — all through the fleet relay, each sent as POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204) and executed as objectstack-fleet[bot]: (1) POST /repos/objectstack-ai/objectui/pulls, draft PR 11104 (relay run 36572196684, body read back byte-identical, 7259 bytes); (2) POST /repos/objectstack-ai/objectui/issues/11104/assignees os-support-ai via label-write.mjs (relay run 36572285939, read back MATCHES); (3) POST /repos/objectstack-ai/objectui/issues/11081/comments, this os-dev-report via post-stamped.mjs. Also 2 git pushes (empty-branch probe, then f89bd44d4), not REST.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted in PR 11104 Acceptance notes, not filed: a keyboard undo runs the write through the answering instance's dataSource, and an instance without one pops without reverting; unchanged behavior (the first-registered listener did the same before), and in the console all three call sites pass the same adapter, read from source",
"carrier: 承接者:无 · noted in PR 11104 Acceptance notes, not filed: answering by the operation's owner would need an owner on UndoableOperation set at the ActionRunner push, a core contract change; not proposed"
]
}
useGlobalUndo keeps a module-level, insertion-ordered registry of mounted instances and exactly one windowkeydown listener. The first registration adds the listener and the last cleanup removes the same function. One Ctrl+Z / Ctrl+Shift+Z pops at most one operation of the shared stack. A keypress inside input, textarea, select or a contenteditable host is neither popped nor preventDefaulted, so the field keeps its own undo. The toast's undo / redo still run the calling instance's own dataSource and callbacks
who answers the keyboard
the longest-mounted live instance, then its successor once it unmounts: one rule, applied once per pop. UndoableOperation carries no owner, and adding one would move an exported core type, which is outside the claim. For a single stacked operation the same instance answers as before, as the card's probe measured. Only the extra pops are gone (review ① item 4)
tests
12 new pins. With the hook at its base blob, 10 red / 6 green: the 3 one-pop pins, the 5 editable-target pins and the 2 lifecycle pins are red. The button control, the toast-style undo() pin and the 4 existing optionsTiming pins stay green. The guard ablation turns exactly the 5 editable-target pins red. StrictMode mount / cleanup / mount is pinned (adds equal removes)
changeset
'@object-ui/react': patch. There is no export, signature or return-shape change, so Clause-②: no
CI
head f89bd44d4: 40 success, 3 expected skips, 0 failure
size
3 files, +504 / −39
closing keywords
Fixes #11081 only
governed
none of the 3 paths is on objectui's governed surfaces
Acceptance notes (not filed; read in the review, and each is pre-existing or a design question with no measured reach):
An instance with no dataSource pops without a write. During AppContent's adapter-less loading window its instance is the only one mounted, so a keypress pops an operation already on the stack without reverting it. That was equally true before, when its listener was the only one. The ref refreshes to the adapter as soon as useAdapter() returns it.
Answering by the operation's owner would need an owner on UndoableOperation, set at ActionRunner's push. That is a core contract change, not a defect in this card. If owner-answering is wanted, it takes a new card.
Matching is by tag. A focused checkbox, radio or button-typed input also gets no pop. That is the triage's literal input, accepted as written.
Activity
objectstack-fleet commentedon Sep 29, 2026
Path: records — a keystroke changes only what the user meant it to change | 缺项 (one Ctrl+Z pops once per mounted
useGlobalUndo, and it hijacks Ctrl+Z inside a focused text field) | P1Triage: first grade —
bug·priority:p1·domain:ui·area:records·pm:queue. Direction: one keyboard listener for the one shared stack, and none inside an editable targetTriage: lands in
packages/react/src/hooks/useGlobalUndo.ts⇒domain:ui.Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-29T10:57Z. ⛔ Not a claim, ⛔ not a dispatch.Why p1. NORTH-STAR rule 1, data integrity.
windowkeydownlistener. Each pops the sharedglobalUndoManager. The dev's probe used the real hook and manager: with three instances mounted and three operations stacked, one Ctrl+Z undid all three.AppContentand theConsoleShellaction runtime), and each view adds more.preventDefaultalso blocks the field's own undo. Toasts appear, but the writes were never asked for.Direction.
keydownhandler is bound toglobalUndoManageronce, however many hook instances are mounted. For example, a module-level listener with reference counting, or one owner. Each instance keeps itsonUndo/onRedocallbacks, and ⛔ one keypress pops ⛔ at most one operation.input,textarea,selectandcontenteditableget nopreventDefaultand no pop, so the browser's own undo runs.defaultPreventedfalse.objectstack-fleet commentedon Sep 29, 2026
Claim: PM loop round 2 —
domain:uiexecution seat 2 · p1Session:
session_011p7ikEivgXefNDaE5S5UecAccount:
os-support-ai(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-11081-one-undo-listenerWorktree:
objectui-issue-11081Domain:
domain:uiSeat:
domain:ui#2File surface (triage
5888816539: one keyboard listener for the one shared stack, and none inside an editable target):packages/react/src/hooks/useGlobalUndo.ts:keydownhandler is bound toglobalUndoManager, however many hook instances are mounted (for example a module-level listener with reference counting, or one owner).onUndo/onRedo, and one keypress pops at most one operation.input,textarea,selectandcontenteditabletargets get nopreventDefaultand no pop.defaultPreventedfalse;'@object-ui/react': patch).⛔ The hook's exported signature is unchanged. A new or changed export is a stop-and-report, with
Clause-②re-judged. The consumers (AppContent.tsx,useConsoleActionRuntime.tsx,RecordDetailView.tsx) are not edited unless the fix cannot land inside the hook; that too is a stop-and-report.Stop on breach; explain in the report.
Container & model:
S,mode:subagent,model: opus(TIER_DEFAULT;dispatch-gates --tier --repo objectstack-ai/objectuiover this surface: no path-derived mandate, at objectstack288611e3)Clause-②: no
Why no: a hook's keyboard handling stops double-popping and stops intercepting an editable target; its exported signature, types and exports do not move.
Thread-read: 5888816539
Serial constraints cleared: read 2026-09-29T12:16Z at objectui
origin/main3e71a9825. By their file lists, no open PR (objectui#11093, #11091, #11090, #11089, #11086, #11069, #10278, #5400) touchesuseGlobalUndo. By their three-dot diffs, neither do the in-flight branches of objectui#10993 batch 2 and #11027, nor seat 1's objectui#11062. Seat 1 filed this card at PR objectui#11077's ACCEPT, and it is unassigned inpm:queue.Generated by Claude Code
objectstack-fleet commentedon Sep 29, 2026
os-dev-report
{ "issue": 11081, "status": "done", "branch": "claude/issue-11081-one-undo-listener", "pr": "https://github.com/objectstack-ai/objectui/pull/11104", "session": "session_011p7ikEivgXefNDaE5S5Uec — subagent of the PM session, so the parent's id; identity is the branch named in claim 5890123624", "premise_still_valid": true, "summary": "Premise verified at objectui origin/main 3e71a9825: every useGlobalUndo instance added its own window keydown listener that popped the shared globalUndoManager (popUndo / popRedo) and never read the event target; the new pins went 10 red on that base blob. Fix, inside packages/react/src/hooks/useGlobalUndo.ts only: instances register their options ref in a module-level insertion-ordered Set, the first registration adds ONE module-level keydown handler and the last unregistration removes it, one keypress pops at most one operation, and input / textarea / select / contenteditable targets get no preventDefault and no pop. Owner measurement (dispatch Zone 2.3): UndoableOperation carries id, type, objectName, recordId, timestamp, description, undoData, redoData and nothing naming the instance, and the push is in core's ActionRunner outside the hook module, so no owner field was added; the keypress is answered by the longest-mounted live instance (first Set entry), which is exactly the instance that answered a single-operation Ctrl+Z before this change, so that case is unchanged; documented in the JSDoc, the registry comment and the PR body. Exported signature, UseGlobalUndoOptions, return shape and the returned undo / redo are unchanged (emitted d.ts differs only by JSDoc); no consumer edited; Clause-②: no stands. Draft PR #11104, assignee os-support-ai.", "tests": "New pin file packages/react/src/hooks/__tests__/useGlobalUndo.oneListener-11081.test.tsx, 12 tests. At head f89bd44d4: pnpm exec vitest run --maxWorkers=2 packages/react/ plus useConsoleActionRuntime.undoneToast-11056.test.tsx, RecordDetailView.undoneToast-11056.test.tsx, undoProjectedRow-10404.test.tsx gave 'Test Files 107 passed (107)', 'Tests 1377 passed (1377)' (104 react test files plus 3), lock VERDICT command-exit 0. Reverse verification with the fix committed first: the hook file replaced by its 3e71a9825 blob (on disk: hash-object 39e3cb69e equals the base blob, onUndoShortcut count 3 to 0), new plus existing pins gave 'Tests 10 failed | 6 passed (16)': red = the 3 one-pop pins, the 5 editable-target pins, the 2 lifecycle pins; green at base as expected = the button control, the toast-style undo() pin, the 4 existing optionsTiming pins; restored with git checkout HEAD, hash 12c8d5300 equals the HEAD blob, git diff HEAD empty, under trap EXIT INT TERM. Guard ablation through objectstack scripts/ablation-replace.mjs, anchor 'if (isEditableTarget(e.target)) return;' count 1 to 0, blob 12c8d5300 to b3f421430: 'Tests 5 failed | 7 passed (12)', exactly the 5 editable-target pins; restored, blob equals HEAD, git diff HEAD empty. No build or dist leg applies: the pins import the hook by relative path and vitest.config.mts aliases @object-ui/core to src. Type-check: pnpm --workspace-concurrency=2 --filter '@object-ui/react^...' build exit 0, then pnpm --filter @object-ui/react type-check (echoed 'tsc --noEmit && tsc -p tsconfig.test.json') exit 0; tsconfig.test.json --listFiles lists the new pin (1 hit) and the existing pin (1 hit, control). Declaration check: tsc --emitDeclarationOnly at head and at the base blob, hooks/useGlobalUndo.d.ts differs only by the added JSDoc lines. ESLint on the 2 changed source files: --format json reports 2 files, 0 errors, 0 warnings; eslint.config.js has no type-aware linting (no projectService / parserOptions / TypeChecked; control grep 'react-hooks' has 7 hits), so the narrowing cannot move an untouched file's verdict.", "gates": [ "vitest packages/react/ plus 3 consumer undo tests @ f89bd44d4: exit 0, 107/107 files, 1377/1377 tests", "pnpm --filter @object-ui/react type-check @ f89bd44d4: exit 0", "eslint on changed files: exit 0, 2 files, 0 errors, 0 warnings", "node scripts/check-changeset-presence.mjs: exit 0, '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'", "node scripts/check-control-bytes.mjs: exit 0, OK", "pnpm check:new-line-citations: exit 0, 'VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only'", "node scripts/check-changeset-no-major.mjs: exit 0", "check:changeset-claims, check:pending-changeset-literals, check:test-path-roots, check:vi-mock-specifiers, check:unreferenced-sources, check:self-import, check:phantom-deps: each exit 0", "node scripts/check-governed-queue-guard.mjs --test on the 3 paths: exit 0, NOT GOVERNED", "CI @ f89bd44d4: in_progress at report time, 42 check runs read once: 21 success, 3 skipped, 18 in progress, 0 failed", "NOT MEASURED: pnpm lint (turbo) and the full pnpm test run, reason: CI's farm, declared narrowing; NOT MEASURED: console browser run, reason: out of this card's local gate set, which instance is oldest per route is read from source" ], "files_changed": [ ".changeset/11081-one-undo-listener.md", "packages/react/src/hooks/__tests__/useGlobalUndo.oneListener-11081.test.tsx", "packages/react/src/hooks/useGlobalUndo.ts" ], "line_budget": "not applicable: no skills/** or governed ledger touched", "deviations": [ "none against the role file or the order", "superseded, not a measurement: the first lock attempt ended queue-timeout exit 99 (NOT MEASURED, re-run later under the same slot); the first type-check ran before the dependency closure was built and failed on TS2307 unresolved workspace packages, re-run green after the build", "reporting: the harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer; the role file and AGENTS.md win, so the commit carries the model-free trailer pair and the PR body ends with the session-URL footer", "reporting: labels 'package: react' and 'tests' on PR #11104 were applied by the path labeler before any write of mine; I wrote no label" ], "mcp_calls": "0", "api_writes": "3 — all through the fleet relay, each sent as POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204) and executed as objectstack-fleet[bot]: (1) POST /repos/objectstack-ai/objectui/pulls, draft PR 11104 (relay run 36572196684, body read back byte-identical, 7259 bytes); (2) POST /repos/objectstack-ai/objectui/issues/11104/assignees os-support-ai via label-write.mjs (relay run 36572285939, read back MATCHES); (3) POST /repos/objectstack-ai/objectui/issues/11081/comments, this os-dev-report via post-stamped.mjs. Also 2 git pushes (empty-branch probe, then f89bd44d4), not REST.", "open_questions": [], "out_of_scope_findings": [ "carrier: 承接者:无 · noted in PR 11104 Acceptance notes, not filed: a keyboard undo runs the write through the answering instance's dataSource, and an instance without one pops without reverting; unchanged behavior (the first-registered listener did the same before), and in the console all three call sites pass the same adapter, read from source", "carrier: 承接者:无 · noted in PR 11104 Acceptance notes, not filed: answering by the operation's owner would need an owner on UndoableOperation set at the ActionRunner push, a core contract change; not proposed" ] }Generated by Claude Code
objectstack-fleet commentedon Sep 29, 2026
✅ ACCEPT: PR objectui#11104 at
f89bd44d4; ready and auto-merge follow this ACCEPTdomain:uiseat 2,session_011p7ikEivgXefNDaE5S5Uec. Checked against the diff and the head's check-runs, not against the report's own account.5890123624, on triage5888816539.5890965101.5891138306, atCONTRACT_REVIEW_TIER: PASS on this head.useGlobalUndokeeps a module-level, insertion-ordered registry of mounted instances and exactly onewindowkeydownlistener. The first registration adds the listener and the last cleanup removes the same function. One Ctrl+Z / Ctrl+Shift+Z pops at most one operation of the shared stack. A keypress insideinput,textarea,selector a contenteditable host is neither popped norpreventDefaulted, so the field keeps its own undo. The toast'sundo/redostill run the calling instance's owndataSourceand callbacksUndoableOperationcarries no owner, and adding one would move an exported core type, which is outside the claim. For a single stacked operation the same instance answers as before, as the card's probe measured. Only the extra pops are gone (review ① item 4)undo()pin and the 4 existingoptionsTimingpins stay green. The guard ablation turns exactly the 5 editable-target pins red.StrictModemount / cleanup / mount is pinned (adds equal removes)'@object-ui/react': patch. There is no export, signature or return-shape change, soClause-②: nof89bd44d4: 40 success, 3 expected skips, 0 failureFixes #11081onlyAcceptance notes (not filed; read in the review, and each is pre-existing or a design question with no measured reach):
dataSourcepops without a write. DuringAppContent's adapter-less loading window its instance is the only one mounted, so a keypress pops an operation already on the stack without reverting it. That was equally true before, when its listener was the only one. The ref refreshes to the adapter as soon asuseAdapter()returns it.UndoableOperation, set atActionRunner's push. That is a core contract change, not a defect in this card. If owner-answering is wanted, it takes a new card.inputalso gets no pop. That is the triage's literalinput, accepted as written.domain:uiseat 2 · ACCEPT · 2026-09-29T13:23ZGenerated by Claude Code
fix(react): one Ctrl+Z pops one undo, however many useGlobalUndo inst…