Skip to content

fix(app-shell, plugins): hosted-console UX defects from the 2026-09-28 cloud E2E (six of seven) - #10924

Merged
hotlong merged 7 commits into
mainfrom
claude/issue-10899-hosted-console-ux-defects
Sep 28, 2026
Merged

hotlong merged 7 commits into
mainfrom
claude/issue-10899-hosted-console-ux-defects

Conversation

@hotlong

@hotlong hotlong commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Refs #10899 — six of its seven items land here; item 4 needs a maintainer decision and the replay-ordering half of item 5 is root-caused but not changed, so the card stays open.

Part of objectstack-ai/cloud#2440

Clause-②: no

One commit per item, each with its own test, ablation and changeset. Session https://claude.ai/code/session_786273a0-0246-4bb2-b026-bb37ba5d7295 (dispatch branch claude/issue-10899-hosted-console-ux-defects).

Verdict per item

# Item Verdict
1 Assistant FAB covers the dock's send button done — the FAB renders only while the dock is collapsed (ConsoleLayout)
2 Failed record comment shown as sent done (objectui half of cloud#2431, per the ruling that cloud mounts audit on hosted tenants) — a comment/reply lands only after its sys_comment write resolves; a failed write shows a localized toast and the composer keeps the draft
3 Dashboard 403 renders as a raw English exception done — queryDataset throws a typed AnalyticsForbiddenError for 403 PERMISSION_DENIED; DatasetWidget renders a localized no-access state. The missing grant itself is cloud#2439
4 Assistant scope chip says 「新建应用」 inside an app open — needs a maintainer decision (below). Not changed here
5 Empty 「执行过程」 blocks; replay orders steps differently empty blocks: done. Ordering: root-caused, producer outside this change (below). Not changed here
6 Studio edit/design offered to a non-platform-admin done — the producer is the console. The server's permission answer is already right; the console ignored it
7 Marketplace labels an older version as an update done — "update" means higher SemVer 2.0.0 precedence, at both call sites

What each commit does

  • Item 7 — versionPrecedence.ts orders two versions by SemVer 2.0.0 §11 (the grammar the package manifest declares for version). cloudUpdateAvailable and the install dialog's per-environment hasUpdate both use isNewerVersion instead of !==. Non-SemVer or equal pairs offer no update.
  • Item 1 — the FAB is the dock's collapsed affordance (ADR-0057 open question 2). ConsoleLayout now renders it only while dock.expanded is false, which is the rule ChatDockLauncher already follows.
  • Item 6 — the page pencil (PageView) and the top-bar hammer (AppHeader) required only the workspace-admin role, and an org owner holds that role. Both now also require manage_metadata through useCanAuthorMetadata, which reads GET /api/v1/auth/me/permissions. HomePage's builder CTAs already use the same check. If the server reports no systemPermissions, the check fails open, so older servers keep the admin-only behaviour.
    • Measured producer: on the PM's rig control DB (read-only), the signed-up owner holds cloud_control_plane_member (no system permissions) and organization_admin_no_bypass = manage_org_users, setup.access, setup.write. Framework's organization_admin withholds manage_metadata on purpose. So the server's answer was correct and the console did not read it.
  • Item 2 — RecordDetailView no longer adds optimistic rows. It awaits the create, then appends the row. On failure it shows toast.error(detail.commentFailed) (new key, all ten packs) and rejects back to the caller.
    • The three composers (RecordActivityTimeline, ThreadedReplies, RecordComments) now handle a rejected onAddComment / onAddReply explicitly: they keep the draft. Before this, the throw escaped as an unhandled rejection.
    • RecordDetailView.feedRecordScope.test.tsx had a case that asserted the opposite of the ruling ("keeps an unpersisted comment when the write never lands"). It is rewritten to the ruled behaviour.
  • Item 3 — queryDataset gains a code-based branch for the analytics read admission's 403 + PERMISSION_DENIED (readAdmissionDeniedError in @objectstack/service-analytics). The new AnalyticsForbiddenError carries httpStatus: 403 and code: 'PERMISSION_DENIED'. DatasetWidget classifies the error with the shared classifyLoadError, the same classifier the list view uses. For forbidden it renders dashboard.widgetForbiddenTitle / widgetForbiddenMessage (new, all ten packs). A 403 with no code keeps the generic error.
  • Item 5 (empty blocks) — a reloaded build hydrates one assistant message per persisted step, and each step's text is the (called …) placeholder. ChatbotEnhanced now shows the 执行过程 note only when the turn shows no tool row and no build or design panel. The copy/regenerate bar renders only for a turn with visible prose. The chore(deps-dev): bump storybook from 8.6.15 to 8.6.17 #772 fallback, where a placeholder turn with nothing else keeps its note, is preserved and pinned.

Item 4 — why it is not changed here

The in-app dock runs the build agent on an app-less, product-only conversation scope (ChatDock.tsx, chatConversationScope with no appId). It sends no package binding. So in ADR-0057 Amendment A1's terms the chip is telling the truth: the thread is unbound, and "New app" is the unbound label. A1.b also records the dock keeping the legacy product-only key as a decision. Making the chip name the current app means either:

  • A — bind the dock's build thread to the current app's package (editPackageId + app:PKG:build scope). The agent then gets a declared write target, and the dock, /ai/build?package= and the Studio copilot share one thread. This reverses the A1.b decision.
  • B — relabel the unbound chip. The agent would still guess the target app.

Recommendation: A, for the build product only, and only when the active app is an authorable package. Details are in the report on the card. It changes a decided ADR line, so it waits for the maintainer.

Item 5 — replay ordering (measured, not changed)

I read the rig tenant DB (ai_messages, read-only). The follow-up proposal turn (EFYj) was still persisting its last todo_write and final prose at 04:36:42–44Z. The user's 「确认,开始搭建。」 (turn dh2L) was persisted at 04:36:39Z. The conversation store orders by created_at, id across turns (cloud packages/service-ai/src/conversation/objectql-conversation-service.ts, MESSAGE_ORDER). The replay therefore puts the confirm message inside the previous turn's steps, while the live run showed that turn whole.

Two things contribute:

  • the store orders by time, not by turn (cloud);
  • the plan card's build button stays clickable while its own turn is still streaming (ChatbotEnhanced).

Both are named in the report.

Tests

All runs are on HEAD d8969d4bc unless noted.

  • New pins (one per item, each ablated — revert the fix → red, restore → git diff HEAD empty):
    • item 7: versionPrecedence-10899.test.ts, MarketplacePackagePage.updateIsHigherVersion-10899.test.tsx (ablation: 1 failed)
    • item 1: ConsoleLayout.fabYieldsToOpenDock-10899.test.tsx (1 failed)
    • item 6: PageView.editInStudioNeedsAuthoring-10899.test.tsx, AppHeader.designInStudioNeedsAuthoring-10899.test.tsx (2 failed)
    • item 2: RecordDetailView.commentWriteFailure-10899.test.tsx + the rewritten feed-scope case (3 failed). Composer half: composerKeepsDraftOnRejection-10899.test.tsx — with the catch removed, the run exits 1 on unhandled rejections while the draft assertions still pass. Keeping the draft used to be an accident of the throw.
    • item 3: queryDataset.forbidden-10899.test.ts, DatasetWidget.forbiddenState-10899.test.tsx (2 failed)
    • item 5: ChatbotEnhanced.hydratedStepTurns-10899.test.tsx (2 failed)
  • Package suites: one vitest run over packages/app-shell/ packages/plugin-chatbot/ packages/plugin-dashboard/ packages/data-objectstack/ packages/plugin-detail/ packages/i18n/ — Test Files 1409 passed | 2 skipped (1411), Tests 14850 passed | 23 skipped (14873), exit 0
  • Build: turbo run build for the six touched packages plus the @object-ui/app-shell^... closure — 29/29 successful.
  • Type-check: turbo run type-check for the six packages — 6/6 successful.
  • check:i18n-keys, check:i18n-drift, check:i18n-dead-keys, check:new-line-citations (0 new), check:control-bytes, check-changeset-presence (6 changesets for 6 packages), check-changeset-no-major — all exit 0.
  • Lint, narrowed to this diff: eslint --no-inline-config over the 33 changed .ts/.tsx files. For every pre-existing file, the error/warning counts at HEAD equal its counts at the merge base (the only errors are 4 pre-existing no-console in data-objectstack/src/index.ts, 4/127 → 4/127). New files have 0 errors. Type-aware linting is not enabled, so this diff cannot change the verdict on any untouched file. The repo-wide lint belongs to CI.

Acceptance notes

  • ⚠️ The shared verify lock was not taken: the wrapper reported UNLOCKED (declared) · no usable flock on this host. Build, type-check and suites ran unserialized.
  • NOT MEASURED: an in-browser check of the six fixes. The PM's rig was observed through its screenshots and read-only DB reads only — no sign-in, no writes.
  • Out of scope, noted and not filed:
    • handleToggleReaction still swallows a failed reaction write (same optimistic shape as item 2; no carrier).
    • The reply input (ThreadedReplies) renders only under an item with replyCount > 0, and no feed read sets that, so a loaded discussion offers no reply affordance.
    • Other admin-role-only authoring affordances (view add/rename/delete in ObjectView, the report editor, ObjectDataPage) do not read manage_metadata. Same class as item 6, not measured on the control plane.
    • ObjectChart's dataset path, DatasetReportRenderer and DashboardFilterBar still print the (now typed, English) refusal message instead of a localized state.
  • Changesets: six, all patch.

Generated by Claude Code

hotlong and others added 6 commits September 28, 2026 14:15
The package detail page compared the installed and latest approved versions
with `!==`, so an environment holding v1.1.0 while v1.0.0 was the latest
approved read "Update available -> v1.0.0". Both the page badge/button and the
install dialog's per-environment hint now use SemVer 2.0.0 precedence
(`isNewerVersion`); equal, older or non-SemVer pairs offer no update.

Co-authored-by: Claude <noreply@anthropic.com>
The FAB is the dock's collapsed affordance, but ConsoleLayout rendered it in
both states; over an open rail its fixed bottom-right position covered the
composer's send button. It now renders only while the dock is collapsed,
matching ChatDockLauncher.

Co-authored-by: Claude <noreply@anthropic.com>
…ability

The page pencil and the top-bar hammer were gated on the workspace-admin role,
which an organization owner holds while framework's organization_admin set
withholds manage_metadata. On the cloud control plane that offered a customer
the platform's own page editor. Both now also require manage_metadata via
useCanAuthorMetadata (the server's /me/permissions answer, fail-open when the
server reports none), matching HomePage's builder CTAs.

Co-authored-by: Claude <noreply@anthropic.com>
… as sent

RecordDetailView appended the comment (or reply) optimistically and swallowed
the sys_comment create rejection, so a 404 on a tenant without sys_comment read
as a posted comment that vanished on reload. The row is now appended only after
the write resolves; a failed write raises a localized toast
(detail.commentFailed, all ten packs) and rejects back to the composer, which
keeps the draft. The three composers treat a rejected onAddComment/onAddReply
explicitly as "not written". The feed-scope pin that asserted the swallowed
failure is rewritten to the ruled behaviour.

Co-authored-by: Claude <noreply@anthropic.com>
…shows "no access"

queryDataset had no branch for the analytics read admission's 403
PERMISSION_DENIED, so it threw the generic "Dataset query failed: 403
Forbidden - [Analytics] Access denied: ..." string that DatasetWidget printed
verbatim. It now throws a typed AnalyticsForbiddenError (httpStatus 403, code
PERMISSION_DENIED), and DatasetWidget renders a localized no-access state for
anything classifyLoadError reads as forbidden (dashboard.widgetForbidden*, all
ten packs). Other failures keep the detailed alert; a code-less 403 stays
generic.

Co-authored-by: Claude <noreply@anthropic.com>
A reloaded multi-step build hydrates into one assistant message per step whose
text is the "(called ...)" placeholder. Each rendered its tool row, the italic
agent-activity note, and a full-height (invisible) action bar, so every step
read as an empty block. The note is now only the fallback for a placeholder
turn that shows no tool row or build/design panel, and the copy/regenerate bar
renders only for a turn with visible prose.

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 4 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/6661-app-launcher-nav-menu-renderers.md

  • names en.ts → packages/i18n/src/locales/en.ts — edited by this change

    Three new strings — the launcher's and the menu's accessible names, and the menu's empty state — are declared under console.nav in en.ts and its nine sibling packs. An inline defaultValue alone is not a fix: it renders English at one call site and leaves the string untranslatable everywhere (objectui#3517).

.changeset/6687-chatbot-surface-authorable.md

  • names ChatbotEnhanced.tsx → packages/plugin-chatbot/src/ChatbotEnhanced.tsx — edited by this change

    content/docs/plugins/plugin-chatbot.mdx's Properties table listed surface ('card' | 'plain', "bordered panel or a frameless full-page workspace"), but the key had zero read points: none of the three ComponentRegistry.register('chatbot*', ...) sites in renderer.tsx forwarded it, and ChatbotSchema did not declare it. surface was real only as a prop of the React component — ChatbotEnhanced.tsx defines ChatbotSurface, defaults it to 'card', and branches six layout decisions off isPlainSurface — so it was reachable by a hand-written React host and by nobody writing metadata. An author who wrote surface: 'plain' got the 'card' default, with no error and no signal.

.changeset/7125-dashboard-empty-state-keys-retired.md

  • names DatasetWidget.tsx → packages/plugin-dashboard/src/DatasetWidget.tsx — edited by this change

    Not touched: table.noRows ('No rows to display') and engine.form.noRows (packages/app-shell/src/views/metadata-admin/i18n.ts, read at widgets.tsx) — two different, same-named keys in different namespaces. Nor the comments in WidgetEmptyState.tsx, DatasetWidget.tsx, ObjectDataTable.tsx and PivotTable.tsx that record WHY three widgets with three strings became one shared empty state; the packs' own comment keeps that rationale and now names the retirement instead of a row that is gone.

.changeset/7997-detail-view-related-retired.md

  • names RecordDetailView.tsx → packages/app-shell/src/views/RecordDetailView.tsx — edited by this change

    Why it retired. @objectstack/spec declares no DetailView schema at all — every DetailView occurrence in packages/spec/src is prose about this repo's own RecordDetailView.tsx — so this array mirrored no protocol schema and drifted freely: it declared columns as TableColumn[] while the renderer it fed also accepted bare field names, { field, label } and legacy { name, label } spellings. The axis that carried the ruling was measured zero pull: no application code authored the member, both internal producers of a detail-view node (RecordDetailDrawer, renderers/record-details.tsx) synthesize it without related, and the only in-tree authorings carrying real columns were two documents — both rewritten here.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 29c76e573 (merge-base with origin/main): 33 file(s) changed outside .changeset/, read against 1669 pending declaration(s) that publish a body (2264 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3096.5 KB 3104.5 KB
Main entry chunk (gzip) 148.5 KB 350 KB
Entry file index-DT2Mpn4l.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.58KB 6.17KB
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.22KB 10.61KB
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.70KB 2.23KB
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.70KB 10.94KB
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) 557.59KB 133.60KB
core (index.js) 9.93KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.59KB 63.15KB
fields (index.js) 261.01KB 66.28KB
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 (translateFn.js) 0.20KB 0.18KB
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.32KB 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.96KB 14.83KB
plugin-charts (index.js) 83.99KB 22.86KB
plugin-chatbot (index.js) 197.72KB 46.92KB
plugin-dashboard (index.js) 137.82KB 36.70KB
plugin-designer (index.js) 215.03KB 44.21KB
plugin-detail (index.js) 233.51KB 61.80KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 161.21KB 41.41KB
plugin-gantt (index.js) 170.35KB 42.19KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.17KB 12.20KB
plugin-timeline (index.js) 31.00KB 9.09KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 88.55KB 22.21KB
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.81KB 3.58KB
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) 119.16KB 39.05KB
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) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
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.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.22KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
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.26KB
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.27KB 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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3096.5 KB 3104.5 KB
Main entry chunk (gzip) 148.5 KB 350 KB
Entry file index-DT2Mpn4l.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.58KB 6.17KB
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.22KB 10.61KB
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.70KB 2.23KB
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.70KB 10.94KB
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) 557.59KB 133.60KB
core (index.js) 9.93KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.59KB 63.15KB
fields (index.js) 261.01KB 66.28KB
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 (translateFn.js) 0.20KB 0.18KB
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.32KB 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.96KB 14.83KB
plugin-charts (index.js) 83.99KB 22.86KB
plugin-chatbot (index.js) 197.72KB 46.92KB
plugin-dashboard (index.js) 137.82KB 36.70KB
plugin-designer (index.js) 215.03KB 44.21KB
plugin-detail (index.js) 233.51KB 61.80KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 161.21KB 41.41KB
plugin-gantt (index.js) 170.35KB 42.19KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.17KB 12.20KB
plugin-timeline (index.js) 31.00KB 9.09KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 88.55KB 22.21KB
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.81KB 3.58KB
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) 119.16KB 39.05KB
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) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
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.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.22KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
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.26KB
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.27KB 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

@hotlong
hotlong added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit ac15833 Sep 28, 2026
44 checks passed
@hotlong
hotlong deleted the claude/issue-10899-hosted-console-ux-defects branch September 28, 2026 09:34
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…nt authorable app (objectui#10926) (objectstack-ai#10950)

Fixes objectstack-ai#10926
Clause-②: no

The console's assistant dock now binds its **build** thread to the
current app, per the maintainer ruling 「绑定当前应用(推荐)」 (epic session,
2026-09-28). The ADR amendment that records the ruling is its own
governed draft PR, objectstack-ai#10947. This PR has code, tests and a changeset only.

## What changed

- **`layout/ChatDock.tsx`**, the dock's default body
(`ChatDockConversation`):
- When the resolved agent is `build` and the current app's package is
authorable, the dock passes that package as `editPackageId`. That makes
its scope `chatConversationScope({ appId: PKG, product: 'build' })`,
which is `app:PKG:build`: the key the Studio copilot and
`/ai/build?package=PKG` resolve.
- "Authorable" is the A1.b switcher's own predicate,
`isPlatformBuiltinApp` (the `com.objectstack.*` prefix). There is no
second resolver.
- `ChatPane` receives `editPackageId`, so the chip, the edit-mode empty
state and `context.packageId` name the app.
  - `ask`, a built-in app and no app keep the product-only key.
- The dock runs the same A1.b migration read the `?package=` entry runs
(`legacyScope` + `adoptLegacy`). Without it, a dock that resolves first
would mint a fresh `app:PKG:build` thread, and the full page's adoption
of a product-only thread bound to PKG could then never happen.
- The pane receives only a scope-matched conversation, the same gate
`AiChatPage` uses. Moving between apps now changes the scope while the
dock stays mounted.
- The panel and the mobile sheet pass the bound package to `onMaximize`.
- **`layout/ConsoleLayout.tsx`**:
  - Hands the dock `activeApp?._packageId` (both presentations).
- `openDockFullPage(boundPackageId)` navigates to
`/ai/build?package=PKG` with the Studio door's one-shot opt-out
(`objectstack:ai-full-page-requested`). Without the opt-out, a thread
that built the app would bounce straight to Studio on arrival
(objectui#5799).
  - With no bound package it navigates to `/ai`, unchanged.
- **`console/ai/AiChatPage.tsx`**: the adopt predicate becomes the
exported `isThreadBoundToPackage(messages, packageId)`, so the full page
and the dock share one predicate. `adoptLegacyBuildThread` calls it, and
its behaviour is unchanged. No other region of the file is touched.
- **`hooks/chatScope.ts`**: module doc only (names the dock as a third
`app:X:build` surface).
- `.changeset/10926-dock-binds-build-thread-to-app.md`:
`@object-ui/app-shell` minor.

No new package-entry export and no new public prop. `ChatDockPanel` and
`ChatDockMobileSheet` are not exported from the package entry;
`isThreadBoundToPackage` is a module export of `AiChatPage.tsx`, like
its neighbour `deriveBoundPackageId`. No locale key moves.

## Deviation: one file outside the claim's declared surface

The claim's file surface did not list `layout/ConsoleLayout.tsx`. It
came from the dispatch assumption that "`editPackageId` is read in
`chatScope.ts` and `layout/ChatDock.tsx`". Measured on base `af2221d`,
`ChatDock.tsx` never read `editPackageId` and passed `appId: undefined`.
The current app (`activeApp`) and the maximize navigation
(`openDockFullPage`) live only in `ConsoleLayout.tsx`. The ruling cannot
be delivered without touching that file, short of a second current-app
resolver, which the dispatch ruled out.

What was touched there: two `appPackageId` prop lines and the maximize
target. PR objectstack-ai#10924 (landed) changed a different hunk of the same file,
the FAB line. `main` was merged twice (`7e8b3c0`, then `5c94589`), both
cleanly.

## Tests (head `8d4efd6`)

New `layout/__tests__/ChatDock.appBinding-10926.test.tsx` renders the
real dock body, the real `ChatPane`, the real `useChatConversation` and,
for the sharing pin, the real `AiChatPage`, over a `fetch` stub. It has
11 tests:

- The chip and the empty state read the app label, and
`context.packageId` is the app's package.
- The key is `app:app.crm:build`, and the product-only key stays empty.
- The dock and `/ai/build?package=X` settle on the same conversation id.
- A thread already keyed by another `app:X:build` surface is resumed.
- `ask` stays ambient.
- A `com.objectstack.*` app and no app keep `build`.
- A product-only thread bound to X is adopted, and its key is left in
place. One bound elsewhere is not adopted.
- Switching apps never pairs the old thread with the new package.
- Both maximize paths carry the bound package.

New `layout/__tests__/ConsoleLayout.dockBinding-10926.test.tsx` has 3
tests: the package reaches the rail and the sheet, a bound maximize goes
to `/ai/build?package=app.crm` with the opt-out, and an unbound maximize
goes to `/ai` with no opt-out.

Runs, from the repo root through the shared verify lock:

- `pnpm exec vitest run --maxWorkers=2` over `layout/__tests__/`,
`console/ai/__tests__/`, `console/__tests__/`,
`views/studio-design/__tests__/`, `chrome/`,
`hooks/__tests__/{chatScope,useChatConversation,surfaceAgent}` and two
`src/__tests__` pins: **Test Files 102 passed (102), Tests 750 passed
(750)**, exit 0.
- `pnpm --filter @object-ui/app-shell type-check` (`tsc --noEmit && tsc
-p tsconfig.test.json`): exit 0. Both new test files are in the test
program (`--listFilesOnly`).
- The dependency closure was built first (`turbo run build
--filter=@object-ui/app-shell^...`): 28/28 successful.

Not run locally: the full 844-file app-shell suite. CI runs it.

**Ablations.** Each was committed first, then mutated through
`ablation-replace.mjs` with the counts expected before running. After
each, the restore was proven: blob equals HEAD and `git diff HEAD` is
empty. No `dist` is involved, because the subjects are imported by
relative path.

1. Binding disabled (anchor `product === 'build' && appPackageId` →
`product === 'ablated' && appPackageId`): expected 8 failed / 3 passed,
**observed 8 failed / 3 passed**. The ask, built-in and no-app cases
stay green. A first attempt was refused by the tool because the
replacement contained the anchor, so nothing ran.
2. Scope-match gate removed (`const scopeMatched = conversationScope ===
scope;` → `const scopeMatched = true;`): expected 1 failed / 10 passed,
**observed 1 failed / 10 passed** (the app-switch pin).
3. Bound maximize branch removed in `ConsoleLayout` (`if
(!boundPackageId) {` → `if (boundPackageId === boundPackageId) {`):
expected 1 failed / 2 passed, **observed 1 failed / 2 passed**.

## Gates (head `8d4efd6`, exit codes)

- `node scripts/check-changeset-presence.mjs`: 0
- `node scripts/check-changeset-no-major.mjs`: 0
- `node scripts/check-governed-queue-guard.mjs --test` over the 7
changed paths: 0, NOT GOVERNED
- `check:new-line-citations`: 0
- `check:control-bytes`: 0
- `check:vi-mock-specifiers`, `check:vi-mock-inherit`,
`check:vi-mock-override-shape`: 0 each
- `check:changeset-claims`, `check:pending-changeset-literals`: 0 each
- `check:i18n-keys`, `check:test-path-roots`,
`check:unreferenced-sources`, `check:self-import`, `check:phantom-deps`:
0 each
- `eslint --no-inline-config` on the 6 changed TS/TSX files: exit 0, **0
errors**. This narrowing is a measurement, not a skip:
  - The population is `eslint.config.js`'s `**/*.{ts,tsx}` block.
  - The file count comes from `--format json`: 6.
- No `parserOptions.project` or `projectService` is configured, and the
repo's one custom rule reads no files. So the diff cannot move the
verdict of an untouched file.
- Against the base versions of the same files (fed through
`--stdin-filename`), the diff adds one
`react-refresh/only-export-components` warning, for the new exported
predicate. The file already has 12 of that class, and there is no
warning ratchet.

## Acceptance notes

- **`sys_metadata`.** The ruling's authorable test is
`isPlatformBuiltinApp`, which does not exclude the DB-authored
`sys_metadata` pseudo-package that `appRoute.ts`'s `studioPackageId`
refuses. An app carrying that `_packageId` would bind to
`app:sys_metadata:build`, as the A1.b switcher already lists it. Not
measured whether any app list item carries it. Carrier: none; noted here
only.
- **Thread per app.** Each authorable app a maker opens with the dock
expanded mints its own build conversation on first open, as the Studio
copilot does per package. The rows are listed in the `/ai` sidebar.
- **Bare `/ai/build`** is untouched. It still resolves the product-only
key, and the code did not require changing it.

---
_Generated by [Claude
Code](https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant