Skip to content

fix(app-shell): a URL parameter never sends a message on the AI build page (objectui#12493) - #12536

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12493-handoff-never-sends
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12493-handoff-never-sends

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12493
Clause-②: no

What changed

The AI build page (packages/app-shell/src/console/ai/AiChatPage.tsx) no longer reads ?handoffPrompt= from the URL. A URL can no longer send a message to the build agent on the signed-in user's behalf. A stale parameter in an old link is ignored, and the existing URL mirror drops it once the conversation id is in the URL.

  • The in-app handoff moved to navigation state. The ask surface's "Open in Builder" handoff (ADR-0057 P4, ADR-0063 decline-and-redirect) now calls navigate('/ai/build', { state: { handoffPrompt, parentConversationId } }). The page reads it with useLocation().state. Only the app's own navigate() call can set that state. The user's click on the handoff card is the action, so the handed-off text is still sent once on arrival, as before. ?package= stays in the URL because it only scopes the surface and sends nothing.
  • The prompt is consumed once. Browser history keeps an entry's state across a reload. So as the page seeds the prompt, it replaces that history entry's state with one that has no handoffPrompt, and a reload does not send it again. parentConversationId stays in the entry until the URL mirror navigates to /ai/build/ID without state, as the URL parameter did before. Seeding is armed once per history entry (location.key), not per prompt value, so a second handoff with the same text still seeds. The seed effect now runs before the route canonicalization and URL mirror effects. If they all run in the same flush, the mirror's navigation therefore wins over this same-URL replace.
  • No confirm dialog was added: the rule is that the URL never sends.

Census

  • searchParams.get / useSearchParams in packages/app-shell/src/console/ai/: only AiChatPage.tsx reads URL parameters (new, package, agent, and before this change handoffPrompt / parentConversationId). new, package and agent only select or scope a conversation and send nothing.
  • stashPendingFirstMessage, autoSend, initialPrompt and pendingFirstMessageRef.current = across packages/app-shell/src and packages/plugin-chatbot/src: the handoff seed was the only out-of-band seed.
  • handoffPrompt across the repo (packages, apps, docs, tests): the only producer was openBuilder in this file. The other hits are CHANGELOG entries. No other producer needs a URL, so no prefill seam is kept.

Tests

New: packages/app-shell/src/console/ai/__tests__/AiChatPage.handoffNeverFromUrl-12493.test.tsx. It renders the real page and fakes the chat hook so that every sendMessage call is recorded.

  • A page loaded with ?handoffPrompt= on the build route sends nothing (sent stays [] after the conversation mints and the mirror lands).
  • The in-app handoff, through the real openBuilder reached from the pane's onOpenBuilder, sends its text exactly once. The URL never carries it, and the build agent's first turn carries the source conversation id.
  • A reload of that history entry (a remount on the same pathname, search and state) sends nothing. The entry's state no longer has handoffPrompt.
  • CONTROL: a user-typed first message sends.

Gate readings, at head ea7adc772:

  • pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build: exit 0.
  • pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json; the test project compiles __tests__): exit 0.
  • pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/console/ai/: exit 0, Test Files 36 passed (36), Tests 210 passed (210).
  • pnpm check:control-bytes, check:i18n-keys, check:handler-key-reads and check:unreferenced-sources, plus node scripts/check-changeset-fixed.mjs: all exit 0.
  • Narrowed eslint (eslint --no-inline-config) on the 2 changed source files: 0 errors and 34 warnings. All 34 warnings are in AiChatPage.tsx, and the base file at 1c3b0699a has the same count (0 errors, 34 warnings). ① Population: eslint.config.js lints both files (--format json returned 2 file results). ② File count: 2, read from that JSON. ③ Invariance: type-aware linting is not enabled (no parserOptions.project / projectService in eslint.config.js), so this diff cannot change the verdict on any untouched file. The repo-wide pnpm lint is left to CI.

Ablation, done after the fix was committed. node scripts/ablation-replace.mjs (from objectstack) restored the URL read in the seed effect, using the old value-keyed guard. The anchor count went 1 to 0 and the blob changed from 8b7df383 to bb8a2768. Result: × a page loaded with ?handoffPrompt= sends nothing, with AssertionError: expected [ ... ] to deeply equal [] and Tests 1 failed | 3 passed (4). The other three pins stayed green. Restore was proven by blob == HEAD (8b7df3836950) and an empty git diff HEAD.

Acceptance notes

  • packages/plugin-chatbot/src/useObjectChat.ts (around lines 800-805) has a comment saying the URL mirror strips "the param" after a handoff send. The behaviour it describes still holds, because the mirror's state-less navigation drops parentConversationId. Only the wording is stale. It is outside this card's file surface, so it is left for whoever next edits that file.
  • Reach: the same auto-send ships in the objectstack console pin and in the released 17.7.0 console (see the triage). Bumping the console pin is the triage seat's follow-up.

Generated by Claude Code

… page (objectui#12493)

The build page no longer reads ?handoffPrompt= from the URL. The in-app
"Open in Builder" handoff from the ask surface now travels as react-router
navigation state, which only the app's own navigate() can set, and the prompt
is removed from the history entry as it is seeded so a reload does not send
it again. A stale parameter in an old link is ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
…nder (objectui#12493)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 69 chunks) 3086.7 KB 3119.4 KB
Main entry chunk (gzip) 73.3 KB 350 KB
Entry file index-CpCpfJ9S.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) 20.90KB 7.67KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
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.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.62KB 0.83KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
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.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 594.75KB 143.42KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 254.55KB 71.19KB
fields (index.js) 282.89KB 72.11KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
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.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.67KB 11.85KB
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) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
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.04KB 3.92KB
plugin-calendar (index.js) 54.94KB 16.05KB
plugin-charts (index.js) 84.97KB 23.36KB
plugin-chatbot (index.js) 201.72KB 48.07KB
plugin-dashboard (index.js) 143.88KB 38.88KB
plugin-designer (index.js) 233.95KB 50.08KB
plugin-detail (index.js) 262.16KB 69.28KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 179.38KB 46.59KB
plugin-gantt (index.js) 178.36KB 45.08KB
plugin-grid (index.js) 262.33KB 72.76KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 127.76KB 32.64KB
plugin-map (index.js) 27.07KB 8.96KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 46.66KB 13.13KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 93.05KB 23.86KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
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 (node-slots.js) 7.18KB 2.34KB
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-approval-decision.js) 0.20KB 0.20KB
types (record-components.js) 0.20KB 0.19KB
types (record-panels.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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants