Repository navigation
Description
Current behavior
When a server-side action handler returns { redirectUrl }, objectui's consoleServerAction.ts (the one place the /api/v1/actions/... response is interpreted for navigation) has exactly two paths:
- with
opensInNewTab: true— the pre-opened spinner tab is driven to the URL (popup-safe, works well); - without it —
window.open(url, '_blank')after the awaited POST, where the user-gesture context is already gone, so the popup blocker fires almost every time and the user is left with the “Popup blocked” toast fallback.
A same-tab navigation exists only as the error fallback when the pre-opened tab handle has gone stale (window.location.href = url). There is no way for an author or a handler to ask for it: ActionSchema is strict and has no post-success navigation key (redirect / onSuccess / navigate are all rejected), and openIn applies to type:'url' only.
References: spec opensInNewTab/newTabUrl (#1787, in packages/spec/src/ui/action.zod.ts); redirectUrl convention fixed by objectui#2967 and consolidated in objectui#2904 (packages/app-shell/src/utils/consoleServerAction.ts). All shipped in spec/console@17.0.0.
Why this matters
“Server-side action completes → take the user to the resulting record” is the standard ask for copy/clone/generate-style actions. Real case: steedos-labs/os-project-titanwind-ehr#1172 — a “copy as new version” record-header button clones a v+1 draft, but the user is left on the old record and has to re-filter the list to find the new one. The new-tab form is a workable interim, but the natural UX is an in-place jump.
Ask
- Same-tab form for the redirectUrl convention. Let the handler (or the action metadata) express it — e.g. handler returns
{ redirectUrl, openIn: 'self' }, or anActionSchemakey — and have the console perform an SPA route navigation (no full page load, immune to popup blocking). - (Companion) client-side routing convention. Today the handler must hand-build the console route (
/apps/<appId>/<object>/…), leaking the console's URL scheme into every app's server code. A convention like “handler returns{ recordId, objectName }and the client routes to that record's detail page” keeps route knowledge where it belongs.
Either half alone is not enough for the case above: (1) without (2) still hard-codes routes server-side; (2) without (1) still lands in a new tab.
Acceptance
A type:'script' action whose handler returns the designated shape navigates the CURRENT tab to the target record's detail page via the SPA router — no full reload, no popup-blocker interaction.
Activity
os-support-ai commentedon Aug 18, 2026
Triage: lands in the decision box —
needs-user-decision+domain:spec, type Feature. Feature-class + a public contract shape (ActionSchemakey / redirectUrl convention) ⇒ manual floor. (Triage seat, sessionsession_0185aJyu6TRnHbcVgSfGPSho.)⭐ Rule together with #9474. These two cards are one design family: #9474 asks for post-success navigation for
api-type actions (proposedonSuccess.navigate+${result.*}interpolation), this card asks for a same-tab form of thescript-type redirectUrl convention. Ruling them separately risks two navigation contracts for one need. The recommendation below treats them as one shape; the maintainer can still split them deliberately.Routing note: cross-repo feature — contract-first split applies. The spec key shape is the upstream half (this card,
domain:spec); the console navigation behavior (consoleServerAction.tsSPA route hop) is a downstream objectui card once the shape is ruled, carryingBlocked-by:this one.Four-facet block:
redirectUrlhandler convention for script, a schema key for api). A singleonSuccessshape with anopenIn: 'self' | 'newTab'member subsumes both cards and retires the popup-blocker workaround class instead of patching it per type.ActionSchemais strict and already rejects improvised keys (redirect/onSuccess/navigateall bounce) — that's the right posture; the fix is to declare ONE closed shape, not to loosen. A closed enumopenInbeats handler-side ad-hoc conventions an AI would have to guess.window.open). ⛔ No general navigation DSL, no per-surface variants.Recommendation: adopt one
onSuccessnavigation shape at the spec level answering both #9474 and this card, with same-tab as an explicit member; interim (pre-ruling) the new-tab form remains the documented workaround.Generated by Claude Code
os-steve commentedon Aug 18, 2026
Maintainer ruling recorded — maintainer, 2026-08-18, in-session to the
domain:specPM (sessionsession_01Fs18A2DdXLVN2h8PaaFBcP), verbatim: 「9566 插队」 then 「9566 同意」 approving the presented shape. Ruled together with #9474 per the triage recommendation (one navigation contract, not two):ActionSchemagains one CLOSEDonSuccesskey, applying to bothapiandscriptaction types:navigate(a route/URL template) +openIn: 'self' | 'newTab'(closed enum — no general navigation DSL, no per-surface variants).navigatetemplate gains${result.*}— the server response joins${param.*}/${ctx.*}(feat(actions): api 型动作需要「成功后跳转到服务端返回的记录」能力(ActionSchema 跳转键 + 响应进入插值作用域) #9474's companion ask).onSuccess.navigatedefaultsopenIn: 'self'(new surface, natural SPA-route UX, immune to popup blocking); the shipped handler-return convention{ redirectUrl }WITHOUTopenInkeeps its current new-tab behavior (17.0.0 compatibility — no silent behavior flip for existing handlers); a handler may returnopenIn: 'self'explicitly to opt into the same-tab jump.consoleServerAction.ts,executeAPInavigation handling, interpolation implementation) are downstream objectui card(s) filed at this card's landing,Blocked-by:these cards.One spec PR delivers the contract for BOTH cards (
Fixes #9566,Fixes #9474); the runtime capability reaches users when the objectui half lands (tracked there; customer ref steedos-labs/os-project-titanwind-ehr#1172).Claim: this seat, branch
claude/issue-9566-onsuccess-navigation, fable (clause-② — new authorable key + closed enum + interpolation-scope contract); measured quota exemption → opus if refused, recorded here. Priority: p0 per the maintainer's 插队.Generated by Claude Code
feat(spec): ActionSchema.onSuccess — post-success navigation with ${r…
os-steve commentedon Aug 18, 2026
{ "issue": 9566, "sibling": 9474, "outcome": "delivered", "pr": 9601, "branch": "claude/issue-9566-onsuccess-navigation", "head": "b53b4fb70", "key_shape": "ActionSchema.onSuccess: strictObject { navigate: string (route/URL template; documented scope ${param.*} + ${ctx.*} + ${result.*} NEW = server response), openIn: enum('self','newTab').default('self') } — optional, refine-scoped to type 'api'|'script' (the #4352 enforcement shape; ActionSchema is one strict object discriminated by a `type` key, not per-type schemas); InlineActionSchema deliberately does not pick it", "default_mechanism": "materialized .default('self') — the file's convention (type/refreshAfter/ai.exposed); parse output always carries the resolved member so the future console branch needs no fallback of its own; pinned via parse-output observable. Handler-return { redirectUrl } compat (absent openIn => new-tab, 17.0.0) recorded in the key's JSDoc, no handler code touched", "files": [ "packages/spec/src/ui/action.zod.ts", "packages/spec/src/ui/action-on-success.test.ts", "packages/spec/liveness/action.json", "packages/spec/scripts/strictness-ledger.test.ts", ".changeset/action-onsuccess-navigation.md", "content/docs/references/ui/action.mdx", "docs/audits/2026-07-unknown-key-strictness-ledger.counts.md", "packages/spec/authorable-surface/ui.json", "packages/spec/liveness/state-counts.md" ], "gates": [ "spec test 410 files / 10957 tests PASS at b53b4fb70 (union re-run at final head)", "spec typecheck + test-typecheck ledger PASS", "check:generated all 13 artifact gates PASS (--fix regenerated exactly the 3 proved stale: gen:docs, gen:strictness-ledger, gen:liveness-counts)", "check:liveness PASS after drilling the onSuccess container per child (navigate/openIn, planned strength, #9340/#9463 convention)", "dispatch-gates matched families all PASS: adr-0087-registration, changeset-no-major, empty-changeset, cross-package-test-inputs, dev-prereqs, affected-docs, changeset-gate-self-tests, doc-authoring, docs-audit-scope, docs-redirects, merge-driver, objectui-changeset, quick-reference-counts, role-word, spec-parsed-alias, type-source-resolution, doc-formula-expressions", "convention-triggered PASS: query-options-erasure, engine-double-contract, where-matcher, type-check-coverage, type-check-debt --re-measure (33 entries, none above), nul-bytes", "consumer sweep DOWNSTREAM: turbo build ./packages/* closure 70/70 PASS against rebuilt spec dist; @objectstack/example-showcase validate + typecheck PASS" ], "reverse_verification": "Predicted then observed, from committed state (git restore --source=origin/main action.zod.ts, pins kept): 17 red — all accept pins, both default pins, every message-bearing refusal pin, all 4 type-scope refusals, both top-level guidance pins, and the strictness site-count pin (9->8). 4 predicted VACUOUS survivors stayed green (pure success===false pins also satisfied by the unknown-key rejection) — reported as vacuous, not counted as coverage. Restored via git checkout HEAD; 36/36 green, clean tree.", "deviations": [ "Enum spelling observation, flagged not deviated: ruled 'newTab' coexists with the shipped top-level openIn's 'new-tab' (type:'url', 17.0.0). Implemented exactly as ruled; an issue.input-keyed enum error (ActionLocationSchema precedent) makes the crossover spelling loud in both directions. Harmonizing the shipped value would be a breaking migration outside this ruling.", "Type-scope refinement (refuse onSuccess on url/modal/flow/form) added as ENFORCEMENT of the ruling's 'applies to api and script' — ADR-0078 posture, #4352 shape — since ActionSchema cannot express it structurally; pinned per type.", "strictness-ledger.test.ts site-count pin moved 8->9 for the new strictObject site, per that test's own 7->8 precedent.", "Liveness entry drilled per child rather than one container verdict — the gate's undeclared-container check (#4956) required it; both children planned with the same cross-repo measurement and amend-on-landing instruction." ] }Generated by Claude Code
Generated by Claude Code
os-steve commentedon Aug 18, 2026
ACCEPT — review of record against PR #9601 (head
b53b4fb70), full diff read (9 files), covering BOTH this card and #9474.onSuccessstrict object (navigate+openIn: 'self'|'newTab'),${result.*}documented into the navigate scope as CONTRACT (engine stays objectui's), defaults split by surface precisely as ruled — materialized.default('self')on the schema key (justified against the file's own convention and pinned via parse output), handler-return compat recorded in the JSDoc without touching runtime code.redirect/navigate/redirectUrltop-level) now land on full rewrite prescriptions instead of bare rejections; the'new-tab'/'newTab'crossover has a dedicatedissue.input-keyed error both ways; the api/script type scope is ENFORCED by a action.body 的 type 门是 declared ≠ enforced —— spec 说只在 type:'script' 生效,runtime 有 body 就绑 #4352-shape refinement with the ADR-0078 reasoning in the message (a key other types would never read is refused loudly, with the per-type alternative named);InlineActionSchemadeliberately excluded with the widening rule cited.DashboardWidgetSchema的 ~22 个 widget 级键从未被台账分类,而 dashboard.json 的 _note 声称它们已分类 #4956 undeclared-container check demanded it) with amend-on-landing instructions naming the exact objectui read sites to cite. Strictness pin 8→9 per its own precedent. Changeset minor with the adr-0087 not-required reasoning inline. Fable ran at tier — no quota event, no label loop.Landing: ready + auto-merge (squash) once CI is green. On MERGE, pre-registered: this seat files the objectui downstream card —
consoleServerAction.tsSPA-navigation branch honoring schemaonSuccess+ handleropenIn: 'self',executeAPInavigation handling,interpolateTargetgains theresultscope member —Blocked-by: objectstack#9566 + objectstack#9474, dispatchable when objectui's spec pin covers this landing; liveness amends tolivewith that half.Generated by Claude Code
feat(spec): ActionSchema.onSuccess — post-success navigation with ${r…
test(spec): the first ui/ file group's test titles state each cited d…