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
console: a type: "api" action with opensInNewTab + newTabUrl opens no tab and POSTs to its target instead #26747
Filing gate: ① product defect, reproduced, landing site named.
Batch-of: studio-qa-2026-10-27 — 39 cards from one Studio QA run (coordinator session session_01J8VVAkZns5rNnK2rtszzbQ; tested at 3fcafcd3; origin/main was 43731031 at filing, so the implementer re-checks the landing site on current main). The run record is the qa-run issue filed with this batch.
Symptom
The declared new-tab behaviour is ignored from both the record header and the more-menu.
Reproduction
Action export_tab on gapdata_item: type: "api", opensInNewTab: true, newTabUrl: "https://example.com/export/{recordId}" → click from the header and from the more-menu → no tab opens; network shows a POST to the target.
Expected
The runner opens newTabUrl (interpolated) in a new tab as the spec documents, or the spec refuses the pair on api actions.
Suspected landing site (the implementer verifies)
Console action runner for api actions under ui/.
Filing facts
reach: Console UI, measured twice by the data gap-fill card.
Business reason: A declared behaviour the runtime does not honour ("declared ⇒ enforced").
Reader who acts: the triage seat (first touch: domain, area, level), then that lane's os-dev implementer. QA suggestion only: domain:ui, area:records, p2.
p2: a type: "api" action with opensInNewTab + newTabUrl opens no tab and POSTs instead. Honour the declared new-tab behaviour on every surface that runs actions. From the studio-qa-2026-10-27 batch. Re-check the landing site on current main.
Claim: PM loop round 1
Session: session_01QsR2jTfvjNTaR29ehLk26T
Account: zhuangjianguo
Branch: claude/project-thread-bol9ec (platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)
Worktree: objectstack-issue-26747
Domain: domain:ui
Seat: domain:ui#4
File surface: whichever of ui/packages/app-shell/src/utils/consoleServerAction.ts, ui/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx or ui/packages/core/src/actions/ActionRunner.ts holds the break (named in the report), tests beside, one root changeset; ⛔ a second interpolator; ⛔ packages/spec/**, server packages, any new key (stop on breach; explain in the report)
Container & model: S, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate; reason: the maintainer's standing rule 「只认领不需要 fable 的卡」, 2026-10-27T10:39Z), effort: high
Clause-②: no
Metadata-protocol: none
Responsibility: the console action dispatch (ui, this lane) POSTs an api action that declares opensInNewTab + newTabUrl instead of opening the tab, which breaks the spec's documented zero-roundtrip path · the platform path that may cover it: the newTabUrl fast path in consoleServerAction.ts (~181-210), which this dispatch does not reach · reached by every declared new-tab api action on the record header and more-menu
Thread-read: 6448334618
Premise: holds (consoleServerAction.ts holds the fast path at ~188-208, yet QA G8 measured a POST from the header and more-menu on origin/main 6090bb73 at 2026-10-27T20:24Z)
Serial constraints cleared: no open PR touches the surface (the thread re-checks at start)
Dispatched at 2026-10-27T20:29Z to the card thread on that branch; the order lives in the thread's opening message.
The card thread (claude/project-thread-bol9ec) measured the break. The console registers two api handlers, and neither reads opensInNewTab / newTabUrl; both always fetch the target. The list pages run the one in useConsoleActionRuntime.tsx, already in the surface. The record header and the ⋯ more-menu run RecordDetailView.tsx's own apiHandler, passed in its ActionProvider handlers map, which was outside the surface. The fix exports the fast path's own condition from consoleServerAction.ts. Both handlers then route an api action that meets it to the existing console server-action handler, which already resolves the record id, pre-opens the tab and substitutes {recordId}. No second interpolator.
File surface: ui/packages/app-shell/src/utils/consoleServerAction.ts (the exported predicate), ui/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx, ui/packages/app-shell/src/views/RecordDetailView.tsx (its apiHandler only), tests beside (including RecordDetailView.apiNewTabUrl-26747.test.tsx), one root changeset; ⛔ ui/packages/core/**, packages/spec/**, server packages, any new key (stop on breach)
Clause-②: no
Metadata-protocol: none
An absolute newTabUrl (QA's value) is a separate open question. The spec calls the key a path template, and the fast path prefixes baseUrl. That behaviour is not changed here.
{
"issue": 26747,
"status": "done",
"branch": "claude/project-thread-bol9ec",
"pr": "https://github.com/objectstack-ai/objectstack/pull/26922",
"session": "session_01QsR2jSH8FbKjEztmGT1sbB",
"premise_still_valid": true,
"summary": "The failing link is the console's registered type:'api' handlers, not the runner or the surfaces: ActionRunner.dispatchCollected hands an api action to the registered handler, and both api handlers (useConsoleActionRuntime.apiHandler for list surfaces and ConsoleShell's root provider, RecordDetailView.apiHandler for the record header and its more-menu) always fetched target and never read opensInNewTab/newTabUrl; the fast path existed only in the script-registered handler built by createConsoleServerActionHandler. New utils/newTabUrlFastPath.ts holds the rule: takesNewTabUrlFastPath (now also the fast path's own condition in consoleServerAction.ts) and withNewTabUrlFastPath(apiHandler, consoleHandler), which routes such an action to the surface's console server-action handler (its record-id resolution, pre-opened tab and {recordId} substitution; no second interpolator) and every other api action to apiHandler unchanged, forwarding the fast path's row-count refusal only for those actions. Both hosts register the composed handler; the hook also returns it as apiHandler.",
"tests": "head 1bcc587b. Build closure turbo build app-shell^... + formula/client/sdui-parser/lint: 63 tasks, VERDICT command-exit 0. pnpm --filter @object-ui/app-shell run type-check (covers tests): exit 0 at 1bcc587b. New files RecordDetailView.apiNewTabUrl-26747 (real header click, real more-menu click, control) + useConsoleActionRuntime.apiNewTabUrl-26747 (real ActionRunner list toolbar, returned apiHandler, two-row refusal, control): Test Files 2 passed (2), Tests 7 passed (7). Ablation via ablation-replace.mjs on newTabUrlFastPath.ts ('? consoleHandler(action, context)' -> '? apiHandler(action, context)', anchor 1->0, blob 3ef0dcca->087c133d): Tests 4 failed | 3 passed (7), exactly the four fast-path cases; restored blob == HEAD, git diff HEAD empty (app-shell tests import src, no dist leg). Narrowed app-shell run, 100 files (test files named for useConsoleActionRuntime / consoleServerAction / RecordDetailView / ConsoleShell + every app-shell test naming apiHandler or newTabUrl): Test Files 100 passed (100), Tests 736 passed (736), VERDICT command-exit 0; first run at 0c1ffeb2 was 19 failed files (stubs of utils/consoleServerAction lacked the new export), fixed by moving the rule to its own module. Rest of app-shell left to CI Test UI (declared narrowing). ESLint on the 5 touched ui files: 0 errors, no warning on changed source lines.",
"gates": "dispatch-gates --commands (7 paths): 34 derived; 33 node/pnpm commands exit 0 (each captured before any pipe) + pnpm --dir ui exec vitest run scripts/ exit 0 (Test Files 178 passed | 2 skipped, Tests 5065 passed | 2 skipped). --ran: 34 derived, 34 run, 0 NOT-MEASURED. check-platform-checklist --changed-from origin/main: 0 anchored files. ui check:new-line-citations: 0 new. PR-body gates before pr_create: check-changeset-no-major, check-adr-0087-registration, check-closing-keyword-parity, check-partof-closing-keyword all exit 0. CI check-runs: in_progress at report time.",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
"mcp_calls": "5 — read-only: issue_read x4 (get x1, get_comments x3), search_pull_requests x1; no MCP write",
"api_writes": "2 — POST /repos/objectstack-ai/objectstack/dispatches x2 (fleet-write relay: pr_create with assignees, run 40587029780, read-back identical 8581 bytes, assignee zhuangjianguo; this os-dev-report comment). git push not counted. Reviewer request not made (MCP writes out of bounds).",
"open_questions": [
{
"question": "newTabUrl given as an ABSOLUTE URL (QA's repro: https://example.com/export/{recordId}). The spec describes newTabUrl as 'A path template' and the fast path prefixes the backend origin, so an absolute value now opens a tab at origin + the absolute URL. Refuse it at authoring, or navigate it verbatim?",
"options": [
"A: refuse at authoring (spec refinement: newTabUrl must start with /), matching 'A path template' and the auth-enforcing same-backend endpoint the key exists for (cloud /sso-open); loud for AI-written metadata",
"B: navigate an absolute http(s) value verbatim in the fast path, as the api handlers already treat an absolute target; widens the key's meaning without a spec change",
"C: leave as is"
],
"recommendation": "A, because the spec text already says path template and the endpoint must enforce auth itself, so an off-origin value is an authoring mistake that should fail loudly rather than be tolerated in the renderer (contract-first); it needs a spec card (Fable tier), not this ui card."
}
],
"out_of_scope_findings": [
"class: none filed · opensInNewTab on a type:'api' action WITHOUT newTabUrl (pre-open, then follow a handler-returned redirectUrl) is not wired for api actions either; read-only observation, noted in Acceptance notes, carrier: none",
"class: none filed · core ActionRunner.executeAPI (non-console hosts with no registered api handler) also ignores the pair; read-only, carrier: none"
],
"deviations": "File surface: the seat's amendment 6452857550 added RecordDetailView.tsx (apiHandler) and its test; the RecordDetailView change composes the handler next to serverActionHandler and swaps the handlers-map entry rather than editing apiHandler's body (apiHandler is declared before serverActionHandler). The rule lives in a NEW file utils/newTabUrlFastPath.ts instead of consoleServerAction.ts, forced by 19 existing tests that factory-stub utils/consoleServerAction (exporting it there made them throw on mount). A second new test file sits beside useConsoleActionRuntime (hooks/__tests__/useConsoleActionRuntime.apiNewTabUrl-26747.test.tsx) for the list-toolbar acceptance. Commit trailers: model-free pair.",
"files_changed": [
".changeset/26747-api-action-new-tab-url.md",
"ui/packages/app-shell/src/utils/newTabUrlFastPath.ts",
"ui/packages/app-shell/src/utils/consoleServerAction.ts",
"ui/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx",
"ui/packages/app-shell/src/views/RecordDetailView.tsx",
"ui/packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.apiNewTabUrl-26747.test.tsx",
"ui/packages/app-shell/src/views/RecordDetailView.apiNewTabUrl-26747.test.tsx"
],
"line_budget": "552 changed lines (+546 / -6) across 7 files, under the 3000 human-merge threshold"
}
Activity
objectstack-fleet commentedon Oct 27, 2026
Triage: first grade —
priority:p2·domain:ui·area:records·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T17:04Z⛔ Not a claim, ⛔ not a dispatch.
type: "api"action withopensInNewTab+newTabUrlopens no tab and POSTs instead. Honour the declared new-tab behaviour on every surface that runs actions. From the studio-qa-2026-10-27 batch. Re-check the landing site on currentmain.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 1
Session:
session_01QsR2jTfvjNTaR29ehLk26TAccount:
zhuangjianguoBranch:
claude/project-thread-bol9ec(platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)Worktree:
objectstack-issue-26747Domain:
domain:uiSeat:
domain:ui#4File surface: whichever of
ui/packages/app-shell/src/utils/consoleServerAction.ts,ui/packages/app-shell/src/hooks/useConsoleActionRuntime.tsxorui/packages/core/src/actions/ActionRunner.tsholds the break (named in the report), tests beside, one root changeset; ⛔ a second interpolator; ⛔packages/spec/**, server packages, any new key (stop on breach; explain in the report)Container & model:
S,mode:thread,model: TIER_DEFAULT(dispatch-gates --tier: no path-derived mandate; reason: the maintainer's standing rule 「只认领不需要 fable 的卡」, 2026-10-27T10:39Z),effort: highClause-②: no
Metadata-protocol: none
Responsibility: the console action dispatch (ui, this lane) POSTs an
apiaction that declaresopensInNewTab+newTabUrlinstead of opening the tab, which breaks the spec's documented zero-roundtrip path · the platform path that may cover it: thenewTabUrlfast path inconsoleServerAction.ts(~181-210), which this dispatch does not reach · reached by every declared new-tab api action on the record header and more-menuThread-read: 6448334618
Premise: holds (
consoleServerAction.tsholds the fast path at ~188-208, yet QA G8 measured a POST from the header and more-menu on origin/main6090bb73at 2026-10-27T20:24Z)Serial constraints cleared: no open PR touches the surface (the thread re-checks at start)
Dispatched at 2026-10-27T20:29Z to the card thread on that branch; the order lives in the thread's opening message.
Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim amendment · domain:ui#4 · 2026-10-27T20:31Z
Thread-read: 6452823300
The card thread (
claude/project-thread-bol9ec) measured the break. The console registers twoapihandlers, and neither readsopensInNewTab/newTabUrl; both always fetch the target. The list pages run the one inuseConsoleActionRuntime.tsx, already in the surface. The record header and the ⋯ more-menu runRecordDetailView.tsx's ownapiHandler, passed in itsActionProviderhandlers map, which was outside the surface. The fix exports the fast path's own condition fromconsoleServerAction.ts. Both handlers then route an api action that meets it to the existing console server-action handler, which already resolves the record id, pre-opens the tab and substitutes{recordId}. No second interpolator.File surface:
ui/packages/app-shell/src/utils/consoleServerAction.ts(the exported predicate),ui/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx,ui/packages/app-shell/src/views/RecordDetailView.tsx(itsapiHandleronly), tests beside (includingRecordDetailView.apiNewTabUrl-26747.test.tsx), one root changeset; ⛔ui/packages/core/**,packages/spec/**, server packages, any new key (stop on breach)Clause-②: no
Metadata-protocol: none
An absolute
newTabUrl(QA's value) is a separate open question. The spec calls the key a path template, and the fast path prefixesbaseUrl. That behaviour is not changed here.objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{ "issue": 26747, "status": "done", "branch": "claude/project-thread-bol9ec", "pr": "https://github.com/objectstack-ai/objectstack/pull/26922", "session": "session_01QsR2jSH8FbKjEztmGT1sbB", "premise_still_valid": true, "summary": "The failing link is the console's registered type:'api' handlers, not the runner or the surfaces: ActionRunner.dispatchCollected hands an api action to the registered handler, and both api handlers (useConsoleActionRuntime.apiHandler for list surfaces and ConsoleShell's root provider, RecordDetailView.apiHandler for the record header and its more-menu) always fetched target and never read opensInNewTab/newTabUrl; the fast path existed only in the script-registered handler built by createConsoleServerActionHandler. New utils/newTabUrlFastPath.ts holds the rule: takesNewTabUrlFastPath (now also the fast path's own condition in consoleServerAction.ts) and withNewTabUrlFastPath(apiHandler, consoleHandler), which routes such an action to the surface's console server-action handler (its record-id resolution, pre-opened tab and {recordId} substitution; no second interpolator) and every other api action to apiHandler unchanged, forwarding the fast path's row-count refusal only for those actions. Both hosts register the composed handler; the hook also returns it as apiHandler.", "tests": "head 1bcc587b. Build closure turbo build app-shell^... + formula/client/sdui-parser/lint: 63 tasks, VERDICT command-exit 0. pnpm --filter @object-ui/app-shell run type-check (covers tests): exit 0 at 1bcc587b. New files RecordDetailView.apiNewTabUrl-26747 (real header click, real more-menu click, control) + useConsoleActionRuntime.apiNewTabUrl-26747 (real ActionRunner list toolbar, returned apiHandler, two-row refusal, control): Test Files 2 passed (2), Tests 7 passed (7). Ablation via ablation-replace.mjs on newTabUrlFastPath.ts ('? consoleHandler(action, context)' -> '? apiHandler(action, context)', anchor 1->0, blob 3ef0dcca->087c133d): Tests 4 failed | 3 passed (7), exactly the four fast-path cases; restored blob == HEAD, git diff HEAD empty (app-shell tests import src, no dist leg). Narrowed app-shell run, 100 files (test files named for useConsoleActionRuntime / consoleServerAction / RecordDetailView / ConsoleShell + every app-shell test naming apiHandler or newTabUrl): Test Files 100 passed (100), Tests 736 passed (736), VERDICT command-exit 0; first run at 0c1ffeb2 was 19 failed files (stubs of utils/consoleServerAction lacked the new export), fixed by moving the rule to its own module. Rest of app-shell left to CI Test UI (declared narrowing). ESLint on the 5 touched ui files: 0 errors, no warning on changed source lines.", "gates": "dispatch-gates --commands (7 paths): 34 derived; 33 node/pnpm commands exit 0 (each captured before any pipe) + pnpm --dir ui exec vitest run scripts/ exit 0 (Test Files 178 passed | 2 skipped, Tests 5065 passed | 2 skipped). --ran: 34 derived, 34 run, 0 NOT-MEASURED. check-platform-checklist --changed-from origin/main: 0 anchored files. ui check:new-line-citations: 0 new. PR-body gates before pr_create: check-changeset-no-major, check-adr-0087-registration, check-closing-keyword-parity, check-partof-closing-keyword all exit 0. CI check-runs: in_progress at report time.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "5 — read-only: issue_read x4 (get x1, get_comments x3), search_pull_requests x1; no MCP write", "api_writes": "2 — POST /repos/objectstack-ai/objectstack/dispatches x2 (fleet-write relay: pr_create with assignees, run 40587029780, read-back identical 8581 bytes, assignee zhuangjianguo; this os-dev-report comment). git push not counted. Reviewer request not made (MCP writes out of bounds).", "open_questions": [ { "question": "newTabUrl given as an ABSOLUTE URL (QA's repro: https://example.com/export/{recordId}). The spec describes newTabUrl as 'A path template' and the fast path prefixes the backend origin, so an absolute value now opens a tab at origin + the absolute URL. Refuse it at authoring, or navigate it verbatim?", "options": [ "A: refuse at authoring (spec refinement: newTabUrl must start with /), matching 'A path template' and the auth-enforcing same-backend endpoint the key exists for (cloud /sso-open); loud for AI-written metadata", "B: navigate an absolute http(s) value verbatim in the fast path, as the api handlers already treat an absolute target; widens the key's meaning without a spec change", "C: leave as is" ], "recommendation": "A, because the spec text already says path template and the endpoint must enforce auth itself, so an off-origin value is an authoring mistake that should fail loudly rather than be tolerated in the renderer (contract-first); it needs a spec card (Fable tier), not this ui card." } ], "out_of_scope_findings": [ "class: none filed · opensInNewTab on a type:'api' action WITHOUT newTabUrl (pre-open, then follow a handler-returned redirectUrl) is not wired for api actions either; read-only observation, noted in Acceptance notes, carrier: none", "class: none filed · core ActionRunner.executeAPI (non-console hosts with no registered api handler) also ignores the pair; read-only, carrier: none" ], "deviations": "File surface: the seat's amendment 6452857550 added RecordDetailView.tsx (apiHandler) and its test; the RecordDetailView change composes the handler next to serverActionHandler and swaps the handlers-map entry rather than editing apiHandler's body (apiHandler is declared before serverActionHandler). The rule lives in a NEW file utils/newTabUrlFastPath.ts instead of consoleServerAction.ts, forced by 19 existing tests that factory-stub utils/consoleServerAction (exporting it there made them throw on mount). A second new test file sits beside useConsoleActionRuntime (hooks/__tests__/useConsoleActionRuntime.apiNewTabUrl-26747.test.tsx) for the list-toolbar acceptance. Commit trailers: model-free pair.", "files_changed": [ ".changeset/26747-api-action-new-tab-url.md", "ui/packages/app-shell/src/utils/newTabUrlFastPath.ts", "ui/packages/app-shell/src/utils/consoleServerAction.ts", "ui/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx", "ui/packages/app-shell/src/views/RecordDetailView.tsx", "ui/packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.apiNewTabUrl-26747.test.tsx", "ui/packages/app-shell/src/views/RecordDetailView.apiNewTabUrl-26747.test.tsx" ], "line_budget": "552 changed lines (+546 / -6) across 7 files, under the 3000 human-merge threshold" }Generated by Claude Code