Skip to content

feat(console): tell the comment author, in one sentence, which mentioned users will not be notified; draw picked mentions as chips - #28261

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-c68o40
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-c68o40

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #28218
Refs #27703

Clause-②: yes (widening: @object-ui/data-objectstack ObjectStackAdapter.create gains an optional third parameter opts.onMentionsWithheld, and .update's existing opts gains the same optional member; both still resolve the record, and no export, key or route is added)
Metadata-protocol: none

维护者速读

  • 改了什么:在记录讨论里 @ 了一个看不到这条记录的同事时,评论照常发出,同时弹出一句提示:「Grace Hopper、Linus 不会收到这条评论的通知。」发评论、回复、编辑时新加 @ 三处都会提示;不说原因,没有被拦下的人就不提示。输入框下方把已 @ 的人显示为标签,预览里的 @姓名 也显示为标签。
  • 为什么改:按 comments: the @mention picker offers a member who cannot read the thread, and the author is never told that person will not be notified #27703 裁决 A(「27703 同意」),服务端已在 feat(spec): a comment's author is told which mentioned users will not be notified, and a mention added by editing notifies #28174 把被拦下的人的 id 回给作者本人的写入答复(withheldMentions),但控制台一直不读,作者以为对方已被通知。
  • 风险与回滚:数据适配器的 create / update 多了一个可选的逐次回调,返回值不变,其他调用方不受影响;回调抛错不会让已保存的评论被当成失败。回滚即 revert 本 PR,服务端不受影响。
  • 先看:ui/packages/app-shell/src/views/RecordDetailView.tsx(tellMentionsWithheld 与三个评论写回调);ui/packages/data-objectstack/src/index.ts(notifyWithheldMentions);ui/packages/plugin-detail/src/RichTextCommentInput.tsx(标签行与预览)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/RecordDetailView.withheldMentions-28218.test.tsx packages/data-objectstack/src/withheldMentions-28218.test.ts packages/plugin-detail/src/__tests__/RichTextCommentInput.mentionChips-28218.test.tsx,预期 3 个文件、14 个测试全绿。
  • 需要你做的:无,席位落地。

What changes

The answer reaches the caller (ui/packages/data-objectstack/src/index.ts). Measured first: ObjectStackAdapter.create / .update resolved result.record and dropped every other member of the write answer, so withheldMentions never reached the record page. Both methods now take an optional per-call listener, opts.onMentionsWithheld(ids) (on update beside ifMatch, which still reaches the client alone). A module helper, notifyWithheldMentions, reads the answer structurally, keeps the string ids, and calls the listener once after the write succeeded, only when at least one id is named. A runtime that predates the key, or a write that withheld nobody, calls nothing. A throwing listener is isolated like the subscriber fan-outs, so a stored comment is never reported as failed. What every caller resolves is unchanged. A per-call listener was chosen over the global onWriteWarning channel because the sentence needs the picker's names, which only the writing page holds.

One sentence, no reason (ui/packages/app-shell/src/views/RecordDetailView.tsx). tellMentionsWithheld maps the ids to names through the mention picker's suggestions (the list the write's mentions were extracted from) and raises one toast.warning: detail.mentionsWithheld, "NAMES will not be notified of this comment.", the names joined with the pack's validation.formInvalidJoiner, the list punctuation the record feed already uses. The three sys_comment writes pass it: handleAddComment, handleAddReply and handleEditComment. Per the ruling the sentence gives no reason (one bit per named user), and nothing probes a colleague's access before the write (B was not taken).

Mention chips (ui/packages/plugin-detail/src/RichTextCommentInput.tsx). Under the editor, the users the text mentions are a list of chips labelled detail.mentionedPeople, resolved with the same extractMentions the write uses, so the row shows exactly whom the comment's mentions will carry, multi-word names whole. In the preview, each picked @NAME is one chip (longest name first, not followed by a letter or digit, as extractMentions matches); any other @word keeps the plain highlight it had. The preview escapes the text before any markup, chips included.

Copy. detail.mentionsWithheld and detail.mentionedPeople in all ten packs. The sentence is phrased so it needs no plural forms in any pack. detail.mentionedPeople is also in DETAIL_DEFAULT_TRANSLATIONS (the provider-less mirror of the en pack).

File surface. Within the order's list: RecordDetailView.tsx (the three callbacks and one helper), RichTextCommentInput.tsx, data-objectstack/src/index.ts, the ten packs, tests and the root changeset. Outside it, both small: plugin-detail/src/useDetailTranslation.ts (one row, the chip row's label, which the defaults-mirror test requires beside the pack) and ui/packages/data-objectstack/README.md (the method list names the new option, per ui/AGENTS.md #2). No packages/spec/** and no server change.

Serial constraints. Neither #28116 nor #27713 has landed (both open at the time of writing); this branch is on main 86cf7cef. The edits in RecordDetailView.tsx are the helper and three call sites, in hunks neither PR touches as far as a read of their descriptions shows; whichever lands first, this branch merges origin/main and resolves.

Tests

New:

  • ui/packages/data-objectstack/src/withheldMentions-28218.test.ts (4): create hands the ids to the listener once and still resolves the plain record, and the listener is never forwarded to the client; update hands them on and ifMatch still reaches the client alone (no options bag without it); CONTROL: an answer without the key, with an empty list, or with no string ids calls nothing; a write without the listener resolves as before, and a throwing listener does not reject a stored write.
  • ui/packages/app-shell/src/views/RecordDetailView.withheldMentions-28218.test.tsx (5), through the real RecordDetailView, its composer, reply input and edit form, under a real I18nProvider with the en pack read by key: a comment names the withheld users by their picker names (a multi-word name included) and is posted; only the withheld users are named; CONTROL: a comment whose mentions all reach their users says nothing; a reply names them; an edit that adds a mention names them.
  • ui/packages/plugin-detail/src/__tests__/RichTextCommentInput.mentionChips-28218.test.tsx (5): the chip row names each picked user once, multi-word names whole; it follows the text and is absent with no mention; a prefix of a longer name or an @word naming nobody is no chip; the preview draws a picked @NAME as one chip and keeps other @words plain; the preview escapes the text.

Updated: three assertions that pinned the sys_comment create's exact arguments now include the per-call listener (commentWriteFailure-10899 ×2, feedRecordScope). Their behaviour is unchanged.

Ablations (each through scripts/ablation-replace.mjs, mutation proved on disk, restored to the HEAD blob):

  • RecordDetailView.tsx, the comment write without onMentionsWithheld → RecordDetailView.withheldMentions-28218 2 failed / 3 passed (the two comment cases); restored, blob 6cf08c0a = HEAD.
  • data-objectstack/src/index.ts, notifyWithheldMentions not calling the listener → withheldMentions-28218 3 failed / 1 passed (the control stays green, as it should); restored, blob 20fba8d2 = HEAD.

Acceptance notes

  • Live, on a stock stack (pnpm dev -- --fresh, showcase, at this branch's build, 2026-10-30T20:11Z). As the seeded admin, a user created through POST /api/v1/auth/admin/create-user with no membership (membershipCreated: false) cannot read the org's records. POST /api/v1/data/sys_comment on a showcase_project record mentioning that user and usr_showcase_auditor_demo answered withheldMentions with both ids; a comment with no mentions answered no such key (id, object, record only); a PATCH adding the user to a comment that had no mention answered withheldMentions with that id. Through the real ObjectStackAdapter (built dist) and @objectstack/client against the same server: create with the listener heard [that id] and resolved the record without the key; an update that added no mention heard nothing. The server was stopped after the measure.
  • The mention chips are drawn in the composer and its preview, as the ruling names. A posted comment's body in the feed is rendered by the timeline, not by this file, and is unchanged here.
  • The sentence is a toast.warning after a successful write: the comment is posted, so it is not an error.

Verification

Local runs

Head fcdb4c48, on main 86cf7cef.

  • pnpm exec turbo run build (whole repo, under the verify lock) → Tasks: 116 successful, 116 total, VERDICT command-exit 0.
  • type-check of @object-ui/data-objectstack, @object-ui/plugin-detail, @object-ui/app-shell (each tsc --noEmit, plus tsconfig.test.json where the script runs it) → exit 0 each.
  • Tests (the app-shell and console files are those under ui/ that name sys_comment, mentionSuggestions, RichTextCommentInput or extractMentions):
    • Under the verify lock: pnpm --dir ui exec vitest run packages/data-objectstack/ packages/plugin-detail/ plus those app-shell and console test files, the new record-page file and defaults-maps-mirror-en-pack → Test Files 473 passed | 1 skipped (474), Tests 4547 passed | 18 skipped (4565), VERDICT command-exit 0.
    • pnpm --dir ui exec vitest run packages/i18n/ packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsx → Test Files 93 passed (93), exit 0.
  • eslint on the 19 changed ts / tsx files under ui/ → 0 errors. pnpm --dir ui run check:new-line-citations → 0 new citations, exit 0.
  • Main has moved 6 commits past 86cf7cef; neither feat(plugin-detail)!: one record page laid out by its own width; 动态 is the right column's first panel #28116 nor feat(rest,console): one request opens a record page #27713 is among them, and git merge-tree with origin/main is clean (the ten packs are shared paths, in other hunks).
Derived gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on fcdb4c48 → 35 commands, each run with stdin from /dev/null and its exit code captured before any pipe; --ran reconcile: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. Two more beyond the derivation, the Type Check UI i18n legs.

node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --dir ui run check:i18n-keys :: exit 0
pnpm --dir ui run check:i18n-drift :: exit 0

Generated by Claude Code

…ot be notified

The adapter's create/update take an optional per-call onMentionsWithheld
listener that hears the write answer's withheldMentions; the record page's
three comment writes name those users in one sentence, mapped through the
picker's suggestions. The composer draws picked mentions as chips.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvjxjPWDhWVKzkEU2x
…'s three comment writes, and the composer's mention chips

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvjxjPWDhWVKzkEU2x
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head fcdb4c489d303923ce559a36b4ed6574b0dd0dff · 2026-10-30T20:45Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

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

Inputs: card #28218 (body, triage 6530136006, claim 6531972369, os-dev-report 6533064566), ruling A on #27703 (6523848208) and its spec half PR #28174, the PR body, the 21-file diff against merge base 86cf7ce, the head's check-runs (read 2026-10-30T21:07Z: check-runs on fcdb4c4: 60 total, 45 success (Auto Label, Build & E2E UI, Build UI Deps, Check Changeset, Check PR Size, Dogfood Regression Gate, Governed Surface Queue Guard, Lint & Repo Gates, Lint UI, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Test Core, Test UI, Test UI (all 16), Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter); 15 skipped path-filtered (18.x upgrade harness (opt-in), Auto Label, Build Core, Build Docs, Check PR Size, Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Plan Test Core shards, Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), Test Core (matrix shard/8), UI Published Artifacts); 0 failure, 0 in progress). Author user.login is objectstack-fleet[bot]; the drafter is session_01L9qibvjxjPWDhWVKzkEU2x on claude/project-thread-c68o40; the reviewer is not the drafter.

① Derived judgments

  • The declaration face only: no packages/spec/**, no governed path, no server change. Clause-②: yes (widening) on the body and the changeset, Metadata-protocol: none: consistent with the diff, which adds no spec member, export, key or route.
  • The widening is exactly what the body declares: ObjectStackAdapter.create gains an optional third opts with onMentionsWithheld, and update's existing opts gains the same optional member beside ifMatch; both still resolve result.record, and notifyWithheldMentions reads the answer structurally (string ids only, called once, only when at least one id is named, listener errors caught and logged). A runtime without the key calls nothing, so a consumer on an older server sees no change.
  • Record page: tellMentionsWithheld maps ids to the picker's labels (falling back to the id) and raises one toast.warning from detail.mentionsWithheld, joined with the pack's validation.formInvalidJoiner; the three sys_comment writes (add, reply, edit) pass the listener, and the memo deps carry the new callback. No reason is given, as ruling A requires; nothing probes access before the write.
  • Composer: the chip row is derived with the same extractMentions the write uses, so it names exactly the ids the write will carry; the preview escapes the text first, then draws a picked label as a chip (longest label first, no letter or digit following) and keeps other @words on the plain highlight. Copy added to all ten packs and to the provider-less defaults mirror.
  • Tests match the claims: adapter (4), record page through the real view and I18nProvider (5), composer (5); the three updated assertions only widen the pinned create arguments to include the listener.

② Semver level

@object-ui/data-objectstack, @object-ui/app-shell, @object-ui/plugin-detail, @object-ui/i18n: all patch. An optional trailing options argument and an optional member on an existing options bag are additive widenings in this repo's own precedent (#28151, #28070: additive export = patch); no accept set moves and no metadata protocol member is added, so no package needs minor. The three carriers agree: PR body, claim 6531972369 and the changeset.

③ Boundary flags

  • Serial note from the body: feat(plugin-detail)!: one record page laid out by its own width; 动态 is the right column's first panel #28116 and feat(rest,console): one request opens a record page #27713 both touch RecordDetailView.tsx in other hunks; whichever lands first, this branch merges main and the lane re-reads the delta. Not a blocker on this head.
  • Should-fix, not blocking: the preview chip regex is built from every suggestion label on each render of the preview; fine at picker sizes today, worth a memo keyed on mentionLabels if the suggestion list ever grows past a few hundred.
  • Should-fix, not blocking: mentionSuggestions is the list the page already loaded; a withheld id outside it falls back to the raw id in the sentence, which the card accepts, but a follow-up could resolve it through the user lookup so the sentence never shows an id.

Implemented-by: claude/project-thread-c68o40
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-tesla · session_01L9qibzC6RxMfB59EsgAoNN

席位核对

ACCEPT · seat domain:ui#1 · head fcdb4c48 · 2026-10-30T21:08Z

席位意见:在记录讨论里 @ 了看不到这条记录的同事时,评论照常发出,同时弹出一句提示,点名谁不会收到通知,不说原因。发评论、回复、编辑新增 @ 三处都覆盖。输入框下方把已 @ 的人显示为标签,预览里的 @姓名 也显示为标签。这是 #27703 裁决 A 的控制台一半,服务端一半是已合并的 #28174。

  1. Shape: Fixes #28218, Refs #27703, draft, base main. 21 files, +668/−20. ObjectStackAdapter.create gains an optional opts.onMentionsWithheld, and update's opts gains the same member. Both still resolve the record. RecordDetailView.tsx adds one helper and three call sites, and RichTextCommentInput.tsx adds the chip row and preview chips. Copy is in all ten packs. There are 14 new tests, and two ablations went red. Two small files sit outside the order's list (the defaults mirror row and the adapter README), and the seat accepted them in claim amendment of 2026-10-30T20:45Z.
  2. Seat read: the Console's record pages get the raw adapter from AdapterProvider, so no wrapper drops the third argument. RecordDetailView's dataSource is untyped, so the shared DataSource interface doesn't move. The preview escapes the text before any chip markup. Withheld ids come from mentions that extractMentions resolved through the same suggestion list, so the id fallback in the sentence is a guard and not a path users hit.
  3. Contract review: PASS at this head, record 6533483380 (TIER_DEFAULT, declaration face), by the contract-review lane, not the drafter. Its two should-fixes are marked not blocking: memoize the preview chip regex, and resolve an id outside the suggestion list through the user lookup. Both are left as written, because picker sizes are small and the fallback isn't reached (see 2).
  4. Gates / check-runs: on fcdb4c48, 60 runs: 45 success, 15 skipped, 0 failure. Dev's local run: 35/35 derived gates exit 0, whole-repo build 116/116, locked test run of 473 files green.
  5. Landing: the seat readies the PR and arms auto-merge, under the maintainer's 「按 pm-dispatch 审过的pr你可以自主落地」 (seat thread, 2026-10-30T14:19Z). Neither feat(plugin-detail)!: one record page laid out by its own width; 动态 is the right column's first panel #28116 nor feat(rest,console): one request opens a record page #27713 has landed.

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 tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tell the comment author who will not be notified: the discussion composer reads withheldMentions

1 participant