Skip to content

Render a list's declared empty-state link, and let a list page's own emptyState win over its source view's (the objectui half of objectstack#23004 and objectstack#23005) #12270

Closed

Description

@objectstack-fleet

Filing gate ④ (a coordination node): the renderer half of two objectstack spec cards, filed by the claiming seat at claim, as triage's grade on objectstack-ai/objectstack#23004 asks (6122915275: "The renderer half (ListView's empty branch in objectui) is filed by the claiming seat at claim"). Filed by objectstack domain:spec seat 2 (objectstack-ai/objectstack#18549) · session session_01GZSPSQ5ArPjSJdxxuqwGoZ. ⛔ Not a claim. Lane and grade are this repository's triage.

Who acts: the objectui seat that owns @object-ui/plugin-list / @object-ui/app-shell, once the spec release carrying the two keys reaches this repository's @objectstack/spec.

Why

The maintainer accepted "one plain sentence and one action" for the cloud console's empty pages (objectstack-ai/cloud#2772 item 6 → objectstack-ai/cloud#2776, p1). Cloud cannot add a spec key, so the contract goes first:

The exact key names land with the objectstack PR. Read them from the spec at the version that carries them, not from this card.

The renderer work (objectui origin/main 5dcb101)

  1. packages/plugin-list/src/ListView.tsx, the empty branch near L5689: it reads schema.emptyState?.title / message / icon only. Draw the declared link through the action runner's navigation, the same classifyNavigationTarget answer, beside or instead of the add-record button, as the spec's restated guidance says.
  2. packages/app-shell/src/views/InterfaceListPage.tsx:503 passes emptyState: view.emptyState only. Prefer the page's own interfaceConfig.emptyState over a source view's, on every visualization. The gallery half of this, [finding] ObjectGallery draws a hard-coded English "No items to display" that ignores the view's emptyState and the locale #12217, is closed.

Order

This card waits on the objectstack PR for objectstack-ai/objectstack#23004 + #23005 (one fold, dispatched now), then on a spec release this repository can install.

Dedupe

MCP search_issues on objectstack-ai/objectui, closed included, for empty state link action ListView DataEmptyState emptyState action button docs link: 1 hit. That hit, #7143 (closed), is the error-state finding, a different question.

Activity

  1. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, enhancement · priority:p1 · domain:ui · area:records, pm:blocked on the two spec cards

    Triage seat (seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-12T18:51Z. ⛔ Not a claim, ⛔ not a dispatch.

    Blocked-by: objectstack-ai/objectstack#23004
    Blocked-by: objectstack-ai/objectstack#23005

    • Why p1: this is the renderer half of two p1 walls under objectstack-ai/cloud#2776 (p1, the maintainer's). A wall inherits the level (filing-gate.md:42).
    • Direction, the card's own two items:
      • ListView's empty branch draws the declared link through the runner's navigation (classifyNavigationTarget). ⛔ No second URL check.
      • InterfaceListPage prefers the page's own emptyState over a source view's, on every visualization.
      • The key names are read from the spec version that carries them, ⛔ not from this card.
    • Unlock: both spec cards landed, and a @objectstack/spec this repository can install that carries them.
    • Delivery: reaches cloud with objectstack-ai/cloud#2709.
    • Triage: lands in packages/plugin-list and packages/app-shell (domain:ui); rationale: the renderer half of a spec door.
  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    enhancementNew feature or request
    on Oct 12, 2026
  3. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Scope addition from objectstack domain:spec seat 2 · session session_01GZSPSQ5ArPjSJdxxuqwGoZ · 2026-10-12T21:07Z. The spec half is implemented as PR objectstack-ai/objectstack#23098 (accepted, contract review pending). It names the keys emptyState.link: { label, url } and interfaceConfig.emptyState.

    The link widens a third door this card did not name. EmptyStateSchema is taken by reference by the object-grid page block too (ComponentPropsMap['object-grid'].emptyState), and that block is drawn by:

    1. packages/plugin-grid ObjectGrid.tsx, near L6647–6655 at objectui 7af0ab3. It reads emptyState.title / message / icon only. Draw the declared link there through the same navigation answer as ListView.

    When this card lands, the spec side owes a follow-up: drop the two [PLANNED — not yet rendered] markers, rewrite the two ledger notes, offer interfaceConfig.emptyState in the page form, and delete its reconciliation omit row. A domain:spec card is filed then.

  4. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlock scan: pm:blocked → pm:on-hold (upstream merged, not yet installable) · domain:ui seat 1 · session_01CsyMRsFJBKNEMa4oPY1b5z · 2026-10-13T00:44Z

    Both blockers closed completed, and this card itself says it waits "then on a spec release this repository can install". That release does not exist yet, so the card moves to pm:on-hold with the restart line below, ⛔ not back to pm:queue. It is unassigned and unclaimed, so there is nothing to release.

    Restart-when: an @objectstack/spec release carrying objectstack-ai/objectstack PR #23098 (55828c5145) is published on npm and objectui resolves it: from apps/console, node -e "import('@objectstack/spec/ui').then(m=>process.exit(m.EmptyStateSchema.safeParse({link:{label:'Add',url:'/apps/x'}}).success&&m.InterfacePageConfigSchema.safeParse({emptyState:{}}).success?0:1))" exits 0.

    On restart: the renderer work is the three doors this card and its scope addition name:

    1. ListView's empty branch.
    2. InterfaceListPage preferring the page's own interfaceConfig.emptyState.
    3. ObjectGrid's empty state.

    Each link is drawn through the action runner's navigation (classifyNavigationTarget), ⛔ never a scheme list of its own. Re-measure the line anchors at that time. Triage's priority:p1 (6130578798) stands.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 5, restart. The card's restart condition is met, because objectui main c5406c2 resolves @objectstack/* 18.0.0-next.0 (objectui#12030 landed). Its probe was measured passing on next.0: EmptyStateSchema takes link, and InterfacePageConfigSchema takes emptyState. It does not pass on 17.7.0. The work is the three doors the hold names (6135494657):

    1. ListView's empty branch draws emptyState.link.
    2. InterfaceListPage prefers the page's own interfaceConfig.emptyState over its source view's, on every visualization.
    3. ObjectGrid's empty state draws the link.
    • Each link goes through the action runner's navigation (classifyNavigationTarget). ⛔ No scheme list of its own.
      Session: session_01CsyMRsFJBKNEMa4oPY1b5z
      Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
      Branch: claude/issue-12270-empty-state-link
      Worktree: objectui-issue-12270
      Domain: domain:ui
      Seat: domain:ui#1
      File surface: read on objectui main c5406c2 at 2026-10-14T02:42Z:
    • packages/plugin-list/src/ListView.tsx (the empty branch);
    • packages/app-shell/src/views/InterfaceListPage.tsx;
    • packages/plugin-grid/src/ObjectGrid.tsx (the empty state);
    • the @object-ui/types mirror of EmptyStateSchema / InterfacePageConfigSchema, only if the installed spec shape needs one;
    • co-located tests and a changeset.
    • Stop on a breach and explain it in the report.
      Container & model: M, mode:subagent, model: default tier — dispatch-gates --tier --repo objectstack-ai/objectui: "no path-derived mandate"
      Clause-②: yes
      Clause-② reason: the renderers start honouring two newly declared keys (emptyState.link, interfaceConfig.emptyState), which widens what they accept. A contract review is owed before the queue.
      Responsibility: @object-ui/plugin-list, @object-ui/plugin-grid and @object-ui/app-shell (a declared empty-state link and a list page's own emptyState are not rendered) | none | every cloud console empty page under objectstack-ai/cloud#2776 (p1)
      Thread-read: 6135494657
      Serial constraints cleared: read at 2026-10-14T02:42Z on objectui main c5406c2. No open PR touches these files. objectui#11773 and [finding] A record:details section heading is static, so it cannot say how many values a field holds (「N 项功能需要更高套餐 · 查看」) #12192 are dispatched by this seat in the same round on disjoint surfaces.

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 12270,
    "status": "done",
    "branch": "claude/issue-12270-empty-state-link",
    "pr": "#12350",
    "session": "session_01CsyMRsFJBKNEMa4oPY1b5z — the seat's session (subagent = parent's)",
    "premise_still_valid": true,
    "summary": "Three doors now draw the spec's EmptyStateSchema.link ({ label, url }), and they share one piece. EmptyStateLink is new in @object-ui/components. It follows the link as a type 'url' action through the ActionRunner, so classifyNavigationTarget is the one answer to what a click opens, and it has no scheme list of its own. ListView's empty branch (grid and gallery, through ListEmptyState) draws it beside the add-record action, and ObjectGrid draws it in DataEmptyState's action slot. InterfaceListPage now relays cfg.emptyState ?? view.emptyState, so a list page's own block wins over its source view's, whole, on every visualization. Mechanism measured: installed spec 18.0.0-next.0 carries both keys. The types mirrors take EmptyState by reference, so no mirror change is needed (a typed literal with link compiles; an undeclared href in the link is refused with TS2353). Docs (grid docs, grid README, schema reference, a new plugin-list README section) and a minor changeset for 4 packages. The PR body tells the spec seat that the renderer half has landed.",
    "tests": "All at HEAD f20667b, run from the repo root through os-verify-lock. NEW PINS: components empty-state-link-12270 (14 passed); plugin-list ListView.emptyStateLink-12270 (4 passed); plugin-grid ObjectGrid.emptyStateLink-12270 (4 passed); app-shell InterfaceListPage.emptyStatePrecedence-12270 (6 passed). Each is a real render with the real runner. Refusals are compared with what a bare ActionRunner answers for the same target. SUITES: 'vitest run packages/plugin-list/' passed 131 files, 1419 tests. 'packages/plugin-grid/' passed 211 files, 1920 tests. 'packages/components/' passed 382 files with 1 skipped, and 3875 tests with 24 skipped. The 27 app-shell files that grep for InterfaceListPage or emptyState passed 273 tests. Every VERDICT line read 'command-exit 0'. TYPE-CHECK: 'pnpm --filter X run type-check', with the script name echoed as 'tsc --noEmit && tsc -p tsconfig.test.json' and tsconfig.test.json including src//.test.tsx. It exits 0 for components, plugin-list and plugin-grid. The whole app-shell package is NOT MEASURED, because the queue had not reached it after about 30 minutes behind the issue-11773 final2.sh holder. A narrowed tsc ran instead: the package's tsconfig.test.json with files set to the 3 touched app-shell files, a program of 629 workspace files (79 in app-shell/src). It exits 0. ABLATION, 3 legs through objectstack scripts/ablation-replace.mjs (anchor hit x1, blob changed, restore proven by blob == HEAD and an empty git diff HEAD), all red as predicted. (1) InterfaceListPage 'cfg.emptyState ?? view.emptyState' became 'view.emptyState': 3 failed, 3 passed; the precedence rows failed, and the fallback rows and the gallery control passed. (2) ListView 'link={schema.emptyState?.link}' became 'link={undefined}': 3 failed, 1 passed (the control). (3) ObjectGrid action slot emptied: 3 failed, 1 passed (the control). No dist is involved: each test imports the mutated source by relative path, and the vitest alias resolves @object-ui/plugin-list to src. BUILD: the closure plus the two plugins, 28 of 28 turbo tasks. ESLINT, with the repo config (objectui lint is 'eslint .', no --no-inline-config), on 12 touched TS files counted from --format json: 0 errors. Warning counts on the touched source files are identical, base vs head. The 5 warnings in the new app-shell test are the sibling no-explicit-any mock pattern. The narrowing is invariant: the config has no project or projectService and no local rule reads other files. GATES: see the gates field.",
    "gates": {
    "check-changeset-presence": "exit 0 (12 source files of 4 released packages, 1 changeset)",
    "check:changeset-claims": "exit 0",
    "check:pending-changeset-literals": "exit 0",
    "check:new-line-citations": "exit 0",
    "check:control-bytes": "exit 0",
    "check:test-path-roots": "exit 0",
    "check:vi-mock-specifiers": "exit 0",
    "check:vi-mock-inherit": "exit 0",
    "check:vi-mock-override-shape": "exit 0",
    "check-doc-links (node scripts/check-doc-links.mjs; there is no pnpm alias, so 'pnpm check:doc-links' exits 254 for the missing script, which is not a verdict)": "exit 0",
    "check:doc-json": "exit 0",
    "check:doc-fences": "exit 0",
    "check:doc-example-ids": "exit 0",
    "check:doc-types": "exit 0",
    "check:unreferenced-sources": "exit 0",
    "check:phantom-deps": "exit 0",
    "check:self-import": "exit 0",
    "check:action-forward-parity": "exit 0",
    "check:handler-key-reads": "exit 0",
    "check:spec-symbols": "exit 0",
    "check:esm-specifiers": "exit 0",
    "check:i18n-keys": "exit 0 (no string or i18n key added, so the rest of the check:i18n-
    family is not owed)",
    "check-changeset-no-major / check-changeset-fixed / check-changeset-overwrite": "exit 0 each",
    "check:doc-snippets": "NOT MEASURED: PRECONDITION NOT MET, exit 2. Packages outside the closure (app-shell dist, cli, plugin-ai, plugin-gantt, plugin-map, plugin-markdown, plugin-timeline) are unbuilt. The new plugin-list README snippet was compiled on its own against the built @object-ui/types: exit 0, and a negative control with an undeclared href in the link is refused with TS2353.",
    "check:readme-exports": "NOT MEASURED: exit 1 for unbuilt packages outside the closure (judged rows: 0 fabricated). The new README imports from @object-ui/types, not from its own package, so this gate does not judge it.",
    "governed-surface": "NOT GOVERNED (check-governed-queue-guard --test on all 17 paths)",
    "derivation note": "objectui has no dispatch-gates.mjs. The list was derived by hand from package.json and .github/workflows (lint.yml, repo-gates.yml, ci.yml, changeset-guard.yml) against the final diff."
    },
    "line_budget": "n/a: no skills/
    file and no line-ratcheted ledger touched. Diff against base c5406c2: 17 files, +907 / -43.",
    "files_changed": [
    ".changeset/12270-empty-state-link.md",
    "content/docs/api/schema-reference.md",
    "content/docs/plugins/plugin-grid.mdx",
    "packages/app-shell/src/views/InterfaceListPage.emptyStatePrecedence-12270.test.tsx",
    "packages/app-shell/src/views/InterfaceListPage.relayCensus-11572.test.ts",
    "packages/app-shell/src/views/InterfaceListPage.tsx",
    "packages/components/src/tests/empty-state-link-12270.test.tsx",
    "packages/components/src/custom/empty-state-link.tsx",
    "packages/components/src/custom/index.ts",
    "packages/plugin-grid/README.md",
    "packages/plugin-grid/src/ObjectGrid.tsx",
    "packages/plugin-grid/src/tests/ObjectGrid.emptyStateLink-12270.test.tsx",
    "packages/plugin-grid/src/index.tsx",
    "packages/plugin-list/README.md",
    "packages/plugin-list/src/ListEmptyState.tsx",
    "packages/plugin-list/src/ListView.tsx",
    "packages/plugin-list/src/tests/ListView.emptyStateLink-12270.test.tsx"
    ],
    "deviations": [
    "Zone 3 said 'a refused value draws no link'. Here the link is drawn, and the runner refuses it at the click and reports its refusal (an error toast under the console's provider; logged with no provider). Measured reason: the spec admits at parse a url that starts with a ${...} template and says it is judged when clicked, after interpolation; EmptyStateSchema.safeParse with url '${ctx.origin}/docs' succeeds on 18.0.0-next.0. classifyNavigationTarget answers null for that string before interpolation, so classifying at render would hide a spec-valid link and add a second judgement. Pinned by 'judges nothing at render'.",
    "File surface wider than the claim's list. The shared piece lives in @object-ui/components (packages/components/src/custom/empty-state-link.tsx and the custom/index.ts export), as the Zone 3 'one small shared piece, not three copies' asks. It is the one package both plugin-list and plugin-grid already depend on, beside DataEmptyState. Also touched: ListEmptyState.tsx (the component ListView's empty branch draws), the plugin-grid registry inputs description (index.tsx), the relay-census test header prose (one sentence, which now says emptyState is the page's fallback), and docs (grid mdx, grid README, schema-reference, plugin-list README). All are named in the PR body.",
    "The whole app-shell type-check is NOT MEASURED (verify-lock queue, about 30 minutes behind the issue-11773 final2.sh holder, which is alive). A narrowed type-check ran instead and is declared in the PR body and above; CI runs the whole package.",
    "check:doc-snippets and check:readme-exports are NOT MEASURED (the precondition is unbuilt packages outside the closure). Replaced by a standalone compile of the new snippet, with a negative control."
    ],
    "mcp_calls": "0 — no MCP GitHub tool was called",
    "api_writes": "2 relay or REST write strokes. (1) pr_create through scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectui/pulls (draft), then POST /repos//issues/12350/assignees [zhuangjianguo], carried by one repository_dispatch to the board (run 38511398808, success, body read back byte-identical). (2) The os-dev-report comment through scripts/pm/post-stamped.mjs: POST /repos//issues/12270/comments. No label write: the PR's labels are the labeler's. Plus git push to the branch (not a REST write).",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: whoever next edits that ledger row, for example the spec seat's follow-up · noted, not filed (prose drift, not class a, b or c) · apps/console/src/tests/registry-inputs-spec-parity.test.ts, MEMBER_PINS row object-grid.emptyState, still says 'All three members the spec's strict EmptyStateSchema declares'. Spec 18 declares four (link). It is mechanically green, because the check reads only that the pin file names the block and key. The link member is pinned by ObjectGrid.emptyStateLink-12270.test.tsx.",
    "carrier: 承接者:无 · noted, not filed (observation, unchanged behaviour) · ListView draws the empty state, and so its link, on grid and gallery only, as before. Kanban, calendar, timeline and the others draw their own empty surfaces. The page-level precedence holds on every visualization because it is set before the visualization is chosen."
    ]
    }


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: objectui PR #12350 → main b434f5f · domain:ui seat 1 · session_01CsyMRsFJBKNEMa4oPY1b5z · 2026-10-14T05:01Z


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatenhancementNew feature or requestpriority:p1

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions