Skip to content

fix(layout): useAppShellBranding restores the favicon it replaced (objectui#10040) - #10372

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-10040-branding-favicon-restore
Sep 24, 2026
Merged

os-litant merged 3 commits into
mainfrom
claude/issue-10040-branding-favicon-restore

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10040
Clause-②: no

What changed

useAppShellBranding (packages/layout, AppShell.tsx) now scopes its favicon write to the mount, the same way objectui#8637 scoped its document.title write:

  • when branding.favicon is set and the page has an icon link (#favicon, else link[rel="icon"]), the effect records the link element and its href attribute, then writes the branded URL exactly as before;
  • the same cleanup that restores the title puts that attribute back (setAttribute), or removes it when the link had none;
  • no favicon, or no icon link: nothing is written and nothing is restored. The hook still never creates an icon link.

Why the attribute and not link.href: the property is the URL resolved against the document, so the console's own icon link (href="" in apps/console/index.html until an operator favicon exists) reads back as the page's URL. Restoring through the property would point the icon at the page itself. Measured in happy-dom (the environment-control test) and by ablation leg 2 below.

Dispatch mechanism assumptions A1–A4, measured

  • A1 holds, with one correction. The change mirrors the title restore (capture in the effect, restore in the same cleanup). The "remove a link the effect created" branch does not apply: the effect never creates a link (it writes only when one exists, and content/docs/layout/app-shell.mdx says so), so there is nothing to remove; ablation leg 5 pins that it stays that way. The capture has to be the attribute, not the property (above).
  • A2 holds. Absent branding.favicon, the captured link stays null: no write, no restore (pin, and ablation leg 3).
  • A3: no fight; order measured. A scratch harness (never committed, deleted after the run) mounted the real FaviconSync from apps/console as the first sibling under a MemoryRouter (the console mounts it as the first child of BrowserRouter, ahead of the routes) and the real hook under /apps/:appName/*, with getFaviconUrl mocked, logging every write to the icon link. Order within one navigation commit: shell cleanup (restore), then FaviconSync's effect, then the shell's effect (capture + branded write).
    • No operator favicon. This branch: /apps/a shows A; /apps/b restores "" then writes B; /apps/plain ends ""; /home ends "". On the branch point 86982ace0: /apps/plain stays B and /home stays A, which is the card's defect reproduced at DOM level.
    • Operator favicon O configured. Every end state is identical on this branch and on 86982ace0: the restore puts back O, which is what FaviconSync writes next anyway.
    • Pre-existing and unchanged here: with an operator favicon configured, an in-app navigation (/apps/a to /apps/a/page-2) makes FaviconSync write O over the branded icon, on both trees. Reported to the seat as an out-of-scope finding, not addressed in this PR.
  • A4: pins written; red counts below.

Tests

New: packages/layout/src/__tests__/app-shell-branding-favicon-restore.test.tsx, beside the app-shell-branding-title-* pins: 9 tests (1 environment control, 8 pins).

Ablations, each a trap-armed mutation with a blob-hash restore proof. The tests import ../AppShell source directly, so no dist/ is on the resolution path.

leg mutation result
1 AppShell.tsx set to the branch point 86982ace0 5 failed, 4 passed
2 capture through the link.href property 5 failed, 4 passed
3 cleanup writes the icon even when the effect wrote none 1 failed (the no-favicon pin), 8 passed
4 forward write removed 5 failed, 4 passed
5 effect creates an icon link when the page has none 1 failed (the no-icon-link pin), 8 passed

Every pin is red in at least one leg. The two that are green on the branch point (write while mounted; no favicon means no write) pin existing behaviour and go red in legs 4 and 3. Every restore leg ended with the file's blob equal to the HEAD blob and git diff HEAD empty.

Local verification at d10a9ad9d

  • pnpm --workspace-concurrency=2 --filter "@object-ui/layout^..." build: exit 0
  • pnpm --filter @object-ui/layout type-check: exit 0; tsc -p tsconfig.test.json --listFiles includes the new test file
  • pnpm exec vitest run packages/layout/ apps/console/src/__tests__/tabTitleAfterNavigation.test.tsx: 25 files, 245 tests passed
  • check-changeset-presence, check-changeset-no-major, check-changeset-overwrite, check-changeset-claims, check-new-cross-file-line-citations (0 new), check-control-bytes, check-test-path-roots, check:doc-example-readers, check-vi-mock-specifiers, check-vi-mock-inherit, check-changeset-fixed: all exit 0; check-governed-queue-guard --test on the three paths: NOT GOVERNED
  • eslint on the two touched TS files (--no-inline-config --format json): 2 files, 0 errors, 1 warning (react-refresh/only-export-components on the useAppShellBranding export, present on the branch point too). Narrowing stated: the population is the root eslint.config.js block for **/*.{ts,tsx}; the resolved config has empty parserOptions (no type-aware linting) and no custom rule reads other files, so this diff cannot move an untouched file's verdict. The repo-wide pnpm lint is CI's.

Pending changesets that name AppShell.tsx / useAppShellBranding (the 4830, 6871, 6872 and 8637 ones) were re-read: none is made false by this diff.

Acceptance notes

  • Out of scope, reported to the seat and not filed: FaviconSync's route-keyed re-apply writes the operator favicon over a branded app favicon on every in-app navigation when both are configured; it is the favicon twin of the title race objectui#8637 removed.
  • content/docs/layout/app-shell.mdx says the colour properties are removed again when the shell unmounts, and does not mention that the title and (now) the favicon are restored too. The sentence stays true; the omission is noted, not addressed here (outside the claimed surface).
  • The hook sets only href; link.type, which the console's boot script and FaviconSync set by file extension, is left as it was. Observation only.

Generated by Claude Code

The effect already scopes its document.title write to the mount (captures
the previous title, restores it on cleanup). Its other branded write, the
icon link's href, had no restore, so a branded favicon outlived the shell
wherever nothing else re-applied one.

Capture the icon link element and its href ATTRIBUTE (not the resolved
property, which reads an empty href back as the page URL) when the effect
writes branding.favicon, and put it back in the same cleanup (removing the
attribute when there was none). No favicon, or no icon link, writes and
restores nothing.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
Beside the title-restore pins: the branded favicon is written while
mounted, the previous href attribute comes back on unmount, an A to B
switch restores the pre-A icon, no favicon means no write in either
direction, and the attribute bytes (empty, absent) are what is restored.

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) 3042.0 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-DUig0PBG.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) 541.72KB 129.49KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 222.47KB 61.83KB
fields (index.js) 253.70KB 64.09KB
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) 34.99KB 11.45KB
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.08KB 3.95KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 72.59KB 20.30KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.19KB 35.25KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 260.87KB 67.87KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.34KB 37.47KB
plugin-gantt (index.js) 168.31KB 41.45KB
plugin-grid (index.js) 215.22KB 58.85KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.17KB 28.16KB
plugin-map (index.js) 21.74KB 7.07KB
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.79KB 21.34KB
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: d10a9ad9d30edfa6ce9fe7ec49390df230a1fadb

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

① Derived judgments

  • (a) Restore exactly scoped — confirmed. In useAppShellBranding (packages/layout/src/AppShell.tsx at head) the capture previousFaviconHref = link.getAttribute('href') sits immediately before the write link.href = branding.favicon, both inside the same effect body, and the restore is in that effect's returned cleanup behind if (faviconLink). Dependency list: [branding?.primaryColor, branding?.accentColor, branding?.favicon, title]. For A→B, React runs A's cleanup (restores pre-A) before B's effect body runs, so B captures pre-A; the final unmount runs B's cleanup and lands on pre-A, not A. Pinned by "switching branding A → B shows B, and the final unmount restores the pre-A icon, not A". No breaking case found: the intermediate no-favicon shape (A → undefined → B, the console's loading state) also ends on pre-A (A's cleanup restores; the undefined run captures nothing; B captures pre-A). Observation only: because the effect also keys on the colour props and title, a title-only or colour-only change replays restore-then-branded-write back to back in one synchronous flush; the end state is unchanged and this mirrors the title's existing behaviour.
  • (b) Attribute-vs-property capture — confirmed correct. HTMLLinkElement.href reflects a URL attribute: the getter resolves the content attribute against the document base, so href="" (the console's own icon link in apps/console/index.html, until the boot script or main.tsx writes an operator favicon) reads back as the page URL, and an absent attribute reads back as "", collapsing two distinct states. getAttribute('href') returns the exact bytes or null. Cleanup: null → removeAttribute('href'); '' or any string → setAttribute('href', value). Both pinned ("an empty href … comes back empty, not as the page URL"; "an icon link with no href attribute is left with none"). The environment-control test measures that resolution in happy-dom (root vitest.config.mts, environment: 'happy-dom') before the pins rely on it. The forward write through the property sets the content attribute to the given string, so write and restore act on the same attribute.
  • (c) No favicon / no icon link — confirmed by reading. faviconLink and previousFaviconHref start null; both are assigned only inside if (branding?.favicon) then if (link); the hook contains no createElement; the cleanup's favicon branch is if (faviconLink). Pinned by "a shell with no favicon writes no icon and restores nothing" (which also plants a third-party write during the mount and checks it survives the unmount) and "a page with no icon link gets none — neither on mount nor on unmount".
  • (d) FaviconSync interaction — no fight found. Tree at head: apps/console/src/App.tsx mounts FaviconSync as the first child of BrowserRouter, ahead of the UploadProvider → ConsoleShell → Routes subtree; the only console consumer of the hook is packages/app-shell/src/layout/ConsoleLayout.tsx (layout AppShell with branding={activeApp?.branding …}) inside that later subtree. Within one commit React (the repo's pinned release, via the root pnpm.overrides) runs every passive cleanup before any passive create, whatever the sibling or child placement, so the shell's restore cannot land after a FaviconSync write made in the same commit; among creates, FaviconSync's fires first (earlier, shallower sibling) and the shell's after — the order the dev measured. Across commits (a lazily loaded target route suspending so the old shell's unmount cleanup lands a commit later than FaviconSync's location-keyed write): with an operator favicon O configured, every pre-shell writer (the index.html boot script, main.tsx before createRoot().render(), and FaviconSync's own first effect) writes O, so the captured pre-A value is O and a late restore writes the same URL FaviconSync just wrote; with no operator favicon, FaviconSync never writes (if (faviconUrl) gate) and the restore is the only writer. A child placement would only make FaviconSync unmount with the shell, leaving the restore alone. The one divergence is the declared replay caveat (a third-party write while mounted is overwritten on unmount), identical to the title's. The pre-existing in-app-navigation overwrite (FaviconSync writing O over a branded icon on /apps/a → /apps/a/page-2) is real on both trees and is correctly reported as out of scope.
  • (e) Pins real and able to fail — consistent. 9 tests (1 environment control + 8 pins) importing ../AppShell source. Re-deriving the ablation table against the assertions: leg 1 (branch point, no restore) reddens the 5 unmount-restore pins (sentinel, A→B, empty href, absent href, rel-only) and leaves the write pin, no-favicon pin, no-link pin and the control green → 5 failed / 4 passed as reported; leg 2 (property capture) reddens the same 5 (relative sentinel and '' come back resolved, absent comes back as '') → 5 / 4; leg 3 (cleanup writes when nothing was written) reddens only the no-favicon pin → 1 / 8; leg 4 (forward write removed) reddens exactly the 5 tests that assert the branded value while mounted (write pin, A→B, empty, absent, rel-only) and leaves the sentinel-restore pin green because it never asserts the branded state → 5 / 4; leg 5 (create a link) reddens only the no-link pin → 1 / 8. Each of the 8 pins is red in at least one leg; the environment control is red in none, by design.
  • (f) Changeset — every sentence true at head. The title/favicon asymmetry on the branch point, the FaviconSync gate, the #favicon then link[rel="icon"] lookup, the attribute capture and href="" reasoning, absent-attribute handling, the no-favicon / no-link no-op, no link creation, and the unconditional replay caveat all match the code as read. No line-address citations. One precision note, not a falsehood: "makes two branded writes to the document" counts the two tab-surface writes (title, icon href); the same effect also writes brand colour custom properties on the document root, which it removes rather than restores on unmount (as content/docs/layout/app-shell.mdx says). The sentence's count is scoped to the surfaces it enumerates.

② Semver level

'@object-ui/layout': patch — appropriate. objectui AGENTS.md, "版本号策略": "minor/patch 独立演进——objectstack 没动时不必跟发;objectui 自己的改动照常用 changeset 推进" and "changeset 里不要声明 major —— fixed 组任一 major 都会把全组推上去、脱离 objectstack 的节奏 … objectui 自身的破坏性变更也标 minor". This is a behaviour repair inside one hook's cleanup with no export, prop, schema or key change, so patch is the right level; it is not major, and the dev's check-changeset-no-major exit 0 agrees. Package name verified against packages/layout/package.json (@object-ui/layout, in the single fixed group of .changeset/config.json, so the group bumps together as usual).

③ Boundary flags

  • Fixes #10040 — appropriate. The card said it was "not a claim that the favicon should be restored"; the seat's claim comment 5820997518 answered that as an invariant restore ("the branded favicon becomes scoped to the mount like the branded title already is … restores an invariant the effect already holds for its other write"), so the PR resolves the card as claimed. Noted: the product question was decided by the seat, not by the card.
  • Closing keywords: only Fixes #10040 in the PR body. objectui#8637 appears in prose only (PR body, code comments, changeset) with no closing keyword; the three commit messages carry no issue references at all. objectui#8637 will not be closed by keyword.
  • Model identifiers: scanned the PR title, PR body and all three commit messages including trailers; none found. The trailers present are a Co-authored-by line and a session-URL line, neither of which is a model identifier.
  • File surface vs claim — matches, no breach. Exactly 3 files: packages/layout/src/AppShell.tsx (+30 / −1, all within useAppShellBranding's JSDoc, effect body and cleanup), packages/layout/src/__tests__/app-shell-branding-favicon-restore.test.tsx (new, beside app-shell-branding-title-assignment, -title-restore, -title-surfaces), .changeset/10040-app-shell-favicon-restore.md (new).
  • Pending changesets naming AppShell.tsx / useAppShellBranding re-read at head — none made false. 4830-app-shell-branding-rightrail-prose (the four fields the hook reads — unchanged), 6871-branding-token-docs (says its own diff does not touch AppShell.tsx — still true of that change), 6872-app-shell-branding-title-jsdoc (wholesale title assignment — unchanged), 8637-tab-title-one-writer (title ownership and restore; FaviconSync "syncs only the favicon" — unchanged). 5867, 9119 and app-shell-page-header-subtitle-4761 mention AppShell in unrelated contexts and are unaffected.
  • Docs: content/docs/layout/app-shell.mdx still says favicon "never creates one" (true) and that colour properties are removed on unmount without mentioning the title/favicon restore — an omission, not a falsehood; outside the claimed surface and correctly noted by the dev.
  • Check-runs on d10a9ad9d30edfa6ce9fe7ec49390df230a1fadb: polled every 60s; all completed at the fifth poll (2026-09-24T20:44Z). 43 check-runs: 40 success, 3 skipped, 0 failure, 0 in_progress. The non-success runs are all skips: Test (coverage), dependabot, Test (coverage shard ${{ matrix.shard }}/4). Every Test (shard N/8), Lint, Type Check, Build Docs, Bundle Analysis and Spec Main Shape Gate run concluded success. (The dev's report saw 42 runs with 19 in progress; one run was added after and also succeeded.)

Implemented-by: claude/issue-10040-branding-favicon-restore
Reviewed-by: session_01BP8CMtACxTdLjqR6rhd33C

VERDICT: PASS


Generated by Claude Code

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

Projects

None yet

2 participants