You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
script actions can only redirect to a NEW TAB — the redirectUrl convention has no same-tab (SPA route) form #9566
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 an ActionSchema key — 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.
Triage: lands in the decision box — needs-user-decision + domain:spec, type Feature. Feature-class + a public contract shape (ActionSchema key / redirectUrl convention) ⇒ manual floor. (Triage seat, session session_0185aJyu6TRnHbcVgSfGPSho.)
⭐ Rule together with #9474. These two cards are one design family: #9474 asks for post-success navigation for api-type actions (proposed onSuccess.navigate + ${result.*} interpolation), this card asks for a same-tab form of the script-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.ts SPA route hop) is a downstream objectui card once the shape is ruled, carrying Blocked-by: this one.
Four-facet block:
Platform long-term coherence — one post-success navigation contract covering both action types beats per-type conventions (redirectUrl handler convention for script, a schema key for api). A single onSuccess shape with an openIn: 'self' | 'newTab' member subsumes both cards and retires the popup-blocker workaround class instead of patching it per type.
Measured business pull — two independent external pulls: steedos-labs/os-project-titanwind-ehr#1172 (this card: clone-then-jump leaves the user on the old record) and the external project feat(actions): api 型动作需要「成功后跳转到服务端返回的记录」能力(ActionSchema 跳转键 + 响应进入插值作用域) #9474's triage recorded as waiting. This is the standard ask for copy/clone/generate actions, not speculative surface.
AI-agent error-resistance — ActionSchema is strict and already rejects improvised keys (redirect/onSuccess/navigate all bounce) — that's the right posture; the fix is to declare ONE closed shape, not to loosen. A closed enum openIn beats handler-side ad-hoc conventions an AI would have to guess.
Startup scope discipline — smallest complete shape: one schema key + the interpolation scope + one console branch (SPA navigate instead of window.open). ⛔ No general navigation DSL, no per-surface variants.
Recommendation: adopt one onSuccess navigation 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.
Maintainer ruling recorded — maintainer, 2026-08-18, in-session to the domain:spec PM (session session_01Fs18A2DdXLVN2h8PaaFBcP), verbatim: 「9566 插队」 then 「9566 同意」 approving the presented shape. Ruled together with #9474 per the triage recommendation (one navigation contract, not two):
ActionSchema gains one CLOSED onSuccess key, applying to both api and script action types: navigate (a route/URL template) + openIn: 'self' | 'newTab' (closed enum — no general navigation DSL, no per-surface variants).
Defaults, deliberately split by surface: schema-declared onSuccess.navigate defaults openIn: 'self' (new surface, natural SPA-route UX, immune to popup blocking); the shipped handler-return convention { redirectUrl } WITHOUT openIn keeps its current new-tab behavior (17.0.0 compatibility — no silent behavior flip for existing handlers); a handler may return openIn: 'self' explicitly to opt into the same-tab jump.
Contract-first split: this spec half lands first and does not wait on objectui; the console halves (SPA route navigation in consoleServerAction.ts, executeAPI navigation 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 插队.
{
"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."
]
}
ACCEPT — review of record against PR #9601 (head b53b4fb70), full diff read (9 files), covering BOTH this card and #9474.
The ruled contract landed exactly: closed onSuccess strict 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.
Anti-drift work beyond the ask, all accepted: feat(actions): api 型动作需要「成功后跳转到服务端返回的记录」能力(ActionSchema 跳转键 + 响应进入插值作用域) #9474's measured probe spellings (redirect/navigate/redirectUrl top-level) now land on full rewrite prescriptions instead of bare rejections; the 'new-tab'/'newTab' crossover has a dedicated issue.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); InlineActionSchema deliberately excluded with the widening rule cited.
Verification: 410 files / 10957 tests, all gate families green, downstream 70/70 build + showcase sweep; reverse verification predicted-then-observed 17 red with the 4 vacuous survivors reported AS vacuous — honest coverage accounting. Liveness drilled per child at planned strength (the gate's liveness: 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.ts SPA-navigation branch honoring schema onSuccess + handler openIn: 'self', executeAPI navigation handling, interpolateTarget gains the result scope member — Blocked-by: objectstack#9566 + objectstack#9474, dispatchable when objectui's spec pin covers this landing; liveness amends to live with that half.
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…