Skip to content

feat(app-shell,plugin-detail,components,i18n): the record page's timeline and related columns are each reader's per-object switches, toggled from the crumb row - #27120

Merged
objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/issue-27052-workspace-preferences
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/issue-27052-workspace-preferences

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #27052
Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:记录页的「时间线」「关联」两列变成读者自己的开关,按对象记住;默认值从对象推导(被工作的对象开时间线,否则关),不是元数据。crumb 行右侧多了一组图标开关(☴ 时间线 ⌘⇧T、⊞ 关联 ⌘⇧R),关掉时间线时它的内容折进关联列底部的「动态」卡。
  • 为什么改:维护者裁决「时间线是否显示,是否可以切换……顶部有很多切换图标」——产品、价格表这类对象的中间列是一条空的流和一个输入框。
  • 风险与回滚:只动前端(app-shell / plugin-detail / components / i18n),无 spec、无元数据键;回滚即 revert 本 PR。偏好存在浏览器 localStorage,回滚后旧键残留无害。
  • 先看:ui/packages/plugin-detail/src/synth/recordPageColumns.ts(默认值推导与排布规则)、ui/packages/app-shell/src/views/RecordDetailView.tsx(crumb 行与快捷键)、ui/packages/app-shell/src/utils/workspacePreference.ts(偏好存储)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/RecordDetailView.panelToggles-27052.test.tsx,预期 6 passed;或看下方证据分支的三张截图。
  • 需要你做的:无,席位已裁两条 open questions(见下方 Resolved)并落地。

What changes

  • One workspace-preference store (app-shell/src/utils/workspacePreference.ts). A preference is declared once and read per scope: user, or object:OBJECT_NAME. Each preference and scope pair has one localStorage key. The value is a small JSON record carrying v, and a record of another version reads as "no choice". Reads are live through useSyncExternalStore, other tabs arrive through the storage event, and with no storage (SSR, a blocked origin, a refused write) a read falls back to the default while a refused choice is held for the page.
  • recordOpenPreference is a thin alias over it. Its key os:record-open-mode and its bare drawer / split spelling are unchanged, so a reader's earlier choice reads back and the record-open preference tests pass unchanged.
  • Two new preferences. record.columns.timeline and record.columns.related take on / off per object. With no choice, recordPageColumnDefaults derives the default: the timeline is on for a worked object and off otherwise, and the related column is on. recordTimelineDefaultReason names the branch: stageField names a field, a field's options declare a lifecycle kind (unless stageField: false), or enable.files: true. Being the master_detail parent of another object is not a signal (seat ruling 1, PR comment 6464903026): a container that owns child rows is not a worked record. The derivation reads only the object itself, so recordTimelineDefaultReason / recordPageColumnDefaults take no object list.
  • The column tiers read the switches. recordPageArrangementFor(tier, switches) is the rule, and RecordPageColumns takes switches. With the timeline off, a wide page draws details | related and the timeline's blocks become an Activity card at the foot of the related column. With related off, the related column stays behind the existing rail toggle. Below the column widths the page is tabbed as before, the switches are kept, and ?tab= is untouched. Authored kind: full pages and slots.tabs never reach this code.
  • PanelToggles (@object-ui/components). It is a named group of icon toggles with aria-pressed, a tinted pressed state and a tooltip showing the shortcut's keycaps. The related switch is named detail.relatedColumn (「关联列」), apart from the rail toggle and the Related tab, so the row never carries two controls named 「关联」. The record page draws it on its crumb row, right side, only in a column tier. It is not drawn in the drawer or split pane.
  • Shortcuts. ⌘⇧. / Ctrl+Shift+. (timeline) and ⌘⇧, / Ctrl+Shift+, (related) flip the switches while the full record page is in a column tier — matched by physical key (KeyboardEvent.code Period / Comma), since Shift changes the character those keys type. The card's ⌘⇧T / ⌘⇧R are browser-reserved (reopen closed tab, hard reload) and were replaced by seat ruling 2; a ⌘⇧T / ⌘⇧R keydown now leaves the columns and the stored preference untouched. They do nothing while a text field has focus, and they are advertised to the keyboard-shortcuts dialog under the same gate.
  • i18n. detail.timeline, detail.timelineFolded, detail.relatedColumn, detail.panelToggles, console.shortcuts.toggleTimeline and console.shortcuts.toggleRelated are added in all ten packs; no pack names a chord.

Merge order with the record-page navigation PR

The navigation PR for the record page, #27038, adds ↑ ↓ and a back link to the same crumb row. This PR does not depend on it. Whichever lands second places its controls so the row reads ↑ ↓ · divider · panel toggles. The first to land does not wait for the other.

Tests

  • workspacePreference.test.ts covers keys per preference and scope, the versioned record, unreadable spellings, no storage and SSR. workspacePreference.hook.test.tsx covers the live hook: same-scope callers re-render, scopes stay apart, cross-tab storage events arrive, and a refusing storage holds the choice for the page.
  • recordPageColumns.switches-27052.test.ts has one row per derivation branch and per arrangement case. RecordPageColumns.switches-27052.test.tsx checks where each block lands when a column is off.
  • panel-toggles.test.tsx covers the group name, aria-pressed, the tint class, click behaviour and the tooltip keycaps.
  • RecordDetailView.panelToggles-27052.test.tsx mounts the real view. It covers the card's acceptance 1: timeline off on a worked object draws details | related with the folded card, and on restores it. The choice survives a remount and is per object. An object that is not worked opens with the timeline off. ⌘⇧T with an input focused does nothing. Nothing is drawn in the tabbed tier or when embedded.
  • recordOpenPreference.test.ts and the two ObjectView record-open suites pass unchanged (acceptance 3).

Acceptance notes

  • Screenshots (acceptance 2). They were taken on HotCRM f660fd8, fresh DB, with this branch's console at 1720 px wide. HotCRM's crm_lead carries an authored kind: 'full' record page, which the card leaves untouched, so the column tiers never draw there. The worked-object shots therefore use crm_contact, which has enable.files: true and no authored page. Evidence branch (never merge): claude/issue-27052-evidence, folder evidence-27052/.
    • 1-contact-both-columns.png: three columns and both toggles pressed.
    • 1b-contact-toggle-tooltip.png: the tooltip with its keycaps.
    • 2-contact-timeline-off.png: details | related.
    • 3-product-default.png: crm_product opens as details | related, with the timeline folded at the foot of the related column.
    • A browser readback on that run confirmed three more things. The contact's choice survived a reload. Ctrl+Shift+T sent by Playwright turned the timeline back on (that run predates ruling 2; the chord is now Ctrl+Shift+., pinned in panelToggles-27052, not re-measured in a real browser: Playwright injects keys below the browser's accelerator layer). The root measured 1464 px.
  • The drawer width is not yet read through the store. The card asked for the ov:drawer-width:OBJECT key to go through the store too. That read and write live in navigation-overlay.tsx, which this dispatch fences off because the navigation PR edits it. It is left for after that PR lands.
  • ⌘K. The command palette on main has no command registry. CommandPaletteProvider only opens and closes it, and CommandPalette hard-codes its groups. So this PR adds the shortcuts and no palette commands.
  • Two private helpers repeated. The chord matcher and the text-field check repeat AppHeader's, which are private to that file. Sharing them would edit a file outside this card's surface.
  • New preferences only carry v. The record-open preference keeps its bare stored value without v, because its tests pin the stored bytes.

HotCRM reading of the default derivation

Read with recordPageColumnDefaults over the 20 object definitions of HotCRM f660fd8, loaded from source, on head fd0f941f70 (after ruling 1):

object timeline branch
crm_lead, crm_opportunity, crm_task, crm_case ON option-kinds
crm_account, crm_contact, crm_contract, crm_quote ON files
crm_campaign, crm_price_book, crm_event, crm_knowledge_article, crm_product, crm_forecast, crm_opportunity_line_item, crm_quote_line_item, crm_price_book_entry, crm_campaign_member, crm_event_attendee, crm_article_feedback OFF none

8 ON / 12 OFF; the related column defaults to ON for all 20. This matches the card's expected list exactly. Before ruling 1 the four container objects (campaign, price book, event, knowledge article) were ON through a master_detail-parent branch that is now gone.

Open questions — resolved by the seat (PR comment 6464903026), both landed in patch round 1

  1. The master_detail-parent branch: B, dropped. A container that owns child rows is not a worked record (ruling 2 on the card); every object the card expects ON stays ON through stage kinds or files; the rule reads two declared signals of the object itself. Pinned by a view-level case (an object that is only another object's master_detail parent opens with the timeline off) and the HotCRM table above.
  2. Chords: B, moved off the browser's. ⌘⇧. / ⌘⇧, (Ctrl+Shift+. / Ctrl+Shift+,), matched by physical key; tooltip keycaps, aria-keyshortcuts and the shortcuts dialog read the same constants. Delivery through a real browser's accelerator layer is NOT MEASURED on this host (Playwright and devtools inject below it); the page-side behaviour is pinned.

Verification

Final head fd0f941f70. Cloud run: report comment 6464041380 on the card (34 / 34 derived gates exit 0 on b36136e2, ablation of the typing-target guard). Patch round 1 (local, user-token pushes): report comment 6466430387 (34 / 34 gates on a6d6ac6fb4 with the list re-derived identical on fd0f941f70; 634 files / 6348 tests across plugin-detail, i18n, components custom, app-shell utils / chrome / RecordDetailView* / ObjectView*; plugin-detail + app-shell type-check with their test tsconfigs; two reverse verifications with blob hashes and a clean git diff HEAD). CI on fd0f941f70: Test UI 17 / 17 shards, Type Check UI, Lint UI, Build & E2E UI all success.


Generated by Claude Code

…with recordOpenPreference as its alias

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4EjjR2sbXvZiQ3HsgSBhb
…line and related columns are the reader's switches, per object, drawn as panel toggles on the crumb row

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4EjjR2sbXvZiQ3HsgSBhb
…nd tint by aria-pressed; changeset

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4EjjR2sbXvZiQ3HsgSBhb
… apart from the rail toggle and the Related tab

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4EjjR2sbXvZiQ3HsgSBhb
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · PR #27120 ← #27052 · epic PM session_cdb7a717-def7-4e71-9988-f089fbaa18ff · 2026-10-28T09:02Z

Verdict: REWORK (patch round on this PR, same claim) — not governed (0 of 26 paths), +1733 / −167. The first cloud-dispatched card of this epic; the branch, the draft PR and its body are what the dispatch asked for; the session also read its own CI and fixed the one red without a poke.

What the seat read and accepts as built (head 3288364286):

The one red on head 3288364286 — RecordDetailView.recordPageTiers-26678 finding two buttons named /Related/ once the 「关联」 panel toggle joined the rail toggle on the row (job 122675293423) — the cloud session fixed on its own at b36136e279: the switch is named detail.relatedColumn (「关联列」) in all ten packs, and the #26678 pin now finds the rail toggle by data-testid, tighter than the name query, not looser. Accepted on reading; not yet measured by CI: that push came through the Claude GitHub App (committer claude) and GitHub created no workflow run for it — the commit carries five queued third-party check suites and no github-actions suite at all, while pr-body-guards listens to synchronize. On this repository the App's pushes do not fire the PR workflows; only the PR-open event (the fleet bot's pr_create) did. The patch round's push, made with a user token, is what puts the final head under CI. What remains is the two open questions, both answered below and both needing code.

Rulings on the two open questions (seat, within the card's own text; no product semantics beyond it):

  1. master_detail-parent branch: B — drop it. The card's own reading lists campaign, price book, event and knowledge article as OFF; the branch alone turns them ON. A parent that merely owns child rows is a container, not a worked record (ruling 2 on the card), and the rule shrinks to two declared signals (stage kinds, files). Update the derivation, its per-branch test and the HotCRM table in the body.
  2. Chords: B — not browser-reserved. ⌘⇧T / Ctrl+Shift+T reopens the closed tab and ⌘⇧R / Ctrl+Shift+R hard-reloads in Chromium and Firefox; the page never receives them. Use ⌘⇧. (timeline) and ⌘⇧, (related) — Ctrl+Shift+. / Ctrl+Shift+, elsewhere — unless a real-browser measurement on the branch finds them taken too, in which case choose another unreserved pair and say which was measured. Update the tooltip keycaps, the shortcuts dialog entries and the i18n strings that name the chord.

Patch round (one stroke, same branch):

  1. Rulings 1 and 2 above: the derivation drops its master_detail-parent branch (per-branch test row and the HotCRM table in the body follow), and the chords move to the unreserved pair (tooltip keycaps, shortcuts dialog entries, i18n strings that name the chord, the RecordDetailView.panelToggles-27052 chord case).
  2. Rerun the derived gates on the final head and the suites the body names; reverse-verify the derivation change (restore the branch, see the campaign / price book / event / article rows red, restore).
  3. Report as a comment on console: one workspace-preference module (record open mode, drawer width, timeline / related column toggles) read per user × object, with a shared panel-toggle toolbar on the record page — defaults derived from the object, never metadata #27052 (first line os-dev-report), round patch round 1.

Report (card comment 6464041380) read and accepted: premise holds; 34 / 34 gates exit 0 on b36136e2 with the --ran reconciliation quoted; ablation of the typing-target guard went red on exactly the chord case and restored to the HEAD blob; the drawer width's ov: key is NOT routed through the store (the read and write live in navigation-overlay.tsx in @object-ui/components, which cannot import an app-shell store) — accepted as an Acceptance note and carried to the layout card #27055, not this patch round; the record-open preference keeps its bare bytes (its tests pin them) — accepted. Seat edits owed on the body at close-out: detail.relatedColumn in the i18n bullet and the PanelToggles bullet; the HotCRM table after ruling 1; the chords after ruling 2. Out-of-scope finding carried to the epic parent: HotCRM's lead / account / opportunity / case declare authored kind: full pages, so the column tiers and these toggles never reach its most-worked objects until those pages migrate.

Deviations recorded: the cloud session ran on Fable for its first minutes until the maintainer switched it to opus (the desktop launch cannot set the model); the cloud session cannot receive this review, so the patch round runs as a local os-dev agent on the same branch under the same claim.


Generated by Claude Code

hotlong and others added 4 commits October 28, 2026 17:05
…rkspace-preferences

# Conflicts:
#	ui/packages/app-shell/src/views/RecordDetailView.tsx
…nly what the object declares, and the panel chords move off the browser's ⌘⇧T / ⌘⇧R

Two seat rulings on the patch round:

- recordTimelineDefaultReason drops its master_detail-parent branch. A
  parent that merely owns child rows (a price book, a campaign, an event)
  is a container, not a worked record; the rule is now stage field /
  option kinds and enable.files only, read off the object alone, so
  recordPageColumnDefaults no longer takes the app's object list.
- The panel chords are ⌘⇧. (timeline) and ⌘⇧, (related), Ctrl+Shift
  elsewhere, matched by KeyboardEvent.code because Shift changes the
  character those keys type. Chromium and Firefox keep Ctrl/⌘+Shift+T
  and Ctrl/⌘+Shift+R for themselves and the page never receives them.

Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff
Co-authored-by: Claude <noreply@anthropic.com>
…n declaration

The project in the tier suite was worked only as the master_detail parent
of its tasks, which no longer turns the timeline on. Its status options
now declare lifecycle kinds, so the wide tier stays three columns for the
reason the derivation actually reads.

Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff
Co-authored-by: Claude <noreply@anthropic.com>
…ts name

An object definition is passed with its name; dropping the property made
literal definitions fail the excess-property check.

Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff
Co-authored-by: Claude <noreply@anthropic.com>
… fixtures pass none

The derivation reads only what the object declares of itself (stage field,
option kinds, enable.files), so the shape it reads has no name and the
switch fixtures stop passing one.

Claude-Session: https://claude.ai/code/session_cdb7a717-def7-4e71-9988-f089fbaa18ff
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · PR #27120 ← #27052 · epic PM session_cdb7a717-def7-4e71-9988-f089fbaa18ff · 2026-10-28T10:24Z

Verdict: ACCEPT — ordinary queue landing (governed register: 0 of 27 paths; packages/spec untouched; +1786 / −167, under the 3000-line threshold). Supersedes the REWORK above: the cloud session's build plus one local patch round, both under the same claim.

Checklist, judged on GitHub and on the branch:

Landing: ready + auto-merge in this stroke on the maintainer's standing authorization for this epic (verbatim, 2026-10-28: 「授权你用车队身份给 #26758 翻 ready 并挂 auto-merge(squash),后续本 epic 的 PR 复核 ACCEPT 后同样处理」); the queue decides the rest.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 10:25
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 10:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 9dacccf Oct 28, 2026
59 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-27052-workspace-preferences branch October 28, 2026 10:41
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/xl tests tooling

Projects

None yet

3 participants