Skip to content

fix(console): FaviconSync stops writing the favicon, so an app keeps its own icon across in-app navigation (objectui#10379) - #10429

Merged
os-litant merged 2 commits into
mainfrom
claude/issue-10379-favicon-one-writer
Sep 25, 2026
Merged

os-litant merged 2 commits into
mainfrom
claude/issue-10379-favicon-one-writer

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10379

Clause-②: no

What was wrong

FaviconSync (apps/console/src/components/FaviconSync.tsx) re-applied the operator favicon, getFaviconUrl(), from an effect keyed on useLocation(). useAppShellBranding writes the active app's branding.favicon from an effect keyed on that URL. With an operator favicon configured, moving between two pages of the same app ran only the route-keyed writer. The operator favicon then replaced the app's for the rest of the visit. It is the favicon twin of the title race objectui#8637 removed.

The change

FaviconSync no longer writes the favicon, and it now writes nothing at all. Its docblock records both removals (the title, then the favicon) and explains why a mount-only write is not kept either. No other runtime file changes.

Why removing the write is enough, measured on the branch point 74fcda829:

  • When the operator favicon becomes known. getFaviconUrl() is a synchronous read of the @object-ui/app-shell runtime-config singleton. initRuntimeConfig() fills that singleton, and main.tsx awaits it in the Promise.all gate that runs before createRoot().render(). At 74fcda829, git grep -n "initRuntimeConfig(" -- apps packages found that call and no other, apart from tests, comments, docs and the definition itself. The runtime config has no subscription API. So the value is settled before the first render and does not change after it, and there is no late-arrival case to pin. That caller reading is not re-derived by any gate.
  • The boot already writes it. Before React mounts, two boot writers put the operator favicon on #favicon: the pre-React inline script in apps/console/index.html, and main.tsx just before render(). sharedGetJson joins the inline script's in-flight request. The script registers its continuation first, so its write lands before main.tsx resumes.
  • So the route-keyed write had nothing left to do. Its first run repeated the boot's write. Each later run either repeated that write again (when no shell owned the icon) or overwrote an app's own icon (the defect).
  • Leaving the app. Since objectui#10372 (cd7b728b, an ancestor of the branch point), the shell captures the href attribute it finds on mount and restores it on unmount. With no write left in FaviconSync, the shell is the only favicon writer while React runs. What it captures is therefore the operator favicon, whichever order the components mount in. Under StrictMode, which main.tsx uses, the shell restores and then captures again, and the second capture still reads the operator favicon.
  • Why not keep a mount-only write. It would only repeat the boot's write. It would also be correct only while FaviconSync renders before the shell. Ablation leg 2 below shows that order dependence as a red test.

Evidence (code head 6e529c3eb)

The new pin is apps/console/src/__tests__/faviconAfterNavigation.test.tsx. It renders the real FaviconSync and the real AppShell from @object-ui/layout (which calls useAppShellBranding) under a MemoryRouter. The icon link is copied from the shipped index.html. The shipped pre-React script runs against a stubbed GET /api/v1/runtime/config, and initRuntimeConfig() joins its request, so getFaviconUrl() answers from the real singleton. fetch is the only stub. The file has 13 tests:

  • environment controls;

  • navigating inside the app, and navigating a third time;

  • switching apps;

  • leaving the app, with and without an operator favicon;

  • an app without its own favicon (the control);

  • both render orders, and StrictMode.

  • Branch point 74fcda829, before the fix: Tests 4 failed | 9 passed (13). The in-app navigation case reads Expected: "https://cdn.example.test/app-a.svg", Received: "/operator-O.png".

  • Head 6e529c3eb: the pin passes, and so do the neighbouring suites (tabTitleAfterNavigation, App.uploadAltitude-10131, App.docsPortalLazy, internalFormShell, runtimeConfigBootDedup, and the packages/layout suite app-shell-branding-favicon-restore): Test Files 7 passed (7), Tests 44 passed (44).

  • Ablation on the committed fix, through ablation-replace. Each leg's anchor hit once and the blob changed. Each restore was proven by blob == HEAD and an empty git diff HEAD.

    • Leg 1 puts the [location]-keyed write back: Tests 4 failed | 9 passed (13), including the in-app navigation case.
    • Leg 2 uses a mount-only write ([] deps): Tests 1 failed | 12 passed (13). Only the order with FaviconSync rendered AFTER the shell fails.
  • Real Chromium 141 (/opt/pw-browsers/chromium) against the console Vite dev server. A throwaway page rendered the same tree under BrowserRouter and StrictMode, with a cold deep link to /apps/a. The operator favicon was written the way main.tsx writes it.

    variant boot on mount after in-app nav after leaving
    before (a byte copy of the 74fcda829 component) /operator-O.png app-a.svg /operator-O.png /operator-O.png
    after (this branch) /operator-O.png app-a.svg app-a.svg /operator-O.png

    An app without its own favicon read /operator-O.png at every step in both variants. The probe files were deleted after the run, and none of them is committed.

Gates (local, code head 6e529c3eb)

  • node scripts/check-changeset-presence.mjs: exit 0. It printed "2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)".
  • pnpm check:new-line-citations: exit 0, VERDICT new-cross-file-line-citations: 0 new citation(s).
  • pnpm check:control-bytes, pnpm check:changeset-claims, pnpm check:test-path-roots, pnpm check:pending-changeset-literals: exit 0 each.
  • Lint, narrowed to the two touched files. eslint --format json ran from apps/console, whose own lint script is eslint . under the root eslint.config.js. The JSON reports 2 files, 0 errors, 0 warnings.
    • Population: the console has no config of its own, so both files are linted under the root flat config.
    • Invariance: that config sets no parserOptions.project or projectService, so linting is not type-aware. No custom rule in eslint-rules/ reads the filesystem. So this diff cannot change a verdict on any untouched file.
  • Console type-check: NOT MEASURED as the gate. It waits on ^build of the 35-package closure, which is too long for the foreground cap on this shared box. CI Type Check owns it. A narrowed run was done instead:
    • tsc -p used a throwaway tsconfig that extends apps/console/tsconfig.json. Its files were the two touched files plus vite-env.d.ts, and @object-ui/* was mapped to package sources.
    • --listFilesOnly shows both files in the program, and neither has a diagnostic.
    • The run's 93 diagnostics are all TS6133, TS6196 or TS6198, unused-declaration reports in other packages' sources under the console's noUnusedLocals.
    • Positive control: a throwaway file in the same directory with a deliberate TS2322 was reported. The throwaway files were removed.

The changeset is .changeset/10379-favicon-one-writer.md, declaring '@object-ui/console': patch. The dispatch said the console is not a released package, but the population of check-changeset-presence says it is. @object-ui/console is in the fixed group of .changeset/config.json, so its apps/console/src/ is guarded. The objectui#8637 changeset (.changeset/8637-tab-title-one-writer.md) also declares '@object-ui/console': patch.

Patch round 1 (head 6da5b45ee): two pending changesets corrected

The seat answered the report's open question with A in comment 5824829480: the inert FaviconSync and its App.tsx mount stay. The same comment amended the claim's file surface to add .changeset/8637-tab-title-one-writer.md, body only. The coordinator's patch-round message also asked for every other pending changeset that names FaviconSync, BrandingSync or the favicon to be re-read, by symbol as well as by path, and corrected where this PR makes it false.

Commit 6da5b45ee corrects two sentences:

  • .changeset/8637-tab-title-one-writer.md said the console's route-keyed writer "is now FaviconSync, which syncs only the favicon". It now says the writer "became FaviconSync, which kept only the favicon write until objectui#10379 removed that too".
  • .changeset/10040-app-shell-favicon-restore.md said "In the console that something is FaviconSync, which only writes when an operator favicon is configured". It is now in the past tense: FaviconSync "wrote the icon only when an operator favicon was configured (until objectui#10379 removed that write)".

Both frontmatters are byte-identical. The sha256 of the block between the two --- lines is the same at 74fcda829 and at 6da5b45ee: 63f43504… for 8637 and ee79b57f… for 10040. check-changeset-overwrite (report-only) lists both as modified, with "declared at base" equal to "declares now".

What was read. Pending changesets at HEAD were searched with git grep -l -i -E 'FaviconSync|BrandingSync|favicon' and 'getFaviconUrl|icon link|faviconUrl|route-keyed|route keyed' over .changeset/*.md. The control: BrandingSync must hit 8637, and it does. Six files matched:

  • 8637-tab-title-one-writer: one sentence corrected. The other sentences were re-read against head and are true. "Both run on the commit that mounts the shell" narrates the defect, and the sentences around it put it in the past.
  • 10040-app-shell-favicon-restore: one sentence corrected. The others are true at head.
  • 10379-favicon-one-writer: this PR's own changeset, true.
  • 4830-app-shell-branding-rightrail-prose, console-boot-request-dedup-5544, console-preboot-branding-origin-5660: they mention the favicon but not FaviconSync, and nothing in them is made false by this PR.

Gates on 6da5b45ee, each exit 0: node scripts/check-changeset-presence.mjs, pnpm check:changeset-claims, node scripts/check-changeset-no-major.mjs, pnpm check:control-bytes, node scripts/check-changeset-overwrite.mjs, pnpm check:new-line-citations, pnpm check:pending-changeset-literals.

6da5b45ee differs from 6e529c3eb only in those two changeset bodies. git grep finds no test or script that names either file. So the test, ablation, browser, lint and type-check readings above, taken on 6e529c3eb, still describe this head.

Premise check

  • Held: FaviconSync had the location-keyed effect, the shell captures and restores the icon (objectui#10372), and the defect reproduces (red above, in the DOM and in the browser).
  • Corrected: useAppShellBranding lives in @object-ui/layout (packages/layout/src/AppShell.tsx), not in @object-ui/app-shell. It was read, not edited.

Acceptance notes

  • FaviconSync now renders nothing and runs no effect, and App.tsx still mounts it. The seat ruled to keep it (comment 5824829480, answer A). Its docblock records both removed writes, and both pins render the real component. Deleting it is separate dead-code removal. Three other leftovers are now inert:

    • that mount in App.tsx;
    • vi.mock('../components/FaviconSync') in App.uploadAltitude-10131.test.tsx;
    • the getFaviconUrl entries (returning an empty string) in three console test mocks of @object-ui/app-shell.

    Carrier: none.

  • useAppShellBranding writes the icon href but never its type. With an operator .png favicon and an app .svg favicon, the link reads type="image/png" with the app's .svg URL while the app is shown. That pairing already appeared on every shell mount. This PR makes it last for the whole visit instead of only until the first in-app navigation. Whether a browser renders that icon was not measured. Carrier: none.

  • The Branding section of the layout docs (content/docs/layout/app-shell.mdx) says the colour properties are removed on unmount. It does not mention the title and favicon restores, so it is incomplete but not false. It was not touched.

This draft was written by the domain:ui#4 seat's dispatched developer, session https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C. It stays a draft on purpose: marking it ready and queueing it are the seat's steps.


Generated by Claude Code

…its own icon across in-app navigation (objectui#10379)

The favicon twin of the objectui#8637 title race. FaviconSync re-applied
the operator favicon from an effect keyed on useLocation(), while
useAppShellBranding writes the app's branding.favicon from an effect keyed
on that URL. Moving between two pages of one app ran only the route-keyed
writer, and the operator icon replaced the app's.

One writer: FaviconSync now writes nothing. The boot (index.html's
pre-React script and main.tsx) puts the operator favicon up before React
mounts, from a runtime config main.tsx awaits before the first render, and
the shell captures and restores that icon (objectui#10040). A mount-only
write would only repeat the boot and would be right only while FaviconSync
renders before the shell, so it is not kept.

The pin renders the real FaviconSync and the real AppShell under a
MemoryRouter, boots the favicon through the shipped index.html script
against a stubbed runtime/config fetch, and covers in-app navigation,
leaving the app, an app without its own favicon, both render orders and
StrictMode.

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) 3043.6 KB 3104.5 KB
Main entry chunk (gzip) 148.4 KB 350 KB
Entry file index-C0Y9UeLX.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.68KB 6.20KB
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.18KB 10.59KB
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.41KB 1.23KB
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) 26.08KB 7.56KB
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) 542.46KB 129.64KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 224.12KB 62.26KB
fields (index.js) 255.56KB 64.73KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
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.39KB 3.10KB
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.44KB 14.62KB
plugin-charts (index.js) 72.54KB 20.26KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.45KB 35.33KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 260.94KB 67.93KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.34KB 37.47KB
plugin-gantt (index.js) 168.47KB 41.53KB
plugin-grid (index.js) 215.43KB 58.90KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.36KB 28.24KB
plugin-map (index.js) 22.05KB 7.14KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.72KB 8.96KB
plugin-tree (index.js) 10.74KB 3.76KB
plugin-view (index.js) 85.86KB 21.38KB
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) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
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) 0.20KB 0.18KB
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

…tes the favicon (objectui#10379)

Two pending changesets name FaviconSync in the present tense, and this
branch made both statements false: FaviconSync now writes nothing.

- 8637-tab-title-one-writer: "it is now FaviconSync, which syncs only
  the favicon" now reads that it became FaviconSync, which kept only the
  favicon write until objectui#10379 removed that too.
- 10040-app-shell-favicon-restore: "that something is FaviconSync,
  which only writes when an operator favicon is configured" moves to the
  past tense and names objectui#10379 as the change that removed the write.

Body only. Both frontmatters are byte-identical to HEAD (sha256 of the
block between the two --- lines compared before commit).

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) 3043.6 KB 3104.5 KB
Main entry chunk (gzip) 148.4 KB 350 KB
Entry file index-DILnC8yP.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.68KB 6.20KB
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.18KB 10.59KB
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.41KB 1.23KB
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) 26.08KB 7.56KB
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) 542.46KB 129.64KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.86KB 62.25KB
fields (index.js) 255.56KB 64.73KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
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.39KB 3.10KB
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.44KB 14.62KB
plugin-charts (index.js) 72.54KB 20.26KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.45KB 35.33KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 260.94KB 67.93KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.34KB 37.47KB
plugin-gantt (index.js) 168.47KB 41.53KB
plugin-grid (index.js) 215.43KB 58.90KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.36KB 28.24KB
plugin-map (index.js) 22.05KB 7.14KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.72KB 8.96KB
plugin-tree (index.js) 10.74KB 3.76KB
plugin-view (index.js) 85.86KB 21.38KB
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) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
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) 0.20KB 0.18KB
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: 6da5b45eef6f42d068c7a6a2207732a755493ee1

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

① Derived judgments

  • Head and lineage. GET pulls/10429 returns head.sha 6da5b45eef6f42d068c7a6a2207732a755493ee1 on claude/issue-10379-favicon-one-writer, as expected. Two commits on top of the branch point 74fcda8292280167a4c22f6edf6586f29135b7da (the merge-base with origin/main): 6e529c3eb (fix, test, own changeset) and 6da5b45ee (the two changeset-body corrections only, by --name-status). No amend, force or rebase is visible in the chain. cd7b728b (objectui#10040 / PR objectui#10372, the shell's favicon restore) is an ancestor of the branch point, as the body says.
  • (a) One-writer claim: holds. At head FaviconSync is a bare return null with no imports, no hook and no DOM write. Measurements re-derived at head: getFaviconUrl() is return current.branding?.faviconUrl on the module-level let current in packages/app-shell/src/runtime-config.ts, synchronous. The singleton is mutated only by the non-exported applyUpdate, which is called only from initRuntimeConfig, and by resetRuntimeConfigForTesting. The module exports no subscribe, listener, emit or observer API (grep over the file: none). initRuntimeConfig( has exactly one production caller, apps/console/src/main.tsx, inside the Promise.all whose .finally calls createRoot().render(); every other hit at head and at the branch point is a test, a comment, a doc or the definition. Pre-React writers confirmed: the inline applyEarlyBranding script in apps/console/index.html and main.tsx before render() both set href and type on #favicon. Complete writer list at head (non-test, all of apps, packages, examples): (1) the inline script in apps/console/index.html; (2) apps/console/src/main.tsx before render(); (3) useAppShellBranding in packages/layout/src/AppShell.tsx. Nothing else touches #favicon, link[rel="icon"] or an icon link. Post-boot arrival: initRuntimeConfig's docblock says repeat calls "re-fetch and re-merge", so a second call would change the singleton silently, but no production code makes one; there is no runtime branding update, org-switch re-init or preview-mode favicon path in the tree (the designer's faviconUrl strings are i18n labels for an app's own branding.favicon, which flows through the shell, not the operator icon). The docblock's caveat that a future re-read would need its own writer is the right disclosure. If the config fetch fails, getFaviconUrl() is undefined before and after the PR, so that path is unchanged.
  • (b) Leaving the app and render order: holds. The hook is a plain useEffect with deps [primaryColor, accentColor, favicon, title]; it captures link.getAttribute('href') before writing and restores it (or removes the attribute) in cleanup, gated on branding?.favicon. With FaviconSync inert the only React-phase writer is the shell, so in either mount order the captured value is whatever boot left, the operator favicon. Under StrictMode the cleanup restores the operator icon and the re-run captures it again. An app with no branding.favicon takes the if (branding?.favicon) false branch: faviconLink stays null, nothing is written and nothing is restored, so the operator icon is untouched. All three behaviours are pinned (cases 7 to 13 below).
  • (c) The pin is real and can fail: holds. faviconAfterNavigation.test.tsx imports FaviconSync from ../components/FaviconSync and AppShell from @object-ui/layout (which calls useAppShellBranding), under MemoryRouter; the icon link is parsed out of the shipped index.html and the shipped inline script is executed by name. vi.stubGlobal('fetch', ...) is the only stub; there is no vi.mock, and the root vitest.setup.dom.tsx the console config uses contains no mock or stub. Case list (13): 2 environment controls; entering; in-app nav; third nav; switching apps; leaving with operator favicon; leaving after two apps; leaving with no operator favicon; plain app throughout; branded to plain; sync-after-shell order; StrictMode. Arithmetic against the branch-point component (a [location]-keyed write, same as ablation leg 1): the route-keyed write fires on every navigation and, in the after-shell order, after the shell's write, so exactly four cases go red: in-app nav, third nav, sync-after-shell, and StrictMode (which navigates in-app); the other nine pass because the shell's write lands last on mount in tree order, the shell restores on unmount, and the no-operator and plain-app cases never see a differing write. That is 4 failed / 9 passed, as reported. For a mount-only write (leg 2) the only case where the write lands after the shell is the sync-after-shell order; under StrictMode the re-run write precedes the shell's re-capture, so it passes. That is 1 failed / 12 passed, the after-shell case, as reported. tabTitleAfterNavigation.test.tsx still imports and renders the real FaviconSync.
  • (d) Changeset truth: holds. .changeset/10379-favicon-one-writer.md declares '@object-ui/console': patch; every sentence is true at head, including "a deployment with no operator favicon is unaffected" (the removed write was guarded on a truthy URL). The corrected 8637 and 10040 bodies are true at head sentence by sentence against the hook and the boot code; 8637's "Both run on the commit that mounts the shell" is narrative of a component that no longer exists and was not made false by this PR. Frontmatter is byte-identical at the branch point and at head: sha256 of the block between the two --- lines is 63f435048e308e0b75a66ef417547268f5561914cb06b300b67539cfb1ce6f00 for 8637 and ee79b57f39608e3ffe12a65dd5c88bb58fe87c354d1af590166424ca35522d0d for 10040 at both shas, matching the dev's report. No line-address citation in any of the three changesets, the docblock or the new test. @object-ui/console is a released package by the gate's own definition: it is in the fixed group of .changeset/config.json, apps/console/package.json is named @object-ui/console with private unset, and scripts/check-changeset-presence.mjs sets PACKAGE_ROOTS = ['packages', 'apps'] and takes the flattened fixed group as the versioned set, so apps/console/src/** is guarded. AGENTS.md line 167 says the same in words. The other three pending changesets (4830-app-shell-branding-rightrail-prose, empty frontmatter, docs-only; console-boot-request-dedup-5544; console-preboot-branding-origin-5660) describe the hook's favicon read and the two boot writers, none names FaviconSync, and none is made false.
  • (e) The docblock: true at head. Both removed writes are recorded with their cards; the boot-writer paragraph is accurate (both writers set the bare product name and the operator favicon); the initRuntimeConfig await, the shell's capture and restore, the order-dependence argument against a mount-only write, and "renders nothing and runs no effect" are all verified. One nuance, not a falsehood: "the pin that goes red, in both render orders" is exact for a route-keyed write and covers both orders; a mount-only write goes red in the after-shell order only, which is what leg 2 showed. The pre-existing paragraph citing a real-browser reading on objectui#8637's pull request is unchanged text from the branch point and was not re-derived.
  • (f) The proposed body: every factual sentence checked is true at head. Verified: the defect mechanism; the singleton, caller and subscription claims; the two boot writers and sharedGetJson joining the inline script's request (the script registers its continuation first, and main.tsx also writes the icon itself before render(), so the order between the two boot writers is not load-bearing); the cd7b728b ancestry; StrictMode in main.tsx; the 13-case list; the layout suite packages/layout/src/__tests__/app-shell-branding-favicon-restore.test.tsx exists; the ablation script scripts/ablation-replace.mjs exists in objectstack; the lint population (console lint is eslint ., no console-local eslint config, root eslint.config.js sets no parserOptions.project or projectService, no non-test file in eslint-rules/ reads the filesystem); turbo type-check depends on ^build; the fixed-group reading; 6da5b45ee differs from 6e529c3eb only in the two changeset bodies; no test or script names either corrected changeset; the three inert leftovers (App.tsx mount, the vi.mock in App.uploadAltitude-10131.test.tsx, and the getFaviconUrl entries returning an empty string in the @object-ui/app-shell mocks of App.docsPortalLazy, App.uploadAltitude-10131 and internalFormShell); the docs Branding section names only the colour removals on unmount, so "incomplete but not false" is right. Not re-derived, by rule: the run counts (red 4 of 13, green 44 of 44, the ablation outputs, the lint JSON, the narrowed tsc diagnostic count, the real-browser table) and the 35-package closure count; their premises and arithmetic are consistent with the code. Identifier scan: no model-family token in the body; Fixes #10379 is its only closing reference (the objectui#NNNN spellings are not closing references); the only version-like tokens are one browser major-version number naming the probe browser in the evidence table and the API path prefix, neither a model identifier. Note that the body labels evidence by "code head 6e529c3eb" and the patch round by 6da5b45ee, which is accurate.
  • (g) The type and href pairing: verified at source, still cosmetic and unmeasured. useAppShellBranding writes link.href = branding.favicon and never type; both boot writers set type from the operator URL's extension. So with an operator .png and an app .svg, type="image/png" sits next to an .svg href from the moment the shell mounts, which predates this PR. Before the PR the first in-app navigation rewrote both attributes to the operator values (the defect itself), ending the pairing; after the PR it lasts the whole app visit. The HTML specification makes type on a link advisory, the shell's own 10040 changeset and pin speak of href only, and the dev's real-browser reading recorded hrefs, not rendering. Judgment: the PR extends the duration of a pre-existing state and does not introduce it; no user-visible regression is shown, and the correct icon URL is what the card asked for. A carrier-none note is acceptable for this card; if the seat wants it closed, the fix is one type write beside the href write in packages/layout, outside this claim's surface, as its own small card.

② Semver level

patch on @object-ui/console is right, and major is excluded by rule. AGENTS.md (objectui, at head) line 167: "只要改了发版包(.changeset/config.json 的 fixed 组,含 apps/console)的已发布可执行源码 —— src/ 下的任何文件 …… 就必须新增一个 .changeset/*.md". Lines 259 and 261: "minor/patch 独立演进——objectstack 没动时不必跟发;objectui 自己的改动照常用 changeset 推进" and "changeset 里不要声明 major …… objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可)". This change removes a console component's DOM write with no exported symbol, schema key or public prop moving (Clause-②: no holds: App.tsx still imports and mounts FaviconSync, whose signature is unchanged), so it is a bug fix below the minor line reserved for breaking semantics. The 8637 and 5660 changesets set the precedent of a console patch, and the Changeset Bump Policy and Changeset Declaration checks on this head are green.

③ Boundary flags

  • Closing keyword. Fixes #10379 is the only closing reference in the proposed body; the PR title and both commit messages carry none.
  • Model identifiers. None in the PR title, the proposed body, either commit message or any trailer. The trailers are the bare co-author line with the noreply address and the session-URL line, the model-free pair the dispatch prescribes, not the harness reminder's model-named form.
  • File surface (5 files, REST and git agree). Claim: apps/console/src/components/FaviconSync.tsx, apps/console/src/__tests__/faviconAfterNavigation.test.tsx, .changeset/10379-favicon-one-writer.md. Amendment (comment 5824829480, body only): .changeset/8637-tab-title-one-writer.md. Patch-round instruction, not named on the card: .changeset/10040-app-shell-favicon-restore.md, body only, frontmatter byte-identical. App.tsx and packages/layout are untouched, as answer A requires. Flag for the seat: the 10040 edit rests on the coordinator's patch-round message rather than on the claim or its amendment as written on the card, so the seat should record it on the card so the claimed surface matches the PR.
  • Live PR body is stale. The body on GitHub at review time is the round-0 text; its acceptance note "The older changeset was not edited because it is outside the file surface" is now false at head. The proposed body in proposed-body.md replaces that note correctly, so the seat must apply it before marking the draft ready.
  • Serial. 21 open PRs read by file list at review time; none touches any of the five surface files, FaviconSync, packages/layout/src/AppShell.tsx or either pin.
  • Mergeable state. mergeable: true; mergeable_state read behind at the first GET and clean at the final GET after the checks settled. The PR is a draft, by design.
  • Check runs on the head (polled in the foreground, settled after about two minutes; final read after the last poll). 43 check runs: 40 success, 3 skipped, 0 failure, 0 in_progress. The three non-success runs are all skipped: Test (coverage), Test (coverage shard ${{ matrix.shard }}/4) and dependabot, the coverage and dependabot jobs that do not run on this kind of PR. Type Check, Lint, Line Citation Gate, Changeset Declaration, Changeset Bump Policy, Changeset Claim Re-read, Changeset Overwrite Report, Changeset Fixed Group Check, Control Byte Scan, Inert vi.mock Specifier Check, Test, all eight Test (shard n/8), Test (dist pins), Build & E2E and Live E2E (informational) are success. CI Type Check therefore covers the console type-check the body left NOT MEASURED locally.

Implemented-by: claude/issue-10379-favicon-one-writer
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 01:18
@os-litant
os-litant added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 747f334 Sep 25, 2026
45 checks passed
@os-litant
os-litant deleted the claude/issue-10379-favicon-one-writer branch September 25, 2026 01:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants