refactor(desktop): move App Update controller below AppShell - #4498
Conversation
628a5ec to
d5cd6e9
Compare
|
@Astro-Han could you please review exact head I rebased it onto Exact-head local verification passes: rebuild; renderer architecture 71/71 plus the base ratchet; App Update 22/22; Desktop 1,982/1,982; UI 313/313; typecheck, lint, format, Desktop/UI Knip, ASF headers, AppShell hook scope, Astryx inventory; and the real Electron window smoke. The new hosted CI and Windows package checks are currently queued. |
d5cd6e9 to
076fb61
Compare
|
Rebased onto current How the two real conflicts were resolved:
Local verification on this head with Node 24: desktop Automated update from Claude Code on behalf of the PR author. |
076fb61 to
750ee05
Compare
|
Rebased again onto current Same local verification on this head with Node 24: desktop Automated update from Claude Code on behalf of the PR author. |
750ee05 to
8cc5de4
Compare
|
Rebased onto Posted by Claude Code on behalf of the PR author. |
8cc5de4 to
8a30f75
Compare
|
Rebased once more onto Posted by Claude Code on behalf of the PR author. |
… namespaces through (#4590) Nine renderer feature slices each restated the same twenty-line services context, and their Desktop adapters hand-wrote forwarders whose signatures equal the bridge methods they forward to. Every new slice paid that join cost again, and the copies could drift apart without any test noticing. `application/contracts/feature-services.tsx` now exports `createServicesContext<S>(providerName)`, which returns the Provider and hook pair. Each slice's `services-context.tsx` shrinks to a few lines while keeping every exported name, type, and error message. Desktop adapters hand a bridge namespace through where the port is a structural subset of it (`sessions`, `newTasks`, `scheduledTasks`, `shellRuns`, `todo`, `attachments`), removing 33 identity forwarders. Adapter blocks that rename, guard, filter, or translate stay hand-written. Preload namespaces are plain objects with no `this` usage, so passing one through is runtime-safe, and the port types stay narrow, so nothing new is reachable from feature code. `composition/desktop-feature-services.tsx` is untouched; the tenth slice from #4498 can adopt the factory after it lands. No behavior change. Refs #4582 Generated-by: Claude Code
… namespaces through (apache#4590) Nine renderer feature slices each restated the same twenty-line services context, and their Desktop adapters hand-wrote forwarders whose signatures equal the bridge methods they forward to. Every new slice paid that join cost again, and the copies could drift apart without any test noticing. `application/contracts/feature-services.tsx` now exports `createServicesContext<S>(providerName)`, which returns the Provider and hook pair. Each slice's `services-context.tsx` shrinks to a few lines while keeping every exported name, type, and error message. Desktop adapters hand a bridge namespace through where the port is a structural subset of it (`sessions`, `newTasks`, `scheduledTasks`, `shellRuns`, `todo`, `attachments`), removing 33 identity forwarders. Adapter blocks that rename, guard, filter, or translate stay hand-written. Preload namespaces are plain objects with no `this` usage, so passing one through is runtime-safe, and the port types stay narrow, so nothing new is reachable from feature code. `composition/desktop-feature-services.tsx` is untouched; the tenth slice from apache#4498 can adopt the factory after it lands. No behavior change. Refs apache#4582 Generated-by: Claude Code Generated-by: GLM-5.3-Flash (ZCode)
8a30f75 to
a54ef20
Compare
|
Rebased onto
Exact head: |
a54ef20 to
08c27ef
Compare
|
Rebased onto |
08c27ef to
96a84dc
Compare
|
The run on |
Astro-Han
left a comment
There was a problem hiding this comment.
Reviewed at 96a84dc. All four done-conditions from #4582 hold: the two effects, one state and two refs leave the shell render body (check-app-shell-hooks.mjs 14→12 / 15→14), the four window.maka.* update paths leave app-shell.tsx and the three in about-settings-page.tsx go with them, useAppUpdateController → AppUpdateProvider is in controllerOwners, and the same PR deletes app-shell-app-update.ts, the nine shell-copy keys, the updateReminder / onOpenUpdate threading through SessionRailChrome, and the second copy of AppUpdateStatus in preload and main. The hand-removed ownership entry is the one the checker would otherwise reject (check-renderer-architecture.mjs:2346), so the ledger edit is consistent with regeneration.
The case for the move is stronger than the earlier slices: main had two subscribers to the same status (AppShellContent and About), and the shell one fed download progress into a useState on the renderer root, which is what the 512 → 1 fibers number is measuring. It also fixes a real ordering bug on the About page: the old effect read before it subscribed with no fence, so a slow updateStatus() could overwrite a newer push. The revision counter and the subscribe-then-read test pin that. I compared the nine copy keys across zh-CN / zh-TW / en against the deleted shell-copy.ts entries: identical.
No P0–P2. Five small things, none blocking, take what you want in one pass:
app-update-projection-context.tsgives the About projection a silent default (inactiveAboutProjection), wheremodule-hub-provider.tsx:118throwsProvider is missing. A stray About mount outside the Provider then sits on "not checked yet" forever and the button toasts. Make the desktop context nullable and throw on read; thepackages/uione can keep its default since it explicitly serves other hosts.- Three cases in
app-update-boundary.test.tsadd no constraint: exclusive owner and index/testing-only entry are whatcheck-renderer-architecture.mjs:1865and:2087already enforce, and the "recognizes alias / namespace / re-export / dynamic import" case tests the helper against inline strings. Keep the cross-package sidebar binding, the raw-capability ban and the mount-point checks; drop those three (~70 lines). Same point we settled on #4315. isDevBuildis dead inabout-status-detail.ts(its own comment says there is no dev-build branch) and this PR threads it through a newAboutUpdateRowprop. Delete the option, the prop and the call site.localizedAppUpdateErrorMessageinapp-update-copy.tsonly forwards togeneralizedErrorMessageForLocale, the same shell it copies fromshell-copy.ts. Call the original.create-app-update-services.tshands the feature the wholebridge.appnamespace (icons,sessionProjectInfo,infoincluded) and the adapter test asserts identity on it.create-goal-services.tscloses over the methods it needs; do the same here and drop the===assertion.
Two things before merge. The branch went CONFLICTING a few minutes ago because #4640 and #4823 both landed on renderer-architecture.json; please regenerate rather than hand-merge, and replay the ownership deletion. And test / package were still pending on this head when I read it; the approval stands across the rebase per CONTRIBUTING, but the exact head that merges needs green CI, including the session-workbar.spec.ts line that was red on main.
#4491 shares the ledger, app-shell.tsx, the hook gate and the Astryx inventory with this PR, so whichever lands second regenerates once; I would land this one first since it deletes more.
Manual acceptance still owed on a packaged build, both themes: About page across the eight update states with the percentage moving during downloading; the check button disabling and recovering, including closing and reopening Settings mid-check (the one semantic shift, since checking moved from the page to the Provider); the dev checkout still rendering no status row; the sidebar footer button appearing only for downloaded and versioned error; the install-failure toast and the running-tasks confirm, three locales.
Evidence boundary: static read at 96a84dc against main; no tests, build, Storybook or app run, so the fiber counts and suite results are the body's, not mine.
AI-assisted review: drafted with Maka; I verified the four done-conditions, the copy parity, the ownership edit and the checker overlap myself.
96a84dc to
17ed533
Compare
|
Rebased onto
Exact head: The packaged-build manual acceptance you listed is still owed and is on the author. |
17ed533 to
bb04383
Compare
|
@Astro-Han this needed a real re-merge, so please re-look at the About page before merging: your #4857 rebuilt
Exact head: Posted by Claude Code on behalf of the PR author. |
bb04383 to
1ea93ea
Compare
|
#4895 landed, so this is rebased once more onto Posted by Claude Code on behalf of the PR author. |
Move the renderer-owned App Update lifecycle out of AppShell into a feature slice with AppUpdateProvider as the sole production owner of useAppUpdateController, registered with the controllerOwners guard. About and the Sidebar footer read independent projections, the update contract is shared type-only, and the review follow-ups are folded in: a nullable About projection context, the adapter closing over the five capabilities its port names, and the boundary cases the architecture checker already proves removed.
1ea93ea to
4a7b99e
Compare
|
@Astro-Han #4491 landing first put this branch into a real conflict, so one more look is needed. Rebased onto
Everything else merged cleanly and is byte-identical to the head you approved plus the #4857 re-merge described above. Local verification with Node 24 is fully green (desktop Posted by Claude Code on behalf of the PR author. |
Astro-Han
left a comment
There was a problem hiding this comment.
Re-reviewed at 1ea93ea against main (90014db94), clean merge, CI green. Thanks for calling out the About re-merge instead of burying it.
Interdiff from the head I approved (96a84dc): the five follow-ups are all in (ui/app-update-projection-context.ts:31-45 nullable context and the throwing Consumer; the three boundary cases dropped; model/about-status-detail.ts deleted; localizedAppUpdateErrorMessage gone in favour of generalizedErrorMessageForLocale at app-update-provider.tsx:111/129; create-app-update-services.ts:35-41 closing over the five capabilities), plus the About re-merge. Everything else is byte-identical.
About page: #4857's page verbatim with only update ownership moved. aboutUpdateRow is fed the same errorDetail localisation (:78), toast semantics unchanged (:86-95), isLoading keeps the same meaning with checking now held by the controller (:110), the dev-checkout branch and the button-only-where-a-check-is-honoured rule are copied from main (:211, :103), and the page has no window.maka.app.* update path left. The three deleted boundary tests are covered by the ledger registration (renderer-architecture.json:294-297 with check-renderer-architecture.mjs:1851-1885) and the feature-entry rule at check-renderer-architecture.mjs:2102-2112, so nothing is lost. Stories: the Provider sits inside SettingsStory's ToastProvider, settingsAppUpdateServices reads window.maka.app per call for the reason its comment gives, and the three About stories are untouched.
Two P3s, not blocking:
about-settings-page.tsx:65-71: the first five lines of theAboutUpdateStatusRowdoc comment argue for the change (who owns the subscription, why not the bridge); that belongs in the PR body. The last sentence, that a component is used because the action guard is a hook, is the one the next editor needs.AppUpdateAboutProjectionConsumer's throw when the Provider is missing has no test;app-update-provider-scope.test.tsonly covers the mounted path. Oneassert.throwsrender case.
Manual acceptance before merge, as in the earlier approval: packaged build, the eight update states on the About row, the check button disabling and recovering, dev checkout showing no status row, the sidebar reminder only in downloaded / versioned-error, install-failure toast and the running-task confirmation, in all three locales. I will run that on this head.
AI-assisted review: drafted with Maka; I verified the interdiff, the About page against main and the replacement coverage for the deleted tests myself.
|
Both P3s taken in one follow-up commit on top of
No runtime code changed, so a packaged build from Posted by Claude Code on behalf of the PR author. |
|
Correction to the comment above: the merge went in at Posted by Claude Code on behalf of the PR author. |
Three loose ends from the envelope work:
The UDS test still asserted the pre-envelope return shape of
sessions:setPermissionMode. Assert the committed envelope
(ok:true + session.permissionMode/revision) instead, narrowing through
guards so the check stays runtime-true rather than cast.
abandonPlanProposal re-serialized the {ok:false, error:{code}} envelope
into a plan_control: token and rethrew a plain Error. Nothing parses that
token; the code died at the preload boundary, sessionSettingFailureCopy
saw an unclassified error, and every expected block was also logged as an
unexpected operation. Throw ExpectedOperationError(result.error.code) so
the Host's code survives to updateFailures copy.
The renderer architecture ledger still recorded the app-update files
main's apache#4498 moved into features/, and the two controller-owner rows the
ratchet marks as historical were dropped by the rebase; regenerate the
ledger from main so only this PR's real delta (work-board and workhub
copy catalogs, the operation-diagnostics import edge) is recorded.
Generated-by: Claude Code
Summary
Move the renderer-owned App Update lifecycle out of AppShell into a feature slice, with
AppUpdateProvideras the sole production owner ofuseAppUpdateController.AppUpdateAboutProjectionConsumerand issues the feature's guarded check instead of touching the bridge; the row's copy comes frommain'saboutUpdateRowNo user-visible copy, update protocol, or install behavior is intended to change.
Refs #4582 (supersedes #3439)
Performance evidence
Measured in Electron 43.4.1 with a production React bundle, the same renderer PID, and alternating legacy/scoped runs:
The structural run used 8 rounds x 40 pushes, the pure renderer run 9 alternating rounds x 1,000 pushes, and the full propagation run 7 alternating rounds x 200 pushes. The full path used a real
BrowserWindow, Electron IPC, the production preload bundle, Desktop adapter, and App Update Provider; only the main status producer was a fixture.These measurements cover controlled renderer update propagation. They do not claim faster updater downloads or installs, lower main-process/network/disk cost, total wall/FPS improvement, or an exact production AppShell fiber count.
Verification
npm run rebuild,npm run typecheck,npm run lint, andnpm run format:checkapps/desktopandpackages/ui, ASF headers, stale-dist, andgit diff --checkNODE_NO_WARNINGS=1, Storage passed 1,077 tests with 8 skippednode scripts/desktop-real-window-smoke.mjs --programmatic-only --diagnostic-wait-ms 20000passed against the built renderer: visible 1280x840 movable/resizable window, renderer ready,appFramemounted, Search focused, dock visible, and no ErrorBoundaryThe real-window smoke used a source-built Electron app, not a packaged or signed installer; packaged updater download/install remains outside this PR's evidence.
Review focus
Please review the ownership guard and the two independent projections together: the feature should retain the only update subscription and bridge capability, while About and Sidebar Footer receive only the state needed at their render boundaries.
AI use
Select exactly one:
Tool(s) and scope: OpenAI Codex contributed substantive architecture design, implementation, tests, performance measurement, and verification. The human contributor reviewed the work and chose to submit it.
Checklist
Does this PR entail a change in behavior?