Skip to content

fix(app-shell,console): the tool preview opens the API console inside the app, with its request preset (objectui#10591) - #10632

Merged
os-litant merged 4 commits into
mainfrom
claude/issue-10591-tool-preview-api-console-link
Sep 25, 2026
Merged

os-litant merged 4 commits into
mainfrom
claude/issue-10591-tool-preview-api-console-link

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10591
Clause-②: no

What changed

The tool preview's "Open in API Console" link (Studio, Tools, a tool, the preview's toolbar) now opens the API console inside the current app, and the API console pre-fills the request the link carries.

  • @object-ui/app-shell, ToolPreview.tsx. The link was the bare /developer/api-console?path=.... The console's root route table has no /developer route, so its path="*" catch-all redirected the author to /. The link now points at the developer:api-console registry key inside the app in the URL: /apps/APP/component/developer/api-console. The path is built from useParams().appName and componentRefToUrlSegments('developer:api-console'). The query is built with URLSearchParams and carries path=/api/v1/ai/tools/NAME/execute and method=POST. The link is rendered through react-router's Link, so the router basename applies. It still opens in a new tab. With no router above the preview (the designer gallery), or on a route with no :appName, no link is drawn.
  • @object-ui/console, ApiConsolePage.tsx. The page reads path and method from useSearchParams() once, in a lazy useState initialiser. It pre-fills the URL field and the method selector and sends nothing. The rule for the preset is written in the page header:
    • path is honoured only when it starts with /api/. An absolute http://... URL, a protocol-relative //host URL, any other path and an empty value are ignored.
    • method is read only together with an honoured path, in any letter case. It is used only when it is one of the verbs the selector offers. The selector now renders that same list (REQUEST_METHODS).
    • With no honoured path, the page opens exactly as before.
  • One patch changeset covers both packages.

Nothing else moves. There is no route-table change in either AppContent.tsx or in App.tsx. How the page sends requests and keeps its history is unchanged.

Measurements behind the design (the dispatch's M1 to M4)

  • M1: where the preview renders. In the console, ToolPreview renders inside ResourceEditPage at /apps/APP/metadata/tool/NAME. Framework's Studio app (studio.app.ts) names metadata:resource with type: 'tool', and the nav resolves that to /apps/APP/metadata/tool. Both of app-shell's AppContent branches mount metadata/:type/:name under /apps/:appName/*: the with-app branch and the no-active-app branch. So :appName is always in the URL there. The /studio/:packageId/:tab design surface lists no tools. The designer gallery (preview-gallery.tsx) mounts previews with no router. Source chosen: useParams().appName, the URL's own app segment. ResourceListPage and most app-shell views build their /apps/... links from that same segment. For the key-to-URL step I used the existing componentRefToUrlSegments. I added no new link helper.
  • M2: which target. I chose the registry-key URL. app-shell owns component/:ns/:name/* (ComponentNavView) in every app. The developer/api-console path is declared in the console's systemRoutes, a detail of one host. The registry key is also what Studio's Developer navigation names, so the link and the sidebar's "API Console" entry reach the same path. ComponentNavView also forwards the query as props. ApiConsolePage takes no props and ignores them. The query read goes through the router instead, so it works on both mounts.
  • M3: the preset reaches the page on both mounts. Pinned through the real route table (see below).
  • M4: other app-less /developer/... links. IntegrationsPage builds ${basePath}/developer/api-console with basePath from :appName. It is only mounted under /apps/:appName, so the prefix is always there, and it navigates through the router. That is not a defect, and it is outside the claimed files, so I left it alone. The same sweep found an app-less link of the same class in a sibling preview: see Findings.

Pins (real components, no module mock of the link builder)

packages/app-shell/src/views/metadata-admin/previews/ToolPreview.test.tsx has a new describe block. It renders the real ToolPreview in a MemoryRouter under /apps/:appName/*:

  • the link resolves to /apps/studio/component/developer/api-console, which is the component URL of developer:api-console;
  • it stays in whichever app the author is in (com.acme.crm);
  • it carries path=/api/v1/ai/tools/delete_all_orders/execute and method=POST;
  • a tool name that needs escaping round-trips exactly;
  • under basename="/_console" the href starts with /_console/apps/...;
  • it still has target=_blank and rel=noreferrer;
  • no link on a route without an app, and none with no router (no throw).

apps/console/src/pages/developer/ApiConsolePage.requestPreset-10591.test.tsx is new. It uses the real page, the real registerDeveloperComponents, app-shell's real DefaultAppContent and the console's real systemRoutes. The shell chrome is stubbed the way orphanedPageComponentRefs-10520.test.tsx stubs it:

  • with the preset, the path and POST are pre-filled, and no fetch goes to /ai/tools/ (endpoint discovery still runs);
  • control: with no query string, the URL field is empty and the method is GET;
  • the preset is ignored for an absolute URL, a protocol-relative URL, a path outside /api/ and an empty value;
  • the method is accepted in any case, and an unknown verb is ignored;
  • the preset is read once: a later query change does not overwrite the editor;
  • end to end: the href the real registered tool preview draws, followed into the route table, reaches the API console pre-filled and not the root catch-all;
  • both mounts honour the preset: component/developer/api-console and developer/api-console;
  • harness control: an app-less URL does reach the catch-all stand-in. App.tsx's root table is not exported, so a probe stands in for its catch-all.

Reverse proofs

Each proof ran through ../objectstack/scripts/ablation-replace.mjs in wrap mode, from committed state. Each anchor hit once (1 to 0). The restore was proven by blob equal to HEAD and an empty git diff HEAD.

  1. The old href restored in ToolPreview (the pre-fix template string). 7 red: six link pins in app-shell and the console end-to-end pin. The no-link, new-tab and harness-control pins stayed green. 7 failed and 21 passed of 28.
  2. The query read dropped (the lazy initialiser returns null). 6 red: every preset-dependent pin, including both mounts and end to end. The control and the ignore pins stayed green, as they should with no preset. 6 failed and 6 passed of 12.
  3. The /api/ guard dropped. 3 red: absolute, protocol-relative, and outside /api/. The empty-value case stays green because the null check still refuses it. 3 failed and 9 passed of 12.

Local verification (all at head c0052db4f)

  • pnpm exec turbo run build --filter='@object-ui/console^...' --concurrency=2: 34 of 34 tasks successful. That includes app-shell's own tsc && check-dist-completeness build.
  • pnpm --filter @object-ui/app-shell type-check: exit 0. pnpm --filter @object-ui/console type-check: exit 0. I counted with --listFiles: app-shell's tsconfig.test.json program includes ToolPreview.test.tsx, and the console's program includes the new test.
  • pnpm --filter @object-ui/console build (tsc && vite build && build:plugin): exit 0. The post-build plugins reported: declared-lazy-views 11 of 12 lazy with 1 eager pinned, types-zod still lazy, eager-closure report emitted.
  • Suites that name ToolPreview, ApiConsolePage, the preview registry, registerDeveloperComponents or the preview samples: 33 files and 404 tests passed.
  • The whole previews directory: 61 files and 702 tests passed. The console test project: 119 files and 1305 tests passed.
  • NOT MEASURED: the full @object-ui/app-shell package test. It has 799 test files, which does not fit the foreground cap. I declared it to CI's sharded pnpm test.
  • Gates, all exit 0: check-changeset-presence, check-changeset-no-major, check:new-line-citations (0 new citations), check:control-bytes, check:changeset-claims (the new changeset reads clean against itself, and no pending changeset names a touched file), check:i18n-keys, check:vi-mock-specifiers, check:test-path-roots, check-vi-mock-override-shape, check:eager-closure (3048.5 KB of 3104.5 KB), check:phantom-deps, check:esm-specifiers, check:self-import.
  • Lint on the four touched files, one run: 0 errors and 4 warnings. All four warnings are on lines this diff does not touch: two no-explicit-any in ApiConsolePage (client: any and the catch (err: any)), and two exhaustive-deps on ToolPreview's existing exampleArgs useMemo. This is a narrowed run, and three facts back it:
    • the flat config applies to all four files (all four are in the results, none ignored);
    • the file count, 4, is read from --format json;
    • no type-aware linting is configured (no parserOptions.project or projectService in eslint.config.js), so this diff cannot move a verdict on an untouched file.
  • Not probed in a running app.

Acceptance notes

  • The preset carries no request body. The execute endpoint takes { "parameters": ... }, and the discovery catalog's /tools/:toolName/execute entry has that as its body template. The author types the body.
  • The API console is registered only by @object-ui/console. A host that embeds app-shell without registering developer:api-console now lands on app-shell's "Component not registered" state for that key, not on its root.
  • In the zero-app console, both candidate URLs fall to the "no apps" branches. Neither component/... nor developer/... is a system or metadata route. That was already the case before this change, and this change does not alter it.
  • The designer gallery's tool card no longer shows the link, because the gallery mounts with no router and there is no app to open the console in.

Findings (not fixed here; the seat files them)

  • class a: AgentPreview's "Try in chat" link is the hard-coded /console/ai/agents/NAME/chat, a plain anchor. The console's root route table declares /ai/:agent and /ai/:agent/:conversationId, and nothing under /console. So the root catch-all redirects it to /, the same failure this card had. I read this at the source and did not probe it in a running app. The agent is reachable from Studio, which lists agents through metadata:resource with type: 'agent'. Dedupe words: AgentPreview Try in chat · console/ai/agents chat link · agent preview chat link redirects home.

Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C


Generated by Claude Code

… app, with its preset

The tool preview's "Open in API Console" link pointed at a bare
/developer/api-console URL, which the console's catch-all redirects home. It
now points at the developer:api-console registry key inside the current app,
built through componentRefToUrlSegments and rendered through the router so
the mount basename applies. ApiConsolePage reads the path and method preset
from the query string once on mount and pre-fills the request editor; a path
that does not start with /api/ is ignored, and nothing is sent.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3050.5 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-BKp3ycS3.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 546.99KB 130.83KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 260.13KB 65.84KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.40KB 14.61KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.50KB 35.37KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.96KB 61.65KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 149.07KB 38.02KB
plugin-gantt (index.js) 169.62KB 41.91KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.26KB 15.04KB
plugin-list (index.js) 114.60KB 28.31KB
plugin-map (index.js) 22.42KB 7.38KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.67KB 8.95KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.84KB 21.96KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c0052db4f5fa47abc0b360ebe9867d111ffb8adf

Rendered by an isolated review subagent spawned by the domain:ui#4 seat; its served tier was checked against its transcript stamps (136 of 136 model stamps at the review tier). Adopted by this seat.

① Derived judgments

  • (a) The link resolves. Traced through the real tables at head. Console App.tsx declares /apps/:appName/* (element AppContent, protected) and a trailing * that renders RedirectWithSplash to /; it declares no /developer path and no /console path. Console AppContent wraps app-shell's DefaultAppContent with extraRoutes and extraRoutesNoApp both set to systemRoutes, which carries developer/api-console. app-shell AppContent's with-app branch declares component/:ns/:name/* (element ComponentNavView), metadata/:type/:name (element MetadataResourceEditPage) and {extraRoutes}; the no-active-app branch declares metadata/:type/:name and {extraRoutesNoApp} and no generic component/... route (only the legacy component/metadata/... redirects). ComponentNavView joins :ns and :name with a colon, looks developer:api-console up in the registry, and renders the registered component with the query string spread as string props. registerDeveloperComponents.tsx registers that key as a Suspense wrapper around the lazy ApiConsolePage, which declares no props. So /apps/APP/component/developer/api-console?path=...&method=POST reaches ApiConsolePage with the query intact; no route consumes the query and the router location is one object at every nesting level. Pinned end to end by the console test through the real DefaultAppContent and the real systemRoutes.
    • Same builder as the Studio sidebar: componentRefToUrlSegments is ref.split(':').filter(Boolean) in app-shell's componentRegistry.ts. The sidebar's href for a component nav item is built in @object-ui/layout (resolveHref in NavigationRenderer.tsx, and AppSchemaRenderer.tsx) as ${basePath}/component/${segs.join('/')} with segs = ref.split(':').filter(Boolean) and basePath equal to /apps/APP (UnifiedSidebar.tsx). Same algorithm and same URL shape, not the same function: layout sits below app-shell and cannot import the helper. objectstack studio.app.ts names componentRef: 'developer:api-console' with no params in group_developer, so the sidebar entry and the preview link land on the identical path. The changeset sentence about the path Studio's Developer navigation reaches is true.
    • useParams().appName where the preview renders: ResourceEditPage renders the registered preview (getMetadataPreview(type)) inline, with no nested Routes element and no second React root, as the element of metadata/:type/:name in both AppContent branches, nested under the root /apps/:appName/*. react-router merges ancestor params into descendant matches, so appName is in scope there. Studio's metadata:resource item with params.type: 'tool' resolves through NavigationRenderer's metadata special case to /apps/APP/metadata/tool, matching M1. The /studio/:packageId/:tab surface has three pillars (data, automations, interfaces) and maps no tool type; if a tool preview ever rendered without an app, the outcome is no link, not a throw.
    • Basename with a new tab: Link computes its href through useHref, which joins the router basename onto the resolved pathname before createHref; a target of _blank makes the router's click handler leave the click to the browser, which opens that basename-qualified href. The console resolves its basename from an injected base element (resolveBasename in App.tsx; vite.config.ts names /_console/ as a mount point). Pinned with a real MemoryRouter under basename /_console. Sound.
    • The split into ApiConsoleLink (one unconditional useInRouterContext call) and RoutedApiConsoleLink (one unconditional useParams call) keeps each component's hook order constant, and whether a router sits above a mount cannot change during that mount. Sound; pinned as no link and no throw with no router.
  • (b) The preset read. readRequestPreset(searchParams) runs inside a useState lazy initialiser, so once per mount, and methodOverride and urlOverride are seeded from it once. No effect reads preset or searchParams afterwards (the other preset identifiers in the file belong to the pre-existing QUERY_PARAM_PRESETS chips). Pinned: a later navigate to a different query leaves the editor unchanged. It holds on both mounts: useSearchParams reads the router's current location, which is the page's own URL under developer/api-console and under ComponentNavView alike; nothing keys the element on the search string, and the registered wrapper is a module-scope function, so a query change re-renders without remounting. Under ComponentNavView the query also arrives as props, which ApiConsolePage() does not declare. Both mounts pinned through the real route table.
  • (c) Security and safety of the preset. The guard is path.startsWith('/api/'); the send path is ${client.baseUrl}${effectiveUrl} with baseUrl equal to VITE_SERVER_URL or empty (AdapterProvider). A value that passes the guard begins with a slash followed by a, so it can be neither a scheme-relative //host reference nor carry a scheme; appended after a configured origin, or resolved as a rooted path against the page origin, the authority is fixed before the path is parsed. /api/\\evil is a path with a backslash (the URL parser treats it as a slash for http), /api//evil.com is a path with an empty segment, /api/..%2F.. keeps its percent-encoded characters as a literal segment. None leaves the origin. One nuance the header over-states: /api/../x passes the guard and normalises to /x on the same origin, so this is a same-origin guard rather than an under-/api/ guard; the literal string is what the URL field shows before Send. Not a defect: the stated threat, aiming the request editor at another origin, is closed. Nothing is sent on load: sendRequest is called only from the Send button's onClick and from Enter in the URL field; no effect calls it and the discovery hook reads no query string. Pinned: no fetch to /ai/tools/ with the preset present. A crafted link can pre-fill DELETE on a /api/v1/data/... path. The verb shows in the method selector with its red colouring, the path in the URL field, the body is empty, and Send must be pressed; the console is an authenticated developer tool and the same request could be typed. Judgment: acceptable, the residual risk is social. A marker that the preset came from a link would be a nice-to-have, not a requirement.
  • (d) The tool execute endpoint. objectstack origin/main (949e99bed9) carries no service-ai package and no buildToolRoutes symbol anywhere in the tree: packages/runtime/src/domains/ai.ts dispatches the /ai/** family to "the AI service's registered route handlers" and describes service-ai as a Cloud/Enterprise package absent on an open-edition boot (501). The builder's source is therefore out of this review's reach. Every in-tree reference at that sha agrees on path and verb: docs/audits/2026-06-toolschema-property-liveness.md ("the route is /ai/tools/:toolName/execute", citing tool-routes.ts), packages/spec/src/ai/tool.zod.ts (POST /ai/tools/:name/execute), packages/runtime/src/domains/automation.ts (POST /ai/tools/:tool/execute) and docs/qa/platform-checklist/areas/ai.json (POST /api/v1/ai/tools/qa_probe_tool/execute). objectui's useApiDiscovery.ts mirrors the cloud ledger with POST /tools/:toolName/execute under /api/v1/ai and a { parameters: {} } body template. method=POST is right. The buildToolRoutes name in the ToolPreview docblock is inherited from the pre-fix comment and is unverifiable from objectstack main.
  • (e) Behaviour changes beyond the card. The designer gallery (preview-gallery.tsx) mounts previews with no router and lists tool, so its tool card now shows no link; disclosed in the PR body (M1 and Acceptance notes) and in the changeset; acceptable, since the old link there was a dead URL. REQUEST_METHODS is ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'], identical in members and order to the inline list it replaces, and HttpMethod is that same five-member union; disclosed in the PR body; acceptable. Nothing else: the ToolPreview.tsx diff is the docblock pointer, three imports, API_CONSOLE_REF, the two link components and the toolbar expression; the ApiConsolePage.tsx diff is the header, one import, the constant, readRequestPreset, the useSearchParams and useState pair, two seeded initial states and the selector's list. sendRequest, replayHistoryEntry and the history state are untouched.
  • (f) The pins are real. ToolPreview.test.tsx adds 9 it blocks in a real MemoryRouter with two routes and no module mock; the same-builder pin imports the real componentRefToUrlSegments. ApiConsolePage.requestPreset-10591.test.tsx has 12 it blocks (pre-fill, control, four ignore cases, method case, read-once; end to end, two mounts, harness control) over the real page, the real registerDeveloperComponents, the real DefaultAppContent and the real systemRoutes. Reverse arithmetic re-derived from the case lists: restoring the old href string inside the to expression reddens the six href pins (registry-key path, same-builder path, other app, path plus method, escaping round-trip, basename) and the console end-to-end pin, 7 of 28; nulling the lazy initialiser reddens pre-fill, method case, read-once, end to end and both mounts, 6 of 12, with control, the four ignore cases and the harness control green; dropping the /api/ guard reddens absolute, protocol-relative and outside-/api/, 3 of 12, the empty value still refused by the null check. All three match the dev's counts. Mocks: the console pins stub the shell chrome, useAuth, useMetadata (two apps so /apps/studio resolves), useAdapter (no client, so baseUrl is empty) and fetch (a 404 recorder, which is what the no-send assertion reads). getMetadataPreview and the tool registration arrive through the package entry's register-builtins.js, outside the mocked views/metadata-admin index, so the end-to-end pin draws the real link. MetadataResourceEditPage is stubbed, so the step "the preview lives inside ResourceEditPage with appName in scope" rests on the route-table reading in (a), not on a pin. No pin is tautological.
  • (g) Changeset truth. .changeset/10591-tool-preview-api-console-link.md declares @object-ui/app-shell and @object-ui/console patch; both are in the fixed group. Every sentence re-derived at head: the old bare href and its catch-all redirect, the missing query read, the registry-key path and its equality with the Studio nav path, the path and method query, the router basename and the new tab, the no-router and no-app no-link rule, the once-on-mount read, the no-send rule, both mounts, the /api/ guard with its three ignored shapes, the method rule, and "opens exactly as before" (a null preset seeds both states with their former empty values). No line-address citation. Other pending changesets naming these surfaces: 10520-orphaned-console-pages.md (the four /apps/:app/developer/ routes unchanged and developer:api-console named by Studio: still true), 5291-api-console-storage-slot.md (useApiDiscovery untouched: still true), previews-shared-empty-description-8526.md (ToolPreview still renders EmptyDescription at text-xs with no local Empty: still true). No sentence is now false.
  • (h) Out-of-scope finding, class a: confirmed. AgentPreview.tsx builds chatUrl as /console/ai/agents/NAME/chat and renders it as a plain anchor with target _blank. The root table in App.tsx and StudioRoute.tsx declare no path beginning with /console; the chat routes are /ai, /ai/:agent and /ai/:agent/:conversationId. The anchor matches only the trailing * redirect home, and as a plain anchor it also ignores the router basename. Same shape as this card.

② Semver level

patch on both packages is right and the changeset agrees. objectui AGENTS.md's version rule: "minor/patch 独立演进——objectstack 没动时不必跟发;objectui 自己的改动照常用 changeset 推进" and "changeset 里不要声明 major … objectui 自身的破坏性变更也标 minor". This is a bug fix: a dead link now resolves and a dropped preset is honoured. The diff adds no export; REQUEST_METHODS and readRequestPreset are module-private; ToolPreview still takes MetadataPreviewProps; no route, spec key or exported type is added or removed. Not major.

Clause-②: no stands. The pm-dispatch criterion ("判据:本卡放宽接受集或扩大公开面吗"; "条款②只指已发布契约面"; the published surface being the packages' exports map) is not met. path and method are page-scoped URL query parameters of one host page, produced and consumed inside the same fixed release group; they are not an exported type, a prop, a spec key or an accept set. app-shell's urlParams.ts itself classes such names as page-scoped and unreserved, and neither collides with a reserved name. It is a UI deep-link detail.

③ Boundary flags

  • Model identifiers: none in the four commit messages, their trailers, the PR title or the body; the only trailers are the co-author line and the session-link line.
  • File surface, 5 files against the claim: packages/app-shell/src/views/metadata-admin/previews/ToolPreview.tsx (the link's construction only), ToolPreview.test.tsx beside it, apps/console/src/pages/developer/ApiConsolePage.tsx (the query read, plus the selector rendering the shared verb list), the new ApiConsolePage.requestPreset-10591.test.tsx beside it, and one changeset declaring both packages (the claim's "changesets for both" is the same declaration in one file). apps/console/src/App.tsx, apps/console/src/AppContent.tsx and packages/app-shell/src/console/AppContent.tsx are untouched, so no route-table change; sendRequest, replayHistoryEntry and history are untouched, so no change to sending or history.
  • Serial: the 11 other open PRs (10630, 10629, 10628, 10627, 10620, 10601, 10596, 10574, 10278, 8941, and the 1732-file release PR 5400 swept over all 18 pages of its file list) touch none of the five files, ComponentNavView, registerDeveloperComponents, componentRegistry or the three route tables. 7 commits landed on origin/main since the merge-base b1030c71a; none touches those files.
  • mergeable_state: behind (mergeable true): the base moved, no conflict.
  • Check-runs on the head (all 43 carry c0052db4f5fa47abc0b360ebe9867d111ffb8adf): 40 success, 3 skipped, 0 failures, 0 in_progress after foreground polling (first read 28 success, 11 in progress, 3 skipped; complete at 13:15:48Z on the second 60-second poll). Every non-success is skipped: Test (coverage), dependabot, Test (coverage shard ${{ matrix.shard }}/4). The successes include Line Citation Gate, Changeset Claim Re-read, Changeset Bump Policy, Changeset Declaration, Lint, Type Check, Test, the eight Test shards and Governed Surface Queue Guard.
  • PR body: Fixes #10591 is the only closing keyword; zero angle brackets; no line-address citation; its shas c0052db4f, 651632062 and b1030c71a are the head, an ancestor of origin/main and the merge-base. Every factual sentence I could re-derive at head is true (the route shapes, M1 to M4, the pin descriptions, the acceptance notes, the finding). The build, type-check, gate, lint and test counts are the dev's own measurements and were not re-run here; the check-runs above are the independent instrument for them.
  • Observations, none blocking: the non-English fragment AGENTS.md §测试纪律 in the new test's comment cites that file's heading by its literal name, as 169 existing non-md files already do; the ApiConsolePage header's "same-origin API path" wording is a shade stronger than the guard (see (c)); the ToolPreview docblock's buildToolRoutes name cannot be confirmed from objectstack main (see (d)).

Implemented-by: claude/issue-10591-tool-preview-api-console-link
Reviewed-by: session_01BP8CMtACxTdLjqR6rhd33C

VERDICT: PASS


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 25, 2026 13:27
@os-litant
os-litant added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit eca2760 Sep 25, 2026
45 checks passed
@os-litant
os-litant deleted the claude/issue-10591-tool-preview-api-console-link branch September 25, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants