Repository navigation
studio(nav editor): an untargeted "Add nav item" placeholder is sent on save and fails with a raw Zod path; the error banner stays after the item is removed #11776
Description
Filing gate ① — product defect with a named location and a reproduction. reach: Studio → Interfaces → navigation Edit → Add nav item, leave it unbound: the autosave answers with the raw issue "• navigation.1.objectName — Invalid input: expected string, received undefined".
Who acts on it: objectui triage → the Studio / app-shell owner. ⛔ Not a claim. Found in a manual browser QA pass of Studio on 2026-10-07; filed one card per finding on the maintainer's word: 「你发现的问题全部提交 issue」, and on the one-card-per-finding question 「覆盖规则,逐条立卡」.
What happens
- Add nav item inserts a placeholder; while it has no target every autosave fails with the Zod path above.
- Removing the placeholder (hover trash icon), letting autosave succeed and clicking Done does not clear the banner.
- The resulting draft is identical to the published app, yet Changes lists it as
repair_desk · Update.
Where it comes from (read in source)
AppNavCanvas (app-shell/src/views/metadata-admin/previews/AppNavCanvas.tsx) creates the placeholder as { id: navId, type: 'object' } with no objectName. doNavSave in StudioDesignSurface.tsx filters out only entries without a type — its comment still describes the older placeholder shape — so the untargeted entry is sent.
Expected
Unbound placeholders are kept client-side (or dropped) on save; the error state clears after a successful save; a no-op draft is not offered as a change.
Suggested direction (triage to rule)
Drop entries that have no target for their type; reset the error on save success; make the remove control reachable by keyboard (today it only appears on hover).
Environment
objectstack 879bd38c · examples/app-showcase booted with objectstack dev --ui --seed-admin on an isolated port and SQLite file · objectui 179f6fe9 (HEAD; the framework pin .objectui-sha is a58626c8) served by the console's Vite dev server, perf numbers from a vite build of the same commit · Chromium 141 at 1440×900 · signed in as the seeded platform admin admin@objectos.ai unless stated.
Duplicate check
- Semantic issue search on objectstack-ai/objectui (open and closed), add nav item placeholder navigation objectName invalid input banner stale: 7 hits (0 open, 7 closed).
- Related, not the same defect:
- finding(app-shell,plugin-designer): the designer's nav surfaces show a label-less nav entry as blank, "unnamed" or its id — the entries objectui#9868 makes producible inherit their target's label only at runtime #11196 (closed) — same nav-editor surface, untargeted/label-less entries; different defect (display, not save refusal)
- Positive control in the same run: related lists action id refusal notice shown to end users on objectstack-ai/objectui returned related lists: an action id the related object does not define draws a refusal notice above the list for whoever opens the record — an authoring fault shown to end users; scope it to authors/admins #11768: yes.
Dedupe words: add nav item placeholder · navigation objectName invalid input · stale error banner nav editor · doNavSave cleanup type
Filed by Claude Code (session session_01D76mrPJrSSdaKRxR2rvrMG) from that QA pass.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 7, 2026
Path: write metadata — Studio's navigation editor saves what the author sees | 缺项 | P2
Triage: first grade,
bug·priority:p2(re-graded from the filed p1) ·domain:ui·area:studio·pm:queue. Direction as the body proposesTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-07T16:01Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in the Studio navigation editor (its autosave payload and error banner) ⇒
domain:ui; rationale: a Studio or console surface in objectui. Filed from the Studio browser QA pass of 2026-10-07 (objectstack879bd38c, objectui179f6fe9).typefrom the saved payloadClause-②: no. Patch changeset in objectui.objectstack-fleet commentedon Oct 7, 2026
Claim: PM loop round 6
Session:
session_01DrKzdPdyLLBW3qpZ4vtk7zAccount:
huangyiireneBranch:
claude/issue-11776-nav-placeholder-saveWorktree:
objectui-issue-11776Domain:
domain:uiSeat:
domain:ui#1File surface (line numbers on
8273f4b):packages/app-shell/src/views/metadata-admin/previews/AppNavCanvas.tsx: the placeholder created by Add nav item (:293) and the row's remove control (about:505–:650).packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx, only the Interfaces pillar's nav save (doNavSave:2583–:2630) and the nav editor's error banner state.packages/app-shell/src/views/metadata-admin/i18n.ts: oneengine.*row, only if the remove control has no accessible name today..changeset/11776-*.md.⛔ Not on it:
navSurface.tsanduseSurfaceDeepLink.ts(objectui#11774, just landed);AppPreview.tsx;packages/i18n/**.Any file outside this list: the dev reports it before opening the PR (stop on breach; explain in the report).
Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectuiover these paths: no path-derived mandate; default tier)Clause-②: no
Responsibility:
objectui app-shell Studio: AppNavCanvas's Add nav item inserts an untargeted placeholder {id, type: 'object'}, and doNavSave drops only entries with no type, so the placeholder is sent and every autosave fails with the raw issue navigation.N.objectName; a later successful save does not clear the error banner | the platform path is right: the spec refuses an object nav item with no objectName, as it should | every author who adds a nav item in Studio before binding it; measured by the filer (the banner stays after the placeholder is removed and the save succeeds)Thread-read: 6041728961
Serial constraints cleared:
noneblocking.area:studio: nothing is in flight. objectui#11784 landed as58b277c, and this seat's objectui#11774 landed as8273f4b, which ends the serial line this seat recorded between studio(interfaces): object nav items that carry filters or viewName collapse into one surface — the preview drops the filter and every item for that object highlights #11774 and this card.6020143345). Nothing else of this seat is in flight.area:workflow) first: its triage (6045669919) puts it serial behind seat 3's objectui#11778, which edits the sameFlowNodeInspector.tsxregion (draft PR objectui#11832). It is not claimable until that lands.Why
Clause-②: no: the change is in what Studio's nav editor sends and shows. No accepted input widens: the spec's refusal of an untargeted entry stands, and Studio stops sending one. No export, prop, or@object-ui/typesmember changes, and nopackages/i18nkey is added. App-shell'sENGINE_STRINGSare unexported. If the fix needs a published surface, or a change to what the server accepts, the dev stops and reports, and the seat re-claims.objectstack-fleet commentedon Oct 7, 2026
os-dev-report
{
"issue": 11776,
"status": "done",
"branch": "claude/issue-11776-nav-placeholder-save",
"pr": "#11835",
"session": "session_01DrKzdPdyLLBW3qpZ4vtk7z — subagent run; the id is the dispatching seat's (harness-stamped Claude-Session line)",
"premise_still_valid": true,
"summary": "Studio's nav save now sends the editor's navigation less every entry that names no target for its type, at every depth. The rule is navPayloadOf in AppNavCanvas.tsx; it reads the same NAV_TARGET_READS table as the card's navTarget, and a group is never left out. While the buffer holds such an entry it stays dirty, so the unbound entry stays on the canvas in its place through the re-read a landed save triggers (mutation M1 proves the pin), and binding it sends it there. A browser reload drops it (it is never stored), and the leave guard warns first. The nav save's failure now lives in its own navError state in the same banner. It clears when a nav save lands, on a conflict reload, and when a read-only answer puts the buffer back, and it never clears a leaf's failure (control pin). The row's remove control is always mounted: zero width and opacity while hidden, shown on card hover, card keyboard focus and its own keyboard focus. Enter or Space removes the row. Chromium readings show the at-rest layout is byte-for-byte BASE. Hypothesis corrections: H3 — the control was a role=button span with an accessible name, mounted only while a JS hover state was true; it was not hidden with opacity, so no i18n row was needed. H4 — the id backfill used the filtered index, so leaving an entry out moved a generated id; it now uses the editor position. H2 — the producer is InterfacesPillar's pillar-local error state, not useDraftAutoSave, which is untouched. Four existing suites (objectui#11189, #11204, #11232, #11167) used a bare Add, which the server refuses, as their edit; it is now Add plus bind.",
"tests": {
"head": "2e408c7",
"gates": [
"bash os-verify-lock.sh -c "pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build" → exit 0 → 'VERDICT command-exit 0' (ran before the edits; the closure is upstream of every touched file)",
"bash os-verify-lock.sh -c 'pnpm exec vitest run --maxWorkers=3 [60 files: every StudioDesignSurface*, AppNavCanvas*, AppPreview* test, studio-locale.i18n, ResourceEditPage.navDeepLink-11153, navSurface.test.ts] && pnpm --filter @object-ui/app-shell type-check' on 2e408c7 → exit 0 → 'Test Files 60 passed (60)', 'Tests 420 passed (420)', '@object-ui/app-shell@17.7.0 type-check' script banner echoed, 'VERDICT command-exit 0'",
"tsc -p tsconfig.test.json --listFilesOnly → exit 0 → all 6 touched test files listed (control: previews/AppNavCanvas.tsx listed)",
"pnpm check:control-bytes → 0 → 'check-control-bytes: OK (scanned 7805 tracked text file(s); skipped 85 binary).'",
"pnpm check:new-line-citations → 0 → 'VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0'",
"pnpm check:changeset-claims → 0 → 'No pending changeset names a file this change touches.'",
"pnpm check:pending-changeset-literals → 0 → 'No test source names a pending changeset.'",
"pnpm check:vi-mock-specifiers → 0 → 'check-vi-mock-specifiers: OK'",
"pnpm check:vi-mock-inherit → 0 → 'check-vi-mock-inherit: OK'",
"pnpm check:vi-mock-override-shape → 0 → 'check-vi-mock-override-shape: OK'",
"pnpm check:test-path-roots → 0 → 'check-test-path-roots: OK'",
"pnpm check:metadata-write-doors → 0 → 'OK 17 metadata write door(s) derived'",
"node scripts/check-changeset-presence.mjs → 0 → '8 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/11776-nav-placeholder-save.md.'",
"node scripts/check-changeset-no-major.mjs → 0 → 'No changeset declares a
majorbump.'","npx eslint --format json on the 8 touched .ts/.tsx files, inline config honoured as CI's per-package
eslint .does → 0 → 0 errors, 16 warnings, identical by rule to BASE copies of the two sources (16). Narrowing proof: (1) the population is CI'spnpm lint= turboeslint .per package under the root eslint.config.js; (2) the count of 8 files comes from the JSON; (3) eslint.config.js has no type-aware linting (grep projectService|parserOptions|project:|typeChecked: 0 hits), so this diff cannot change a verdict in an untouched file","grep -naP control-byte self-scan over the diff's 9 files → exit 1 (no match)",
"added gate families beyond the order's list, derived from the diff: check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape (new vi.mock doubles), check:test-path-roots, check:metadata-write-doors (save doubles), check-changeset-presence, check-changeset-no-major, targeted eslint",
"CI not awaited (contract): at report time PR 11835 head 2e408c7 had 42 check runs, 24 completed, 18 in_progress, 0 failed — in_progress"
],
"h1_payload": {
"before_BASE_8273f4b": "Chromium harness. Add nav item, unbound, autosave → PUT 400, navigation [nav_home, nav_landing, {id: nav_item_3, type: object}]. Banner '• navigation.2.objectName — Invalid input: expected string, received undefined'. Remove the placeholder → PUT 200 [nav_home, nav_landing]; the banner STAYS",
"after_2e408c7": "Same steps → PUT 200 [nav_home, nav_landing]; no banner; nav_item_3 still on the canvas, third; leave guard holds. Add two, bind the 2nd then the 1st → PUT 200 [nav_home, nav_landing, nav_item_4=acme_task], then [.., nav_item_3=acme_task, nav_item_4=acme_task]; leave guard released. A 503 save shows the banner "Service unavailable"; removing that row by Tab + Enter → PUT 200 and the banner clears; Done closes editing"
},
"browser_reading": "Dev-only harness (an apps/console HTML entry mounting InterfacesPillar over the real console metadata client; a Playwright route handler played the server and parsed every app PUT with the spec's AppSchema), Chromium /opt/pw-browsers/chromium at 1440x900; removed afterwards, git status clean. Remove control: at rest opacity 0, width 0; card hovered 1, 24px; card keyboard-focused (:focus-visible) 1, 24px; the next Tab lands on button "Remove nav item", 1, 24px, :focus-visible true. Card label widths at rest: 102/85/128px, identical to BASE. The first version (always 24px) gave 70/53(of 79)/96 — measured, corrected in 194fa98",
"reverse_check": "Fix committed; BASE 8273f4b versions of AppNavCanvas.tsx and StudioDesignSurface.tsx checked out (navPayloadOf 3→0, setNavError 5→0 on disk); new pins → 38 failed / 5 passed (the passers are the two spec-only pins, the fixture pin and the two controls). Predicted direction: the first pillar pin received accepted:false with {id: nav_item_3, type: object}. Restored by git checkout HEAD in a trap; blobs == HEAD (9ebfb0f4, 9ba2d62c); git diff HEAD empty",
"ablations": [
"M1 via objectstack scripts/ablation-replace.mjs: 'if (sent.unmoved() && !leftOut)' → 'if (sent.unmoved())', anchor 1→0, blob 9ba2d62c→1b8ceb31 → pin 'stays on the canvas' fails (rail [Home menu, Landing menu] after the re-read) → restored, blob == HEAD, git diff HEAD empty",
"M2 first attempt: a NO-OP — ablation-replace refused it because the replacement was a substring of the anchor; not counted",
"M2 rerun: the landed-save setNavError(null) removed, anchor 1→0, blob 9ba2d62c→6d9b9553 → banner pin fails ('Service unavailable' still shown) → restored, blob == HEAD, git diff HEAD empty"
],
"deviations": [
"navPayloadOf is a module export of AppNavCanvas.tsx (the PM route allowed the helper there). It is not in index.ts, and package exports are only "."; an inline react-refresh/only-export-components disable follows the renderStudioGridList precedent, so warnings equal BASE",
"Fixture triage of four existing suites (objectui#11189, #11204, #11232, #11167): their bare-Add edit became Add plus bind. Two "sends nothing" pins in the 11232 suite would have passed vacuously; they again test an edit that would be sent",
"The InterfacesPillar read-only effect also clears navError (the nav editor's banner state); otherwise a failure about a put-back buffer would have outlived it",
"The BASE dependency-closure build ran before the edits, not after the last commit (no touched file is in that closure)",
"No file outside the claim's surface; no i18n row (the control already had an accessible name); no prop, @object-ui/types member or packages/i18n key"
],
"files_changed": [
".changeset/11776-nav-placeholder-save.md",
"packages/app-shell/src/views/metadata-admin/previews/AppNavCanvas.tsx",
"packages/app-shell/src/views/metadata-admin/previews/AppNavCanvas.navPayload-11776.test.tsx",
"packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx",
"packages/app-shell/src/views/studio-design/StudioDesignSurface.navPlaceholderSave-11776.test.tsx",
"packages/app-shell/src/views/studio-design/StudioDesignSurface.navAutosaveFlush-11189.test.tsx",
"packages/app-shell/src/views/studio-design/StudioDesignSurface.autosaveInFlight-11204.test.tsx",
"packages/app-shell/src/views/studio-design/StudioDesignSurface.autosaveSwitch-11232.test.tsx",
"packages/app-shell/src/views/studio-design/StudioDesignSurface.navEditClosesOnReadOnly-11167.test.tsx"
],
"cleanup": "vite server (my PID tree) killed; harness files removed; worktree node_modules removed and
git worktree removesucceeded without --force"},
"mcp_calls": "0 — none",
"api_writes": "2 — (1) pr_create through scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches (relay), executed as POST /repos/objectstack-ai/objectui/pulls (draft) → objectui#11835; read-back 11484 bytes sent = stored; (2) this os-dev-report comment through scripts/pm/post-stamped.mjs: POST /repos//issues/11776/comments. Plus git push of the branch (not REST). Zero label or assignee writes",
"open_questions": [
{
"question": ""Done" while an unbound entry is on the nav canvas: by objectui#11189's rule Done never closes over an unsent edit, so editing stays open — but nothing on screen says why now that no save fails. What should Done do?",
"options": [
"A — leave as is: editing stays open; the entry was selected at birth, so its inspector already shows "Choose an object; the menu item will open its record list."",
"B — Done selects the first unbound entry, so its inspector shows that existing hint again; no new string and objectui#11189's rule kept",
"C — Done drops the unbound entries from the buffer and closes; contradicts objectui#11189's rule, and a label typed on an unbound entry is lost"
],
"recommendation": "B, as a separate card if wanted. It reuses the inspector's existing hint, adds no string and no new rule, and keeps "Done never closes over an unsent edit". Not done here because the claim's surface was the nav save and banner, and the Done effect sits outside it"
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — the leaf draft save (InterfacesPillar doSave) never clears its own failure on a later landed save either; same class as this card's banner half; read in source only (its success path sets no error; the pillar's only setError(null) is the leaf-load start); not measured at a public door; in the PR's Acceptance notes",
"carrier: 承接者:无 · noted, not filed — the metadata-admin App editor (AppPreview, off-limits here) renders the same AppNavCanvas "Add nav item"; whether its save sends the unbound entry was not measured",
"carrier: 承接者:无 · noted, not filed — the remove control is a role=button span nested inside the card's button element (interactive content in a button); Chromium focuses and activates it (measured); other engines not measured",
"carrier: 承接者:无 · noted, not filed — after Enter removes the focused row, focus falls to the document body"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 7, 2026
Review: ACCEPT — PR objectui#11835 (head
2e408c7) · the dev's open question ruled A: "Done" stays as it is in this PRdomain:uiseat 1 ·session_01DrKzdPdyLLBW3qpZ4vtk7z· reviewed against the PR diff, its file list and check runs, read 2026-10-07T22:19Z. Report:os-dev-report6047965402on this card.Ruling on the open question: A
"Done" while an unbound entry is on the nav canvas keeps editing open, as objectui#11189's rule says (Done never closes over an unsent edit). This PR adds nothing for it.
lastAttemptRef === snapKey), soflushNavSave()returns false and the close waits.Review
Shape: draft →
main. The first line isFixes #11776,Clause-②: nostarts a later line, and the body names the Changes-panel expectation as left to objectui#11807.Scope: 9 files, +776/−66, inside claim
6047119796's surface:AppNavCanvas.tsx;StudioDesignSurface.tsx: the Interfaces pillar'sdoNavSave, the read-only put-back effect, and the banner;@object-ui/app-shellpatch).navSurface.ts,useSurfaceDeepLink.ts,AppPreview.tsx, the Changes panel, the flow files,i18n.tsandpackages/i18n/**are untouched. The shareduseDraftAutoSaveis untouched too: H2 measured the stale banner as the pillar's own state.Clause-② re-read on the diff:
exportisnavPayloadOf, fromAppNavCanvas.tsx.packages/app-shell/src/index.tsnames neither the module nor the function.engine.appNav.removeItemaccessible name.noholds.Mechanism checked on the diff:
NAV_TARGET_READS(pertype, from the spec's branches:object→objectName…action→actionDef.actionName,doc→docthenbook) now backs both the card's displayed target (navTarget, behaviour unchanged) and what a save leaves out (namesNoTarget). The two cannot disagree.group,separatorand unknown types are never left out. Children are filtered recursively, and a kept entry or unchangedchildrenlist is returned by reference.sent.unmoved() && !leftOut). The served-draft re-read cannot install over the unbound entry, and the leave guard holds.lastAttemptRef === snapKeyguard sends nothing new until the author edits.Banner:
navErroris new state that only the nav save sets. It clears on a landed nav save, on a conflict reload, and when the read-only effect puts the buffer back. A leaf'serroris left in place (control pin), and both show when both exist.Keyboard: the remove control is always mounted and in the tab order. At rest it takes no width or opacity, so the card's layout is unchanged; the dev measured label widths identical to base after correcting a first version in
194fa98. It shows on card hover, on card keyboard focus, and on its own focus. Enter or Space removes the entry. Its pre-existing nesting (role=buttonspan inside the card's<button>) is noted out of scope.Fixture triage: four existing suites (objectui#11189, finding(app-shell): Studio's shared draft autosave drops an edit made while a save is in flight on its other callers (Automations measured; Data, page inspector) — the family closure, fix in useDraftAutoSave #11204, finding(app-shell): switching Studio to another flow or page inside the autosave debounce writes the previous item's whole document into the newly opened one (Automations, page inspector measured); family closure with objectui#11203 #11232, finding(app-shell): Studio's Interfaces rail keeps nav editing open after the package answers read-only, so edits are taken on screen and never saved #11167) used a bare Add as their edit, which the server refuses. Each now does Add plus bind. The two "sends nothing" pins in the finding(app-shell): switching Studio to another flow or page inside the autosave debounce writes the previous item's whole document into the newly opened one (Automations, page inspector measured); family closure with objectui#11203 #11232 suite would otherwise have passed vacuously; they again test an edit that would be sent. I accept this: a pin whose edit the save no longer sends was testing the defect.
Tests:
navPayloadOfper type and the remove control by keyboard.accepted: falsewith{id: nav_item_3, type: object}. The 5 green were the spec-only pins, the fixture pin and both controls. The restore was proven by blob hash.!leftOutguard removed: the "stays on the canvas" pin went red.setNavError(null)removed: the banner pin went red. The first M2 attempt was a declared no-op, then rerun.StudioDesignSurface*,AppNavCanvas*andAppPreview*test, among others).app-shelltype-check green.{id: nav_item_3, type: object}, and the banner stayed after the placeholder was removed;Overlap re-read: since the PR's base
8273f4b,main(32124ac) changed none of this PR's paths.CI at
2e408c7: 42 runs, still converging at this read: 26 green, 3 expected skips, 13 in progress, none red. The PR waits to be queued until every check completes.Out-of-scope notes, not filed (read in source only, not measured at a public door):
doSave) does not clear its own failure on a later landed save either;AppPreview) mounts the same Add nav item, and whether its save sends the unbound entry was not measured;bodyafter a keyboard remove.All three are in the PR's Acceptance notes. The
AppPreviewpath is a candidate for the next browser QA pass.PR assignee: set by this seat to
huangyiirene, the card's assignee.Generated by Claude Code
objectstack-fleet commentedon Oct 7, 2026
Landed: PR objectui#11835 →
main03fc85ddomain:uiseat 1 ·session_01DrKzdPdyLLBW3qpZ4vtk7z· verified 2026-10-07T22:50Z.2e408c7before queueing: 40 of 43 check runs passed, and the 3 skipped are the expected ones.check-governed-merges.mjsread 0 of 9 paths governed and 842 changed lines, under the human-merge threshold.03fc85dis one squash commit on parent32124ac. It changes 9 files, +776/−66, the same as the PR.2e408c7and03fc85d.a9e5818d…. The landed bytes are the reviewed head's bytes.main:navPayloadOfhitsAppNavCanvas.tsx.completedby the PR'sFixes.pm:dispatchedwas removed and read back.6048018520): the open question was ruled A. "Done" over an unbound entry keeps editing open, as objectui#11189 rules, and the entry's inspector already says to choose an object. No follow-up card was filed, because no pull is measured.Generated by Claude Code
fix(app-shell): Studio's nav save leaves out entries with no target, …