refactor(desktop): move Goal controller ownership below AppShell - #4316

Merged
Astro-Han merged 1 commit into
apache:mainfrom
chihumyum:refactor/goals-controller-scope
Sep 1, 2026
Merged

refactor(desktop): move Goal controller ownership below AppShell#4316
Astro-Han merged 1 commit into
apache:mainfrom
chihumyum:refactor/goals-controller-scope

Conversation

@chihumyum

@chihumyumchihumyum commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Move production Goal controller ownership out of AppShell and into GoalProvider.

  • Publish separate composer, indicator, and dialog projections at their actual reader boundaries.
  • Tie the projection transport to the authoritative Composer / ChatView prop contracts and perform explicit, compile-checked handoff at the two authorized Desktop leaves.
  • Keep WorkHub and Workbar isolated: their Composer / ChatView instances do not consume the Session Goal projections.
  • Remove Goal controller/model reads and Goal prop plumbing from AppShell; keep useGoalController out of the production barrel.
  • Prevent ownership return with unique-controller, unique-production-consumer, forbidden-hook, leaf-Omit, AppShell Hook-budget, and renderer-architecture guards.
  • Rebase onto 920d7142d and regenerate the Astryx 0.5.2 surface inventory while resolving the documentation conflict.

This intentionally changes one transient UI state: while the selected Session is not yet owner-backed by the hydrated catalog, the unusable Set Goal... entry stays hidden instead of briefly appearing and doing nothing. Goal business behavior otherwise remains in use-goal-controller.ts unchanged. Stabilizing the producer of reportError is left to #4315, where the duplication is visible.

Performance evidence

Measured on final head 3b47b2c35 against 920d7142d in a 12-Session fixture. Each workload ran legacy/scoped ownership as alternating pairs inside the same Electron process; root-mode changes and resynchronization stayed outside the measurement window. The controller-only workload used 6 pairs and the complete goal.pause workload used 12. Renderer profiling sampled at 100 microseconds; the Fiber probe was disabled during each busy-JS window. Values below are conventional medians.

WorkloadLegacy ownerScoped ownerChangePaired result
Controller-only token update: rendered fibers62949-92.2%6/6 improved
Controller-only token update: renderer busy JS5.016 ms1.061 ms-78.9%6/6 improved
Controller-only token update: commits11unchanged6/6 tied
Complete goal.pause: renderer busy JS24.879 ms23.637 ms-5.0%9/12 improved
Complete goal.pause: summed rendered fibers3,053.53,063+0.3%6/12 improved
Complete goal.pause: commits6.57+7.7%2/12 improved, 4 tied

The controller-only result is strong and stable: both ranges are disjoint (629-629 vs 49-49 fibers; 4.199-5.633 vs 0.769-1.363 ms), and every pair improved.

The complete pause workload covers preload -> IPC -> Host -> broadcast -> get -> controller -> React. Its busy-JS ranges overlap (21.848-29.949 vs 20.729-26.598 ms), while commits and fibers did not improve. The performance claim is therefore limited to controller-owned renderer work. The complete-path result is directional only and does not establish an end-to-end pause speedup; renderer busy JS also excludes Electron main-process CPU and I/O.

The temporary dual-owner switch, probes, performance spec, results, and worktrees were removed after measurement and are not part of this PR.

Verification

  • npm run rebuild - passed, including the production renderer build.
  • npm --workspace @maka/desktop run test:dist - 1,845/1,845 passed.
  • npm exec -w @maka/desktop -- playwright test --config e2e/playwright.config.ts e2e/goal-dialog-budget.spec.ts --workers=1 --reporter=line - 1/1 passed.
  • npm run typecheck - passed.
  • npm run lint and npm run format:check - passed (3,055 linted / 1,833 formatted files).
  • npx knip --workspace @maka/desktop --no-progress and npx knip --workspace packages/ui --no-progress - passed.
  • npm run check:app-shell-hooks - passed at 42 Hooks / 80 call sites.
  • npm run check:renderer-architecture -- --base 920d7142d001e81868bcd50a28a5bb3898415e3b - 61/61 fixtures passed.
  • npm run astryx:surface-inventory - passed at 235 files / 1 exclusion.
  • NODE_NO_WARNINGS=1 npm --workspace @maka/storage run test:dist - 1,077 passed / 8 skipped.
  • git range-diff, git diff --check upstream/main...HEAD, git merge-base --is-ancestor upstream/main HEAD, and git merge-tree --write-tree --messages upstream/main HEAD - passed.

Screenshots are not applicable; the only visible change removes a transient unusable menu entry during catalog hydration.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex - renderer architecture, implementation, tests, performance measurement, review handling, conflict resolution, and documentation.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes - described under Summary above
  • No

@github-actionsgithub-actionsBot added the effort/L Under 1000 readable lines label Aug 31, 2026
@chihumyum

Copy link
Copy Markdown
ContributorAuthor

@Astro-Han Hi, can you check this out? Thanks a lot

@chihumyum
chihumyumforce-pushed the refactor/goals-controller-scope branch from 2727ac3 to 25f20ecCompareSeptember 1, 2026 05:58

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this lands cleanly, and the performance section is the most disciplined I've read here in a while. It reports that the complete goal.pause path did not improve (overlapping busy-JS ranges, commits and fibers slightly worse) and explicitly limits the claim to controller-owned render scope. That honesty is what makes the controller-only result — 629 → 63 fibers, 6.6ms → 1.6ms busy JS, disjoint ranges, 6/6 paired — actually persuasive.

The design is right too. Three separate contexts rather than one is exactly what earns the reduction: a composer-only change must not wake the indicator. And passing children through untouched so React bails on the unrelated frame is the correct way to scope this without widening Goal state back to the root.

Reviewed at head 25f20ec6. Approving. One P2 I'd like fixed here, two P3s that don't need to be.

P2 — the cloneElement injection gives up a compile-time contract, and this PR is defining a pattern that will be copied.

GoalComposerTargetProps and GoalIndicatorTargetProps declare every injected prop optional, so ReactElement<GoalComposerTargetProps> is satisfied by any element — the compiler cannot tell that the child is the one that actually reads these props. onSetGoal originates in packages/ui/src/composer.tsx:449 and reaches the child through ChatComposerRegionProps extends Omit<ComponentProps<typeof Composer>, …>, so a rename or removal on that cross-package contract would silently turn the injection into a dead prop with no type error. Before this change, onSetGoal={…} at the JSX site was checked.

Today the hole is covered by goal-dialog-budget.spec.ts — the 设定 Goal… menu item renders only when props.onSetGoal is truthy (composer.tsx:1916), so a broken injection fails a real end-to-end path. That is genuinely good coverage and it's why this isn't blocking. But a compile error is cheaper than an Electron run, and #4315 is already reproducing this provider-plus-boundary shape; every further controller that moves below AppShell will copy it. Cheapest fix: require the injected keys in the target-props types, or type the child as ReactElement<ComponentProps<typeof ChatComposerRegion>>.

P3-1 — the composer's enablement gate changed from activeId to ownerActiveId. Old: activeId && localInteractionAvailable. New: canOpenDialog && activeSessionId, where activeSessionId is ownerActiveId = activeCatalogSession && !sharedSessionActive ? activeId : undefined (app-shell.tsx:367). When activeId is set but the session isn't in the catalog yet, the old code rendered an enabled 设定 Goal… whose openDialog returns early on an empty activeSessionId (use-goal-controller.ts:216); the new gate hides the entry instead. The new behavior is better — a dead menu item is worse than none — but the PR says "No intended user-visible behavior change". No code change needed; I'll say so in the squash message unless it was unintended, in which case restoring activeId is the fix.

P3-2 — the provider re-stabilizes reportError because the producer is unstable.showSessionError is a plain function declaration inside AppShellContent (app-shell.tsx:2620), so it gets a fresh identity every render, which is why GoalProvider needs the reportErrorRef + useLayoutEffect + useCallback trio. That's correct as written, but the workaround sits on the wrong side of the boundary, and #4315's provider repeats the same stabilization. If showSessionError's dependencies allow a useCallback, doing it once would let both providers take the prop directly. Out of scope here — worth settling on #4315, where the duplication becomes visible.

AI use: reviewed with Claude Code (diff reading, tracing the gate and prop-injection paths through AppShell, the goals controller, and the shared Composer contract). I verified ownerActiveId's definition, openDialog's early return, showSessionError's declaration form, and the E2E path that exercises the composer injection myself against the head SHA.

Move production Goal controller ownership into GoalProvider and expose reader-local projections for the composer, indicator, and dialog host.
Keep AppShell free of Goal controller and model ownership, route composer and indicator state through separate UI-owned contexts consumed only by the authorized Desktop leaves, bind them at the authoritative Composer and ChatView props, and lock the boundary with architecture and render-scope coverage.
The Set Goal entry now waits for an owner-backed active session instead of briefly exposing an unusable action before catalog hydration.
Generated-by: Codex
@chihumyum
chihumyumforce-pushed the refactor/goals-controller-scope branch from 25f20ec to 3b47b2cCompareSeptember 1, 2026 09:14
@chihumyum

Copy link
Copy Markdown
ContributorAuthor

Addressed the review feedback in 3b47b2c35:

  • P2 fixed: removed the structurally loose cloneElement injection. The two required projection transport shapes now derive from the authoritative Composer / ChatView prop contracts, and the authorized Desktop leaves hand the values to the real JSX props explicitly. Production-consumer uniqueness, hook absence, leaf Omits, and AppShell exclusion are guarded so WorkHub/Workbar cannot inherit the Session Goal later.
  • P3-1 documented as intentional: the unusable Set Goal... entry remains hidden until the selected Session is owner-backed after catalog hydration. The commit message, Summary, screenshot note, and behavior checkbox now say so.
  • P3-2 left to refactor(desktop): move Module Hub controller below AppShell #4315: producer-side reportError stabilization is unchanged here.

The branch is rebased onto 920d7142d; the Astryx inventory conflict was regenerated against 0.5.2 and passes at 235 files / 1 exclusion.

Final-head performance remeasurement preserves the bounded claim: controller-only rendered fibers 629 -> 49 (-92.2%) and renderer busy JS 5.016 -> 1.061 ms (-78.9%), both 6/6 paired wins with disjoint ranges. Full goal.pause busy JS was directional only (24.879 -> 23.637 ms, -5.0%, 9/12, overlapping ranges), while commits/fibers did not improve, so this is not presented as an end-to-end speedup.

Local final-head verification: rebuild/typecheck/lint/format/Knip passed; Desktop 1,845/1,845; storage 1,077 passed / 8 skipped; Goal E2E 1/1; renderer architecture 61/61; AppShell guard 42 Hooks / 80 call sites; clean merge-tree. Hosted CI is rerunning on the new head.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-verified on head 3b47b2c35. The cloneElement injection is gone and both projection shapes derive from the Composer / ChatView prop contracts, so the P2 is closed.

@Astro-Han
Astro-Han merged commit a5ede6b into apache:mainSep 1, 2026
3 checks passed
@chihumyum
chihumyum deleted the refactor/goals-controller-scope branch September 1, 2026 10:48
abhinav-phi pushed a commit to abhinav-phi/maka that referenced this pull request Sep 1, 2026
…che#4316)
AppShell owned the Goal controller, so every Goal state change re-rendered the
shell and, through it, the whole chat surface. The controller's own token update
re-rendered 629 fibers to repaint an indicator.
Move production ownership into `GoalProvider` and publish the composer,
indicator, and dialog projections separately, each at the boundary that actually
reads it. The two authorized Desktop leaves take their projection from a context
consumer and hand it to the real `Composer` / `ChatView` props, so the render
scope stops at the subtree instead of the shell. The transport shapes are
indexed off those components' own prop contracts, which makes the handoff
compile-checked rather than structural, and the leaf `Omit`s remove the props
from what the parent may pass at all. WorkHub and Workbar mount their own
`Composer` / `ChatView` without the Session Goal projections and are unaffected.
Goal business behavior stays in `use-goal-controller.ts`. AppShell loses its Goal
controller and model reads along with the Goal prop plumbing, and
`useGoalController` stays out of the production barrel. Unique-controller,
unique-production-consumer, forbidden-hook, leaf-`Omit`, AppShell Hook-budget,
and renderer-architecture guards keep ownership from returning.
Controller-owned renderer work improves decisively: rendered fibers 629 -> 49
(-92.2%) and renderer busy JS 5.016 -> 1.061 ms (-78.9%), every pair improved
with disjoint ranges. The complete `goal.pause` path is directional only
(-5.0% busy JS, overlapping ranges, commits and fibers flat), so this is not an
end-to-end pause speedup.
One transient UI state changes: while the selected Session is not yet
owner-backed by the hydrated catalog, the unusable "Set Goal..." entry stays
hidden instead of briefly appearing and doing nothing.
Stabilizing the producer of `reportError` is left to apache#4315, where the
duplication is visible.
Generated-by: Codex
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/LUnder 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@chihumyum@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

refactor(desktop): move Goal controller ownership below AppShell - #4316

Merged
Astro-Han merged 1 commit into
apache:mainfrom
chihumyum:refactor/goals-controller-scope
Sep 1, 2026
Merged

refactor(desktop): move Goal controller ownership below AppShell#4316
Astro-Han merged 1 commit into
apache:mainfrom
chihumyum:refactor/goals-controller-scope

Conversation

@chihumyum

@chihumyumchihumyum commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Move production Goal controller ownership out of AppShell and into GoalProvider.

  • Publish separate composer, indicator, and dialog projections at their actual reader boundaries.
  • Tie the projection transport to the authoritative Composer / ChatView prop contracts and perform explicit, compile-checked handoff at the two authorized Desktop leaves.
  • Keep WorkHub and Workbar isolated: their Composer / ChatView instances do not consume the Session Goal projections.
  • Remove Goal controller/model reads and Goal prop plumbing from AppShell; keep useGoalController out of the production barrel.
  • Prevent ownership return with unique-controller, unique-production-consumer, forbidden-hook, leaf-Omit, AppShell Hook-budget, and renderer-architecture guards.
  • Rebase onto 920d7142d and regenerate the Astryx 0.5.2 surface inventory while resolving the documentation conflict.

This intentionally changes one transient UI state: while the selected Session is not yet owner-backed by the hydrated catalog, the unusable Set Goal... entry stays hidden instead of briefly appearing and doing nothing. Goal business behavior otherwise remains in use-goal-controller.ts unchanged. Stabilizing the producer of reportError is left to #4315, where the duplication is visible.

Performance evidence

Measured on final head 3b47b2c35 against 920d7142d in a 12-Session fixture. Each workload ran legacy/scoped ownership as alternating pairs inside the same Electron process; root-mode changes and resynchronization stayed outside the measurement window. The controller-only workload used 6 pairs and the complete goal.pause workload used 12. Renderer profiling sampled at 100 microseconds; the Fiber probe was disabled during each busy-JS window. Values below are conventional medians.

WorkloadLegacy ownerScoped ownerChangePaired result
Controller-only token update: rendered fibers62949-92.2%6/6 improved
Controller-only token update: renderer busy JS5.016 ms1.061 ms-78.9%6/6 improved
Controller-only token update: commits11unchanged6/6 tied
Complete goal.pause: renderer busy JS24.879 ms23.637 ms-5.0%9/12 improved
Complete goal.pause: summed rendered fibers3,053.53,063+0.3%6/12 improved
Complete goal.pause: commits6.57+7.7%2/12 improved, 4 tied

The controller-only result is strong and stable: both ranges are disjoint (629-629 vs 49-49 fibers; 4.199-5.633 vs 0.769-1.363 ms), and every pair improved.

The complete pause workload covers preload -> IPC -> Host -> broadcast -> get -> controller -> React. Its busy-JS ranges overlap (21.848-29.949 vs 20.729-26.598 ms), while commits and fibers did not improve. The performance claim is therefore limited to controller-owned renderer work. The complete-path result is directional only and does not establish an end-to-end pause speedup; renderer busy JS also excludes Electron main-process CPU and I/O.

The temporary dual-owner switch, probes, performance spec, results, and worktrees were removed after measurement and are not part of this PR.

Verification

  • npm run rebuild - passed, including the production renderer build.
  • npm --workspace @maka/desktop run test:dist - 1,845/1,845 passed.
  • npm exec -w @maka/desktop -- playwright test --config e2e/playwright.config.ts e2e/goal-dialog-budget.spec.ts --workers=1 --reporter=line - 1/1 passed.
  • npm run typecheck - passed.
  • npm run lint and npm run format:check - passed (3,055 linted / 1,833 formatted files).
  • npx knip --workspace @maka/desktop --no-progress and npx knip --workspace packages/ui --no-progress - passed.
  • npm run check:app-shell-hooks - passed at 42 Hooks / 80 call sites.
  • npm run check:renderer-architecture -- --base 920d7142d001e81868bcd50a28a5bb3898415e3b - 61/61 fixtures passed.
  • npm run astryx:surface-inventory - passed at 235 files / 1 exclusion.
  • NODE_NO_WARNINGS=1 npm --workspace @maka/storage run test:dist - 1,077 passed / 8 skipped.
  • git range-diff, git diff --check upstream/main...HEAD, git merge-base --is-ancestor upstream/main HEAD, and git merge-tree --write-tree --messages upstream/main HEAD - passed.

Screenshots are not applicable; the only visible change removes a transient unusable menu entry during catalog hydration.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex - renderer architecture, implementation, tests, performance measurement, review handling, conflict resolution, and documentation.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes - described under Summary above
  • No

@github-actionsgithub-actionsBot added the effort/L Under 1000 readable lines label Aug 31, 2026
@chihumyum

Copy link
Copy Markdown
ContributorAuthor

@Astro-Han Hi, can you check this out? Thanks a lot

@chihumyum
chihumyumforce-pushed the refactor/goals-controller-scope branch from 2727ac3 to 25f20ecCompareSeptember 1, 2026 05:58

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this lands cleanly, and the performance section is the most disciplined I've read here in a while. It reports that the complete goal.pause path did not improve (overlapping busy-JS ranges, commits and fibers slightly worse) and explicitly limits the claim to controller-owned render scope. That honesty is what makes the controller-only result — 629 → 63 fibers, 6.6ms → 1.6ms busy JS, disjoint ranges, 6/6 paired — actually persuasive.

The design is right too. Three separate contexts rather than one is exactly what earns the reduction: a composer-only change must not wake the indicator. And passing children through untouched so React bails on the unrelated frame is the correct way to scope this without widening Goal state back to the root.

Reviewed at head 25f20ec6. Approving. One P2 I'd like fixed here, two P3s that don't need to be.

P2 — the cloneElement injection gives up a compile-time contract, and this PR is defining a pattern that will be copied.

GoalComposerTargetProps and GoalIndicatorTargetProps declare every injected prop optional, so ReactElement<GoalComposerTargetProps> is satisfied by any element — the compiler cannot tell that the child is the one that actually reads these props. onSetGoal originates in packages/ui/src/composer.tsx:449 and reaches the child through ChatComposerRegionProps extends Omit<ComponentProps<typeof Composer>, …>, so a rename or removal on that cross-package contract would silently turn the injection into a dead prop with no type error. Before this change, onSetGoal={…} at the JSX site was checked.

Today the hole is covered by goal-dialog-budget.spec.ts — the 设定 Goal… menu item renders only when props.onSetGoal is truthy (composer.tsx:1916), so a broken injection fails a real end-to-end path. That is genuinely good coverage and it's why this isn't blocking. But a compile error is cheaper than an Electron run, and #4315 is already reproducing this provider-plus-boundary shape; every further controller that moves below AppShell will copy it. Cheapest fix: require the injected keys in the target-props types, or type the child as ReactElement<ComponentProps<typeof ChatComposerRegion>>.

P3-1 — the composer's enablement gate changed from activeId to ownerActiveId. Old: activeId && localInteractionAvailable. New: canOpenDialog && activeSessionId, where activeSessionId is ownerActiveId = activeCatalogSession && !sharedSessionActive ? activeId : undefined (app-shell.tsx:367). When activeId is set but the session isn't in the catalog yet, the old code rendered an enabled 设定 Goal… whose openDialog returns early on an empty activeSessionId (use-goal-controller.ts:216); the new gate hides the entry instead. The new behavior is better — a dead menu item is worse than none — but the PR says "No intended user-visible behavior change". No code change needed; I'll say so in the squash message unless it was unintended, in which case restoring activeId is the fix.

P3-2 — the provider re-stabilizes reportError because the producer is unstable.showSessionError is a plain function declaration inside AppShellContent (app-shell.tsx:2620), so it gets a fresh identity every render, which is why GoalProvider needs the reportErrorRef + useLayoutEffect + useCallback trio. That's correct as written, but the workaround sits on the wrong side of the boundary, and #4315's provider repeats the same stabilization. If showSessionError's dependencies allow a useCallback, doing it once would let both providers take the prop directly. Out of scope here — worth settling on #4315, where the duplication becomes visible.

AI use: reviewed with Claude Code (diff reading, tracing the gate and prop-injection paths through AppShell, the goals controller, and the shared Composer contract). I verified ownerActiveId's definition, openDialog's early return, showSessionError's declaration form, and the E2E path that exercises the composer injection myself against the head SHA.

Move production Goal controller ownership into GoalProvider and expose reader-local projections for the composer, indicator, and dialog host.
Keep AppShell free of Goal controller and model ownership, route composer and indicator state through separate UI-owned contexts consumed only by the authorized Desktop leaves, bind them at the authoritative Composer and ChatView props, and lock the boundary with architecture and render-scope coverage.
The Set Goal entry now waits for an owner-backed active session instead of briefly exposing an unusable action before catalog hydration.
Generated-by: Codex
@chihumyum
chihumyumforce-pushed the refactor/goals-controller-scope branch from 25f20ec to 3b47b2cCompareSeptember 1, 2026 09:14
@chihumyum

Copy link
Copy Markdown
ContributorAuthor

Addressed the review feedback in 3b47b2c35:

  • P2 fixed: removed the structurally loose cloneElement injection. The two required projection transport shapes now derive from the authoritative Composer / ChatView prop contracts, and the authorized Desktop leaves hand the values to the real JSX props explicitly. Production-consumer uniqueness, hook absence, leaf Omits, and AppShell exclusion are guarded so WorkHub/Workbar cannot inherit the Session Goal later.
  • P3-1 documented as intentional: the unusable Set Goal... entry remains hidden until the selected Session is owner-backed after catalog hydration. The commit message, Summary, screenshot note, and behavior checkbox now say so.
  • P3-2 left to refactor(desktop): move Module Hub controller below AppShell #4315: producer-side reportError stabilization is unchanged here.

The branch is rebased onto 920d7142d; the Astryx inventory conflict was regenerated against 0.5.2 and passes at 235 files / 1 exclusion.

Final-head performance remeasurement preserves the bounded claim: controller-only rendered fibers 629 -> 49 (-92.2%) and renderer busy JS 5.016 -> 1.061 ms (-78.9%), both 6/6 paired wins with disjoint ranges. Full goal.pause busy JS was directional only (24.879 -> 23.637 ms, -5.0%, 9/12, overlapping ranges), while commits/fibers did not improve, so this is not presented as an end-to-end speedup.

Local final-head verification: rebuild/typecheck/lint/format/Knip passed; Desktop 1,845/1,845; storage 1,077 passed / 8 skipped; Goal E2E 1/1; renderer architecture 61/61; AppShell guard 42 Hooks / 80 call sites; clean merge-tree. Hosted CI is rerunning on the new head.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-verified on head 3b47b2c35. The cloneElement injection is gone and both projection shapes derive from the Composer / ChatView prop contracts, so the P2 is closed.

@Astro-Han
Astro-Han merged commit a5ede6b into apache:mainSep 1, 2026
3 checks passed
@chihumyum
chihumyum deleted the refactor/goals-controller-scope branch September 1, 2026 10:48
abhinav-phi pushed a commit to abhinav-phi/maka that referenced this pull request Sep 1, 2026
…che#4316)
AppShell owned the Goal controller, so every Goal state change re-rendered the
shell and, through it, the whole chat surface. The controller's own token update
re-rendered 629 fibers to repaint an indicator.
Move production ownership into `GoalProvider` and publish the composer,
indicator, and dialog projections separately, each at the boundary that actually
reads it. The two authorized Desktop leaves take their projection from a context
consumer and hand it to the real `Composer` / `ChatView` props, so the render
scope stops at the subtree instead of the shell. The transport shapes are
indexed off those components' own prop contracts, which makes the handoff
compile-checked rather than structural, and the leaf `Omit`s remove the props
from what the parent may pass at all. WorkHub and Workbar mount their own
`Composer` / `ChatView` without the Session Goal projections and are unaffected.
Goal business behavior stays in `use-goal-controller.ts`. AppShell loses its Goal
controller and model reads along with the Goal prop plumbing, and
`useGoalController` stays out of the production barrel. Unique-controller,
unique-production-consumer, forbidden-hook, leaf-`Omit`, AppShell Hook-budget,
and renderer-architecture guards keep ownership from returning.
Controller-owned renderer work improves decisively: rendered fibers 629 -> 49
(-92.2%) and renderer busy JS 5.016 -> 1.061 ms (-78.9%), every pair improved
with disjoint ranges. The complete `goal.pause` path is directional only
(-5.0% busy JS, overlapping ranges, commits and fibers flat), so this is not an
end-to-end pause speedup.
One transient UI state changes: while the selected Session is not yet
owner-backed by the hydrated catalog, the unusable "Set Goal..." entry stays
hidden instead of briefly appearing and doing nothing.
Stabilizing the producer of `reportError` is left to apache#4315, where the
duplication is visible.
Generated-by: Codex
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/LUnder 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@chihumyum@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(desktop): move Goal controller ownership below AppShell - #4316

Merged
Astro-Han merged 1 commit into
apache:mainfrom
chihumyum:refactor/goals-controller-scope
Sep 1, 2026
Merged

refactor(desktop): move Goal controller ownership below AppShell#4316
Astro-Han merged 1 commit into
apache:mainfrom
chihumyum:refactor/goals-controller-scope

Conversation

@chihumyum

@chihumyumchihumyum commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Move production Goal controller ownership out of AppShell and into GoalProvider.

  • Publish separate composer, indicator, and dialog projections at their actual reader boundaries.
  • Tie the projection transport to the authoritative Composer / ChatView prop contracts and perform explicit, compile-checked handoff at the two authorized Desktop leaves.
  • Keep WorkHub and Workbar isolated: their Composer / ChatView instances do not consume the Session Goal projections.
  • Remove Goal controller/model reads and Goal prop plumbing from AppShell; keep useGoalController out of the production barrel.
  • Prevent ownership return with unique-controller, unique-production-consumer, forbidden-hook, leaf-Omit, AppShell Hook-budget, and renderer-architecture guards.
  • Rebase onto 920d7142d and regenerate the Astryx 0.5.2 surface inventory while resolving the documentation conflict.

This intentionally changes one transient UI state: while the selected Session is not yet owner-backed by the hydrated catalog, the unusable Set Goal... entry stays hidden instead of briefly appearing and doing nothing. Goal business behavior otherwise remains in use-goal-controller.ts unchanged. Stabilizing the producer of reportError is left to #4315, where the duplication is visible.

Performance evidence

Measured on final head 3b47b2c35 against 920d7142d in a 12-Session fixture. Each workload ran legacy/scoped ownership as alternating pairs inside the same Electron process; root-mode changes and resynchronization stayed outside the measurement window. The controller-only workload used 6 pairs and the complete goal.pause workload used 12. Renderer profiling sampled at 100 microseconds; the Fiber probe was disabled during each busy-JS window. Values below are conventional medians.

WorkloadLegacy ownerScoped ownerChangePaired result
Controller-only token update: rendered fibers62949-92.2%6/6 improved
Controller-only token update: renderer busy JS5.016 ms1.061 ms-78.9%6/6 improved
Controller-only token update: commits11unchanged6/6 tied
Complete goal.pause: renderer busy JS24.879 ms23.637 ms-5.0%9/12 improved
Complete goal.pause: summed rendered fibers3,053.53,063+0.3%6/12 improved
Complete goal.pause: commits6.57+7.7%2/12 improved, 4 tied

The controller-only result is strong and stable: both ranges are disjoint (629-629 vs 49-49 fibers; 4.199-5.633 vs 0.769-1.363 ms), and every pair improved.

The complete pause workload covers preload -> IPC -> Host -> broadcast -> get -> controller -> React. Its busy-JS ranges overlap (21.848-29.949 vs 20.729-26.598 ms), while commits and fibers did not improve. The performance claim is therefore limited to controller-owned renderer work. The complete-path result is directional only and does not establish an end-to-end pause speedup; renderer busy JS also excludes Electron main-process CPU and I/O.

The temporary dual-owner switch, probes, performance spec, results, and worktrees were removed after measurement and are not part of this PR.

Verification

  • npm run rebuild - passed, including the production renderer build.
  • npm --workspace @maka/desktop run test:dist - 1,845/1,845 passed.
  • npm exec -w @maka/desktop -- playwright test --config e2e/playwright.config.ts e2e/goal-dialog-budget.spec.ts --workers=1 --reporter=line - 1/1 passed.
  • npm run typecheck - passed.
  • npm run lint and npm run format:check - passed (3,055 linted / 1,833 formatted files).
  • npx knip --workspace @maka/desktop --no-progress and npx knip --workspace packages/ui --no-progress - passed.
  • npm run check:app-shell-hooks - passed at 42 Hooks / 80 call sites.
  • npm run check:renderer-architecture -- --base 920d7142d001e81868bcd50a28a5bb3898415e3b - 61/61 fixtures passed.
  • npm run astryx:surface-inventory - passed at 235 files / 1 exclusion.
  • NODE_NO_WARNINGS=1 npm --workspace @maka/storage run test:dist - 1,077 passed / 8 skipped.
  • git range-diff, git diff --check upstream/main...HEAD, git merge-base --is-ancestor upstream/main HEAD, and git merge-tree --write-tree --messages upstream/main HEAD - passed.

Screenshots are not applicable; the only visible change removes a transient unusable menu entry during catalog hydration.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex - renderer architecture, implementation, tests, performance measurement, review handling, conflict resolution, and documentation.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes - described under Summary above
  • No

@github-actionsgithub-actionsBot added the effort/L Under 1000 readable lines label Aug 31, 2026
@chihumyum

Copy link
Copy Markdown
ContributorAuthor

@Astro-Han Hi, can you check this out? Thanks a lot

@chihumyum
chihumyumforce-pushed the refactor/goals-controller-scope branch from 2727ac3 to 25f20ecCompareSeptember 1, 2026 05:58

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this lands cleanly, and the performance section is the most disciplined I've read here in a while. It reports that the complete goal.pause path did not improve (overlapping busy-JS ranges, commits and fibers slightly worse) and explicitly limits the claim to controller-owned render scope. That honesty is what makes the controller-only result — 629 → 63 fibers, 6.6ms → 1.6ms busy JS, disjoint ranges, 6/6 paired — actually persuasive.

The design is right too. Three separate contexts rather than one is exactly what earns the reduction: a composer-only change must not wake the indicator. And passing children through untouched so React bails on the unrelated frame is the correct way to scope this without widening Goal state back to the root.

Reviewed at head 25f20ec6. Approving. One P2 I'd like fixed here, two P3s that don't need to be.

P2 — the cloneElement injection gives up a compile-time contract, and this PR is defining a pattern that will be copied.

GoalComposerTargetProps and GoalIndicatorTargetProps declare every injected prop optional, so ReactElement<GoalComposerTargetProps> is satisfied by any element — the compiler cannot tell that the child is the one that actually reads these props. onSetGoal originates in packages/ui/src/composer.tsx:449 and reaches the child through ChatComposerRegionProps extends Omit<ComponentProps<typeof Composer>, …>, so a rename or removal on that cross-package contract would silently turn the injection into a dead prop with no type error. Before this change, onSetGoal={…} at the JSX site was checked.

Today the hole is covered by goal-dialog-budget.spec.ts — the 设定 Goal… menu item renders only when props.onSetGoal is truthy (composer.tsx:1916), so a broken injection fails a real end-to-end path. That is genuinely good coverage and it's why this isn't blocking. But a compile error is cheaper than an Electron run, and #4315 is already reproducing this provider-plus-boundary shape; every further controller that moves below AppShell will copy it. Cheapest fix: require the injected keys in the target-props types, or type the child as ReactElement<ComponentProps<typeof ChatComposerRegion>>.

P3-1 — the composer's enablement gate changed from activeId to ownerActiveId. Old: activeId && localInteractionAvailable. New: canOpenDialog && activeSessionId, where activeSessionId is ownerActiveId = activeCatalogSession && !sharedSessionActive ? activeId : undefined (app-shell.tsx:367). When activeId is set but the session isn't in the catalog yet, the old code rendered an enabled 设定 Goal… whose openDialog returns early on an empty activeSessionId (use-goal-controller.ts:216); the new gate hides the entry instead. The new behavior is better — a dead menu item is worse than none — but the PR says "No intended user-visible behavior change". No code change needed; I'll say so in the squash message unless it was unintended, in which case restoring activeId is the fix.

P3-2 — the provider re-stabilizes reportError because the producer is unstable.showSessionError is a plain function declaration inside AppShellContent (app-shell.tsx:2620), so it gets a fresh identity every render, which is why GoalProvider needs the reportErrorRef + useLayoutEffect + useCallback trio. That's correct as written, but the workaround sits on the wrong side of the boundary, and #4315's provider repeats the same stabilization. If showSessionError's dependencies allow a useCallback, doing it once would let both providers take the prop directly. Out of scope here — worth settling on #4315, where the duplication becomes visible.

AI use: reviewed with Claude Code (diff reading, tracing the gate and prop-injection paths through AppShell, the goals controller, and the shared Composer contract). I verified ownerActiveId's definition, openDialog's early return, showSessionError's declaration form, and the E2E path that exercises the composer injection myself against the head SHA.

Move production Goal controller ownership into GoalProvider and expose reader-local projections for the composer, indicator, and dialog host.
Keep AppShell free of Goal controller and model ownership, route composer and indicator state through separate UI-owned contexts consumed only by the authorized Desktop leaves, bind them at the authoritative Composer and ChatView props, and lock the boundary with architecture and render-scope coverage.
The Set Goal entry now waits for an owner-backed active session instead of briefly exposing an unusable action before catalog hydration.
Generated-by: Codex
@chihumyum
chihumyumforce-pushed the refactor/goals-controller-scope branch from 25f20ec to 3b47b2cCompareSeptember 1, 2026 09:14
@chihumyum

Copy link
Copy Markdown
ContributorAuthor

Addressed the review feedback in 3b47b2c35:

  • P2 fixed: removed the structurally loose cloneElement injection. The two required projection transport shapes now derive from the authoritative Composer / ChatView prop contracts, and the authorized Desktop leaves hand the values to the real JSX props explicitly. Production-consumer uniqueness, hook absence, leaf Omits, and AppShell exclusion are guarded so WorkHub/Workbar cannot inherit the Session Goal later.
  • P3-1 documented as intentional: the unusable Set Goal... entry remains hidden until the selected Session is owner-backed after catalog hydration. The commit message, Summary, screenshot note, and behavior checkbox now say so.
  • P3-2 left to refactor(desktop): move Module Hub controller below AppShell #4315: producer-side reportError stabilization is unchanged here.

The branch is rebased onto 920d7142d; the Astryx inventory conflict was regenerated against 0.5.2 and passes at 235 files / 1 exclusion.

Final-head performance remeasurement preserves the bounded claim: controller-only rendered fibers 629 -> 49 (-92.2%) and renderer busy JS 5.016 -> 1.061 ms (-78.9%), both 6/6 paired wins with disjoint ranges. Full goal.pause busy JS was directional only (24.879 -> 23.637 ms, -5.0%, 9/12, overlapping ranges), while commits/fibers did not improve, so this is not presented as an end-to-end speedup.

Local final-head verification: rebuild/typecheck/lint/format/Knip passed; Desktop 1,845/1,845; storage 1,077 passed / 8 skipped; Goal E2E 1/1; renderer architecture 61/61; AppShell guard 42 Hooks / 80 call sites; clean merge-tree. Hosted CI is rerunning on the new head.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-verified on head 3b47b2c35. The cloneElement injection is gone and both projection shapes derive from the Composer / ChatView prop contracts, so the P2 is closed.

@Astro-Han
Astro-Han merged commit a5ede6b into apache:mainSep 1, 2026
3 checks passed
@chihumyum
chihumyum deleted the refactor/goals-controller-scope branch September 1, 2026 10:48
abhinav-phi pushed a commit to abhinav-phi/maka that referenced this pull request Sep 1, 2026
…che#4316)
AppShell owned the Goal controller, so every Goal state change re-rendered the
shell and, through it, the whole chat surface. The controller's own token update
re-rendered 629 fibers to repaint an indicator.
Move production ownership into `GoalProvider` and publish the composer,
indicator, and dialog projections separately, each at the boundary that actually
reads it. The two authorized Desktop leaves take their projection from a context
consumer and hand it to the real `Composer` / `ChatView` props, so the render
scope stops at the subtree instead of the shell. The transport shapes are
indexed off those components' own prop contracts, which makes the handoff
compile-checked rather than structural, and the leaf `Omit`s remove the props
from what the parent may pass at all. WorkHub and Workbar mount their own
`Composer` / `ChatView` without the Session Goal projections and are unaffected.
Goal business behavior stays in `use-goal-controller.ts`. AppShell loses its Goal
controller and model reads along with the Goal prop plumbing, and
`useGoalController` stays out of the production barrel. Unique-controller,
unique-production-consumer, forbidden-hook, leaf-`Omit`, AppShell Hook-budget,
and renderer-architecture guards keep ownership from returning.
Controller-owned renderer work improves decisively: rendered fibers 629 -> 49
(-92.2%) and renderer busy JS 5.016 -> 1.061 ms (-78.9%), every pair improved
with disjoint ranges. The complete `goal.pause` path is directional only
(-5.0% busy JS, overlapping ranges, commits and fibers flat), so this is not an
end-to-end pause speedup.
One transient UI state changes: while the selected Session is not yet
owner-backed by the hydrated catalog, the unusable "Set Goal..." entry stays
hidden instead of briefly appearing and doing nothing.
Stabilizing the producer of `reportError` is left to apache#4315, where the
duplication is visible.
Generated-by: Codex
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/LUnder 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@chihumyum@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(desktop): move Goal controller ownership below AppShell - #4316

Merged
Astro-Han merged 1 commit into
apache:mainfrom
chihumyum:refactor/goals-controller-scope
Sep 1, 2026
Merged

refactor(desktop): move Goal controller ownership below AppShell#4316
Astro-Han merged 1 commit into
apache:mainfrom
chihumyum:refactor/goals-controller-scope

Conversation

@chihumyum

@chihumyumchihumyum commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Move production Goal controller ownership out of AppShell and into GoalProvider.

  • Publish separate composer, indicator, and dialog projections at their actual reader boundaries.
  • Tie the projection transport to the authoritative Composer / ChatView prop contracts and perform explicit, compile-checked handoff at the two authorized Desktop leaves.
  • Keep WorkHub and Workbar isolated: their Composer / ChatView instances do not consume the Session Goal projections.
  • Remove Goal controller/model reads and Goal prop plumbing from AppShell; keep useGoalController out of the production barrel.
  • Prevent ownership return with unique-controller, unique-production-consumer, forbidden-hook, leaf-Omit, AppShell Hook-budget, and renderer-architecture guards.
  • Rebase onto 920d7142d and regenerate the Astryx 0.5.2 surface inventory while resolving the documentation conflict.

This intentionally changes one transient UI state: while the selected Session is not yet owner-backed by the hydrated catalog, the unusable Set Goal... entry stays hidden instead of briefly appearing and doing nothing. Goal business behavior otherwise remains in use-goal-controller.ts unchanged. Stabilizing the producer of reportError is left to #4315, where the duplication is visible.

Performance evidence

Measured on final head 3b47b2c35 against 920d7142d in a 12-Session fixture. Each workload ran legacy/scoped ownership as alternating pairs inside the same Electron process; root-mode changes and resynchronization stayed outside the measurement window. The controller-only workload used 6 pairs and the complete goal.pause workload used 12. Renderer profiling sampled at 100 microseconds; the Fiber probe was disabled during each busy-JS window. Values below are conventional medians.

WorkloadLegacy ownerScoped ownerChangePaired result
Controller-only token update: rendered fibers62949-92.2%6/6 improved
Controller-only token update: renderer busy JS5.016 ms1.061 ms-78.9%6/6 improved
Controller-only token update: commits11unchanged6/6 tied
Complete goal.pause: renderer busy JS24.879 ms23.637 ms-5.0%9/12 improved
Complete goal.pause: summed rendered fibers3,053.53,063+0.3%6/12 improved
Complete goal.pause: commits6.57+7.7%2/12 improved, 4 tied

The controller-only result is strong and stable: both ranges are disjoint (629-629 vs 49-49 fibers; 4.199-5.633 vs 0.769-1.363 ms), and every pair improved.

The complete pause workload covers preload -> IPC -> Host -> broadcast -> get -> controller -> React. Its busy-JS ranges overlap (21.848-29.949 vs 20.729-26.598 ms), while commits and fibers did not improve. The performance claim is therefore limited to controller-owned renderer work. The complete-path result is directional only and does not establish an end-to-end pause speedup; renderer busy JS also excludes Electron main-process CPU and I/O.

The temporary dual-owner switch, probes, performance spec, results, and worktrees were removed after measurement and are not part of this PR.

Verification

  • npm run rebuild - passed, including the production renderer build.
  • npm --workspace @maka/desktop run test:dist - 1,845/1,845 passed.
  • npm exec -w @maka/desktop -- playwright test --config e2e/playwright.config.ts e2e/goal-dialog-budget.spec.ts --workers=1 --reporter=line - 1/1 passed.
  • npm run typecheck - passed.
  • npm run lint and npm run format:check - passed (3,055 linted / 1,833 formatted files).
  • npx knip --workspace @maka/desktop --no-progress and npx knip --workspace packages/ui --no-progress - passed.
  • npm run check:app-shell-hooks - passed at 42 Hooks / 80 call sites.
  • npm run check:renderer-architecture -- --base 920d7142d001e81868bcd50a28a5bb3898415e3b - 61/61 fixtures passed.
  • npm run astryx:surface-inventory - passed at 235 files / 1 exclusion.
  • NODE_NO_WARNINGS=1 npm --workspace @maka/storage run test:dist - 1,077 passed / 8 skipped.
  • git range-diff, git diff --check upstream/main...HEAD, git merge-base --is-ancestor upstream/main HEAD, and git merge-tree --write-tree --messages upstream/main HEAD - passed.

Screenshots are not applicable; the only visible change removes a transient unusable menu entry during catalog hydration.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex - renderer architecture, implementation, tests, performance measurement, review handling, conflict resolution, and documentation.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes - described under Summary above
  • No

@github-actionsgithub-actionsBot added the effort/L Under 1000 readable lines label Aug 31, 2026
@chihumyum

Copy link
Copy Markdown
ContributorAuthor

@Astro-Han Hi, can you check this out? Thanks a lot

@chihumyum
chihumyumforce-pushed the refactor/goals-controller-scope branch from 2727ac3 to 25f20ecCompareSeptember 1, 2026 05:58

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this lands cleanly, and the performance section is the most disciplined I've read here in a while. It reports that the complete goal.pause path did not improve (overlapping busy-JS ranges, commits and fibers slightly worse) and explicitly limits the claim to controller-owned render scope. That honesty is what makes the controller-only result — 629 → 63 fibers, 6.6ms → 1.6ms busy JS, disjoint ranges, 6/6 paired — actually persuasive.

The design is right too. Three separate contexts rather than one is exactly what earns the reduction: a composer-only change must not wake the indicator. And passing children through untouched so React bails on the unrelated frame is the correct way to scope this without widening Goal state back to the root.

Reviewed at head 25f20ec6. Approving. One P2 I'd like fixed here, two P3s that don't need to be.

P2 — the cloneElement injection gives up a compile-time contract, and this PR is defining a pattern that will be copied.

GoalComposerTargetProps and GoalIndicatorTargetProps declare every injected prop optional, so ReactElement<GoalComposerTargetProps> is satisfied by any element — the compiler cannot tell that the child is the one that actually reads these props. onSetGoal originates in packages/ui/src/composer.tsx:449 and reaches the child through ChatComposerRegionProps extends Omit<ComponentProps<typeof Composer>, …>, so a rename or removal on that cross-package contract would silently turn the injection into a dead prop with no type error. Before this change, onSetGoal={…} at the JSX site was checked.

Today the hole is covered by goal-dialog-budget.spec.ts — the 设定 Goal… menu item renders only when props.onSetGoal is truthy (composer.tsx:1916), so a broken injection fails a real end-to-end path. That is genuinely good coverage and it's why this isn't blocking. But a compile error is cheaper than an Electron run, and #4315 is already reproducing this provider-plus-boundary shape; every further controller that moves below AppShell will copy it. Cheapest fix: require the injected keys in the target-props types, or type the child as ReactElement<ComponentProps<typeof ChatComposerRegion>>.

P3-1 — the composer's enablement gate changed from activeId to ownerActiveId. Old: activeId && localInteractionAvailable. New: canOpenDialog && activeSessionId, where activeSessionId is ownerActiveId = activeCatalogSession && !sharedSessionActive ? activeId : undefined (app-shell.tsx:367). When activeId is set but the session isn't in the catalog yet, the old code rendered an enabled 设定 Goal… whose openDialog returns early on an empty activeSessionId (use-goal-controller.ts:216); the new gate hides the entry instead. The new behavior is better — a dead menu item is worse than none — but the PR says "No intended user-visible behavior change". No code change needed; I'll say so in the squash message unless it was unintended, in which case restoring activeId is the fix.

P3-2 — the provider re-stabilizes reportError because the producer is unstable.showSessionError is a plain function declaration inside AppShellContent (app-shell.tsx:2620), so it gets a fresh identity every render, which is why GoalProvider needs the reportErrorRef + useLayoutEffect + useCallback trio. That's correct as written, but the workaround sits on the wrong side of the boundary, and #4315's provider repeats the same stabilization. If showSessionError's dependencies allow a useCallback, doing it once would let both providers take the prop directly. Out of scope here — worth settling on #4315, where the duplication becomes visible.

AI use: reviewed with Claude Code (diff reading, tracing the gate and prop-injection paths through AppShell, the goals controller, and the shared Composer contract). I verified ownerActiveId's definition, openDialog's early return, showSessionError's declaration form, and the E2E path that exercises the composer injection myself against the head SHA.

Move production Goal controller ownership into GoalProvider and expose reader-local projections for the composer, indicator, and dialog host.
Keep AppShell free of Goal controller and model ownership, route composer and indicator state through separate UI-owned contexts consumed only by the authorized Desktop leaves, bind them at the authoritative Composer and ChatView props, and lock the boundary with architecture and render-scope coverage.
The Set Goal entry now waits for an owner-backed active session instead of briefly exposing an unusable action before catalog hydration.
Generated-by: Codex
@chihumyum
chihumyumforce-pushed the refactor/goals-controller-scope branch from 25f20ec to 3b47b2cCompareSeptember 1, 2026 09:14
@chihumyum

Copy link
Copy Markdown
ContributorAuthor

Addressed the review feedback in 3b47b2c35:

  • P2 fixed: removed the structurally loose cloneElement injection. The two required projection transport shapes now derive from the authoritative Composer / ChatView prop contracts, and the authorized Desktop leaves hand the values to the real JSX props explicitly. Production-consumer uniqueness, hook absence, leaf Omits, and AppShell exclusion are guarded so WorkHub/Workbar cannot inherit the Session Goal later.
  • P3-1 documented as intentional: the unusable Set Goal... entry remains hidden until the selected Session is owner-backed after catalog hydration. The commit message, Summary, screenshot note, and behavior checkbox now say so.
  • P3-2 left to refactor(desktop): move Module Hub controller below AppShell #4315: producer-side reportError stabilization is unchanged here.

The branch is rebased onto 920d7142d; the Astryx inventory conflict was regenerated against 0.5.2 and passes at 235 files / 1 exclusion.

Final-head performance remeasurement preserves the bounded claim: controller-only rendered fibers 629 -> 49 (-92.2%) and renderer busy JS 5.016 -> 1.061 ms (-78.9%), both 6/6 paired wins with disjoint ranges. Full goal.pause busy JS was directional only (24.879 -> 23.637 ms, -5.0%, 9/12, overlapping ranges), while commits/fibers did not improve, so this is not presented as an end-to-end speedup.

Local final-head verification: rebuild/typecheck/lint/format/Knip passed; Desktop 1,845/1,845; storage 1,077 passed / 8 skipped; Goal E2E 1/1; renderer architecture 61/61; AppShell guard 42 Hooks / 80 call sites; clean merge-tree. Hosted CI is rerunning on the new head.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-verified on head 3b47b2c35. The cloneElement injection is gone and both projection shapes derive from the Composer / ChatView prop contracts, so the P2 is closed.

@Astro-Han
Astro-Han merged commit a5ede6b into apache:mainSep 1, 2026
3 checks passed
@chihumyum
chihumyum deleted the refactor/goals-controller-scope branch September 1, 2026 10:48
abhinav-phi pushed a commit to abhinav-phi/maka that referenced this pull request Sep 1, 2026
…che#4316)
AppShell owned the Goal controller, so every Goal state change re-rendered the
shell and, through it, the whole chat surface. The controller's own token update
re-rendered 629 fibers to repaint an indicator.
Move production ownership into `GoalProvider` and publish the composer,
indicator, and dialog projections separately, each at the boundary that actually
reads it. The two authorized Desktop leaves take their projection from a context
consumer and hand it to the real `Composer` / `ChatView` props, so the render
scope stops at the subtree instead of the shell. The transport shapes are
indexed off those components' own prop contracts, which makes the handoff
compile-checked rather than structural, and the leaf `Omit`s remove the props
from what the parent may pass at all. WorkHub and Workbar mount their own
`Composer` / `ChatView` without the Session Goal projections and are unaffected.
Goal business behavior stays in `use-goal-controller.ts`. AppShell loses its Goal
controller and model reads along with the Goal prop plumbing, and
`useGoalController` stays out of the production barrel. Unique-controller,
unique-production-consumer, forbidden-hook, leaf-`Omit`, AppShell Hook-budget,
and renderer-architecture guards keep ownership from returning.
Controller-owned renderer work improves decisively: rendered fibers 629 -> 49
(-92.2%) and renderer busy JS 5.016 -> 1.061 ms (-78.9%), every pair improved
with disjoint ranges. The complete `goal.pause` path is directional only
(-5.0% busy JS, overlapping ranges, commits and fibers flat), so this is not an
end-to-end pause speedup.
One transient UI state changes: while the selected Session is not yet
owner-backed by the hydrated catalog, the unusable "Set Goal..." entry stays
hidden instead of briefly appearing and doing nothing.
Stabilizing the producer of `reportError` is left to apache#4315, where the
duplication is visible.
Generated-by: Codex
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/LUnder 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@chihumyum@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

refactor(desktop): move Goal controller ownership below AppShell - #4316

Merged
Astro-Han merged 1 commit into
apache:mainfrom
chihumyum:refactor/goals-controller-scope
Sep 1, 2026
Merged

refactor(desktop): move Goal controller ownership below AppShell#4316
Astro-Han merged 1 commit into
apache:mainfrom
chihumyum:refactor/goals-controller-scope

Conversation

@chihumyum

@chihumyumchihumyum commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Move production Goal controller ownership out of AppShell and into GoalProvider.

  • Publish separate composer, indicator, and dialog projections at their actual reader boundaries.
  • Tie the projection transport to the authoritative Composer / ChatView prop contracts and perform explicit, compile-checked handoff at the two authorized Desktop leaves.
  • Keep WorkHub and Workbar isolated: their Composer / ChatView instances do not consume the Session Goal projections.
  • Remove Goal controller/model reads and Goal prop plumbing from AppShell; keep useGoalController out of the production barrel.
  • Prevent ownership return with unique-controller, unique-production-consumer, forbidden-hook, leaf-Omit, AppShell Hook-budget, and renderer-architecture guards.
  • Rebase onto 920d7142d and regenerate the Astryx 0.5.2 surface inventory while resolving the documentation conflict.

This intentionally changes one transient UI state: while the selected Session is not yet owner-backed by the hydrated catalog, the unusable Set Goal... entry stays hidden instead of briefly appearing and doing nothing. Goal business behavior otherwise remains in use-goal-controller.ts unchanged. Stabilizing the producer of reportError is left to #4315, where the duplication is visible.

Performance evidence

Measured on final head 3b47b2c35 against 920d7142d in a 12-Session fixture. Each workload ran legacy/scoped ownership as alternating pairs inside the same Electron process; root-mode changes and resynchronization stayed outside the measurement window. The controller-only workload used 6 pairs and the complete goal.pause workload used 12. Renderer profiling sampled at 100 microseconds; the Fiber probe was disabled during each busy-JS window. Values below are conventional medians.

WorkloadLegacy ownerScoped ownerChangePaired result
Controller-only token update: rendered fibers62949-92.2%6/6 improved
Controller-only token update: renderer busy JS5.016 ms1.061 ms-78.9%6/6 improved
Controller-only token update: commits11unchanged6/6 tied
Complete goal.pause: renderer busy JS24.879 ms23.637 ms-5.0%9/12 improved
Complete goal.pause: summed rendered fibers3,053.53,063+0.3%6/12 improved
Complete goal.pause: commits6.57+7.7%2/12 improved, 4 tied

The controller-only result is strong and stable: both ranges are disjoint (629-629 vs 49-49 fibers; 4.199-5.633 vs 0.769-1.363 ms), and every pair improved.

The complete pause workload covers preload -> IPC -> Host -> broadcast -> get -> controller -> React. Its busy-JS ranges overlap (21.848-29.949 vs 20.729-26.598 ms), while commits and fibers did not improve. The performance claim is therefore limited to controller-owned renderer work. The complete-path result is directional only and does not establish an end-to-end pause speedup; renderer busy JS also excludes Electron main-process CPU and I/O.

The temporary dual-owner switch, probes, performance spec, results, and worktrees were removed after measurement and are not part of this PR.

Verification

  • npm run rebuild - passed, including the production renderer build.
  • npm --workspace @maka/desktop run test:dist - 1,845/1,845 passed.
  • npm exec -w @maka/desktop -- playwright test --config e2e/playwright.config.ts e2e/goal-dialog-budget.spec.ts --workers=1 --reporter=line - 1/1 passed.
  • npm run typecheck - passed.
  • npm run lint and npm run format:check - passed (3,055 linted / 1,833 formatted files).
  • npx knip --workspace @maka/desktop --no-progress and npx knip --workspace packages/ui --no-progress - passed.
  • npm run check:app-shell-hooks - passed at 42 Hooks / 80 call sites.
  • npm run check:renderer-architecture -- --base 920d7142d001e81868bcd50a28a5bb3898415e3b - 61/61 fixtures passed.
  • npm run astryx:surface-inventory - passed at 235 files / 1 exclusion.
  • NODE_NO_WARNINGS=1 npm --workspace @maka/storage run test:dist - 1,077 passed / 8 skipped.
  • git range-diff, git diff --check upstream/main...HEAD, git merge-base --is-ancestor upstream/main HEAD, and git merge-tree --write-tree --messages upstream/main HEAD - passed.

Screenshots are not applicable; the only visible change removes a transient unusable menu entry during catalog hydration.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex - renderer architecture, implementation, tests, performance measurement, review handling, conflict resolution, and documentation.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes - described under Summary above
  • No

@github-actionsgithub-actionsBot added the effort/L Under 1000 readable lines label Aug 31, 2026
@chihumyum

Copy link
Copy Markdown
ContributorAuthor

@Astro-Han Hi, can you check this out? Thanks a lot

@chihumyum
chihumyumforce-pushed the refactor/goals-controller-scope branch from 2727ac3 to 25f20ecCompareSeptember 1, 2026 05:58

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this lands cleanly, and the performance section is the most disciplined I've read here in a while. It reports that the complete goal.pause path did not improve (overlapping busy-JS ranges, commits and fibers slightly worse) and explicitly limits the claim to controller-owned render scope. That honesty is what makes the controller-only result — 629 → 63 fibers, 6.6ms → 1.6ms busy JS, disjoint ranges, 6/6 paired — actually persuasive.

The design is right too. Three separate contexts rather than one is exactly what earns the reduction: a composer-only change must not wake the indicator. And passing children through untouched so React bails on the unrelated frame is the correct way to scope this without widening Goal state back to the root.

Reviewed at head 25f20ec6. Approving. One P2 I'd like fixed here, two P3s that don't need to be.

P2 — the cloneElement injection gives up a compile-time contract, and this PR is defining a pattern that will be copied.

GoalComposerTargetProps and GoalIndicatorTargetProps declare every injected prop optional, so ReactElement<GoalComposerTargetProps> is satisfied by any element — the compiler cannot tell that the child is the one that actually reads these props. onSetGoal originates in packages/ui/src/composer.tsx:449 and reaches the child through ChatComposerRegionProps extends Omit<ComponentProps<typeof Composer>, …>, so a rename or removal on that cross-package contract would silently turn the injection into a dead prop with no type error. Before this change, onSetGoal={…} at the JSX site was checked.

Today the hole is covered by goal-dialog-budget.spec.ts — the 设定 Goal… menu item renders only when props.onSetGoal is truthy (composer.tsx:1916), so a broken injection fails a real end-to-end path. That is genuinely good coverage and it's why this isn't blocking. But a compile error is cheaper than an Electron run, and #4315 is already reproducing this provider-plus-boundary shape; every further controller that moves below AppShell will copy it. Cheapest fix: require the injected keys in the target-props types, or type the child as ReactElement<ComponentProps<typeof ChatComposerRegion>>.

P3-1 — the composer's enablement gate changed from activeId to ownerActiveId. Old: activeId && localInteractionAvailable. New: canOpenDialog && activeSessionId, where activeSessionId is ownerActiveId = activeCatalogSession && !sharedSessionActive ? activeId : undefined (app-shell.tsx:367). When activeId is set but the session isn't in the catalog yet, the old code rendered an enabled 设定 Goal… whose openDialog returns early on an empty activeSessionId (use-goal-controller.ts:216); the new gate hides the entry instead. The new behavior is better — a dead menu item is worse than none — but the PR says "No intended user-visible behavior change". No code change needed; I'll say so in the squash message unless it was unintended, in which case restoring activeId is the fix.

P3-2 — the provider re-stabilizes reportError because the producer is unstable.showSessionError is a plain function declaration inside AppShellContent (app-shell.tsx:2620), so it gets a fresh identity every render, which is why GoalProvider needs the reportErrorRef + useLayoutEffect + useCallback trio. That's correct as written, but the workaround sits on the wrong side of the boundary, and #4315's provider repeats the same stabilization. If showSessionError's dependencies allow a useCallback, doing it once would let both providers take the prop directly. Out of scope here — worth settling on #4315, where the duplication becomes visible.

AI use: reviewed with Claude Code (diff reading, tracing the gate and prop-injection paths through AppShell, the goals controller, and the shared Composer contract). I verified ownerActiveId's definition, openDialog's early return, showSessionError's declaration form, and the E2E path that exercises the composer injection myself against the head SHA.

Move production Goal controller ownership into GoalProvider and expose reader-local projections for the composer, indicator, and dialog host.
Keep AppShell free of Goal controller and model ownership, route composer and indicator state through separate UI-owned contexts consumed only by the authorized Desktop leaves, bind them at the authoritative Composer and ChatView props, and lock the boundary with architecture and render-scope coverage.
The Set Goal entry now waits for an owner-backed active session instead of briefly exposing an unusable action before catalog hydration.
Generated-by: Codex
@chihumyum
chihumyumforce-pushed the refactor/goals-controller-scope branch from 25f20ec to 3b47b2cCompareSeptember 1, 2026 09:14
@chihumyum

Copy link
Copy Markdown
ContributorAuthor

Addressed the review feedback in 3b47b2c35:

  • P2 fixed: removed the structurally loose cloneElement injection. The two required projection transport shapes now derive from the authoritative Composer / ChatView prop contracts, and the authorized Desktop leaves hand the values to the real JSX props explicitly. Production-consumer uniqueness, hook absence, leaf Omits, and AppShell exclusion are guarded so WorkHub/Workbar cannot inherit the Session Goal later.
  • P3-1 documented as intentional: the unusable Set Goal... entry remains hidden until the selected Session is owner-backed after catalog hydration. The commit message, Summary, screenshot note, and behavior checkbox now say so.
  • P3-2 left to refactor(desktop): move Module Hub controller below AppShell #4315: producer-side reportError stabilization is unchanged here.

The branch is rebased onto 920d7142d; the Astryx inventory conflict was regenerated against 0.5.2 and passes at 235 files / 1 exclusion.

Final-head performance remeasurement preserves the bounded claim: controller-only rendered fibers 629 -> 49 (-92.2%) and renderer busy JS 5.016 -> 1.061 ms (-78.9%), both 6/6 paired wins with disjoint ranges. Full goal.pause busy JS was directional only (24.879 -> 23.637 ms, -5.0%, 9/12, overlapping ranges), while commits/fibers did not improve, so this is not presented as an end-to-end speedup.

Local final-head verification: rebuild/typecheck/lint/format/Knip passed; Desktop 1,845/1,845; storage 1,077 passed / 8 skipped; Goal E2E 1/1; renderer architecture 61/61; AppShell guard 42 Hooks / 80 call sites; clean merge-tree. Hosted CI is rerunning on the new head.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-verified on head 3b47b2c35. The cloneElement injection is gone and both projection shapes derive from the Composer / ChatView prop contracts, so the P2 is closed.

@Astro-Han
Astro-Han merged commit a5ede6b into apache:mainSep 1, 2026
3 checks passed
@chihumyum
chihumyum deleted the refactor/goals-controller-scope branch September 1, 2026 10:48
abhinav-phi pushed a commit to abhinav-phi/maka that referenced this pull request Sep 1, 2026
…che#4316)
AppShell owned the Goal controller, so every Goal state change re-rendered the
shell and, through it, the whole chat surface. The controller's own token update
re-rendered 629 fibers to repaint an indicator.
Move production ownership into `GoalProvider` and publish the composer,
indicator, and dialog projections separately, each at the boundary that actually
reads it. The two authorized Desktop leaves take their projection from a context
consumer and hand it to the real `Composer` / `ChatView` props, so the render
scope stops at the subtree instead of the shell. The transport shapes are
indexed off those components' own prop contracts, which makes the handoff
compile-checked rather than structural, and the leaf `Omit`s remove the props
from what the parent may pass at all. WorkHub and Workbar mount their own
`Composer` / `ChatView` without the Session Goal projections and are unaffected.
Goal business behavior stays in `use-goal-controller.ts`. AppShell loses its Goal
controller and model reads along with the Goal prop plumbing, and
`useGoalController` stays out of the production barrel. Unique-controller,
unique-production-consumer, forbidden-hook, leaf-`Omit`, AppShell Hook-budget,
and renderer-architecture guards keep ownership from returning.
Controller-owned renderer work improves decisively: rendered fibers 629 -> 49
(-92.2%) and renderer busy JS 5.016 -> 1.061 ms (-78.9%), every pair improved
with disjoint ranges. The complete `goal.pause` path is directional only
(-5.0% busy JS, overlapping ranges, commits and fibers flat), so this is not an
end-to-end pause speedup.
One transient UI state changes: while the selected Session is not yet
owner-backed by the hydrated catalog, the unusable "Set Goal..." entry stays
hidden instead of briefly appearing and doing nothing.
Stabilizing the producer of `reportError` is left to apache#4315, where the
duplication is visible.
Generated-by: Codex
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/LUnder 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@chihumyum@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(desktop): move Goal controller ownership below AppShell - #4316

Merged
Astro-Han merged 1 commit into
apache:mainfrom
chihumyum:refactor/goals-controller-scope
Sep 1, 2026
Merged

refactor(desktop): move Goal controller ownership below AppShell#4316
Astro-Han merged 1 commit into
apache:mainfrom
chihumyum:refactor/goals-controller-scope

Conversation

@chihumyum

@chihumyumchihumyum commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Move production Goal controller ownership out of AppShell and into GoalProvider.

  • Publish separate composer, indicator, and dialog projections at their actual reader boundaries.
  • Tie the projection transport to the authoritative Composer / ChatView prop contracts and perform explicit, compile-checked handoff at the two authorized Desktop leaves.
  • Keep WorkHub and Workbar isolated: their Composer / ChatView instances do not consume the Session Goal projections.
  • Remove Goal controller/model reads and Goal prop plumbing from AppShell; keep useGoalController out of the production barrel.
  • Prevent ownership return with unique-controller, unique-production-consumer, forbidden-hook, leaf-Omit, AppShell Hook-budget, and renderer-architecture guards.
  • Rebase onto 920d7142d and regenerate the Astryx 0.5.2 surface inventory while resolving the documentation conflict.

This intentionally changes one transient UI state: while the selected Session is not yet owner-backed by the hydrated catalog, the unusable Set Goal... entry stays hidden instead of briefly appearing and doing nothing. Goal business behavior otherwise remains in use-goal-controller.ts unchanged. Stabilizing the producer of reportError is left to #4315, where the duplication is visible.

Performance evidence

Measured on final head 3b47b2c35 against 920d7142d in a 12-Session fixture. Each workload ran legacy/scoped ownership as alternating pairs inside the same Electron process; root-mode changes and resynchronization stayed outside the measurement window. The controller-only workload used 6 pairs and the complete goal.pause workload used 12. Renderer profiling sampled at 100 microseconds; the Fiber probe was disabled during each busy-JS window. Values below are conventional medians.

WorkloadLegacy ownerScoped ownerChangePaired result
Controller-only token update: rendered fibers62949-92.2%6/6 improved
Controller-only token update: renderer busy JS5.016 ms1.061 ms-78.9%6/6 improved
Controller-only token update: commits11unchanged6/6 tied
Complete goal.pause: renderer busy JS24.879 ms23.637 ms-5.0%9/12 improved
Complete goal.pause: summed rendered fibers3,053.53,063+0.3%6/12 improved
Complete goal.pause: commits6.57+7.7%2/12 improved, 4 tied

The controller-only result is strong and stable: both ranges are disjoint (629-629 vs 49-49 fibers; 4.199-5.633 vs 0.769-1.363 ms), and every pair improved.

The complete pause workload covers preload -> IPC -> Host -> broadcast -> get -> controller -> React. Its busy-JS ranges overlap (21.848-29.949 vs 20.729-26.598 ms), while commits and fibers did not improve. The performance claim is therefore limited to controller-owned renderer work. The complete-path result is directional only and does not establish an end-to-end pause speedup; renderer busy JS also excludes Electron main-process CPU and I/O.

The temporary dual-owner switch, probes, performance spec, results, and worktrees were removed after measurement and are not part of this PR.

Verification

  • npm run rebuild - passed, including the production renderer build.
  • npm --workspace @maka/desktop run test:dist - 1,845/1,845 passed.
  • npm exec -w @maka/desktop -- playwright test --config e2e/playwright.config.ts e2e/goal-dialog-budget.spec.ts --workers=1 --reporter=line - 1/1 passed.
  • npm run typecheck - passed.
  • npm run lint and npm run format:check - passed (3,055 linted / 1,833 formatted files).
  • npx knip --workspace @maka/desktop --no-progress and npx knip --workspace packages/ui --no-progress - passed.
  • npm run check:app-shell-hooks - passed at 42 Hooks / 80 call sites.
  • npm run check:renderer-architecture -- --base 920d7142d001e81868bcd50a28a5bb3898415e3b - 61/61 fixtures passed.
  • npm run astryx:surface-inventory - passed at 235 files / 1 exclusion.
  • NODE_NO_WARNINGS=1 npm --workspace @maka/storage run test:dist - 1,077 passed / 8 skipped.
  • git range-diff, git diff --check upstream/main...HEAD, git merge-base --is-ancestor upstream/main HEAD, and git merge-tree --write-tree --messages upstream/main HEAD - passed.

Screenshots are not applicable; the only visible change removes a transient unusable menu entry during catalog hydration.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex - renderer architecture, implementation, tests, performance measurement, review handling, conflict resolution, and documentation.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes - described under Summary above
  • No

@github-actionsgithub-actionsBot added the effort/L Under 1000 readable lines label Aug 31, 2026
@chihumyum

Copy link
Copy Markdown
ContributorAuthor

@Astro-Han Hi, can you check this out? Thanks a lot

@chihumyum
chihumyumforce-pushed the refactor/goals-controller-scope branch from 2727ac3 to 25f20ecCompareSeptember 1, 2026 05:58

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this lands cleanly, and the performance section is the most disciplined I've read here in a while. It reports that the complete goal.pause path did not improve (overlapping busy-JS ranges, commits and fibers slightly worse) and explicitly limits the claim to controller-owned render scope. That honesty is what makes the controller-only result — 629 → 63 fibers, 6.6ms → 1.6ms busy JS, disjoint ranges, 6/6 paired — actually persuasive.

The design is right too. Three separate contexts rather than one is exactly what earns the reduction: a composer-only change must not wake the indicator. And passing children through untouched so React bails on the unrelated frame is the correct way to scope this without widening Goal state back to the root.

Reviewed at head 25f20ec6. Approving. One P2 I'd like fixed here, two P3s that don't need to be.

P2 — the cloneElement injection gives up a compile-time contract, and this PR is defining a pattern that will be copied.

GoalComposerTargetProps and GoalIndicatorTargetProps declare every injected prop optional, so ReactElement<GoalComposerTargetProps> is satisfied by any element — the compiler cannot tell that the child is the one that actually reads these props. onSetGoal originates in packages/ui/src/composer.tsx:449 and reaches the child through ChatComposerRegionProps extends Omit<ComponentProps<typeof Composer>, …>, so a rename or removal on that cross-package contract would silently turn the injection into a dead prop with no type error. Before this change, onSetGoal={…} at the JSX site was checked.

Today the hole is covered by goal-dialog-budget.spec.ts — the 设定 Goal… menu item renders only when props.onSetGoal is truthy (composer.tsx:1916), so a broken injection fails a real end-to-end path. That is genuinely good coverage and it's why this isn't blocking. But a compile error is cheaper than an Electron run, and #4315 is already reproducing this provider-plus-boundary shape; every further controller that moves below AppShell will copy it. Cheapest fix: require the injected keys in the target-props types, or type the child as ReactElement<ComponentProps<typeof ChatComposerRegion>>.

P3-1 — the composer's enablement gate changed from activeId to ownerActiveId. Old: activeId && localInteractionAvailable. New: canOpenDialog && activeSessionId, where activeSessionId is ownerActiveId = activeCatalogSession && !sharedSessionActive ? activeId : undefined (app-shell.tsx:367). When activeId is set but the session isn't in the catalog yet, the old code rendered an enabled 设定 Goal… whose openDialog returns early on an empty activeSessionId (use-goal-controller.ts:216); the new gate hides the entry instead. The new behavior is better — a dead menu item is worse than none — but the PR says "No intended user-visible behavior change". No code change needed; I'll say so in the squash message unless it was unintended, in which case restoring activeId is the fix.

P3-2 — the provider re-stabilizes reportError because the producer is unstable.showSessionError is a plain function declaration inside AppShellContent (app-shell.tsx:2620), so it gets a fresh identity every render, which is why GoalProvider needs the reportErrorRef + useLayoutEffect + useCallback trio. That's correct as written, but the workaround sits on the wrong side of the boundary, and #4315's provider repeats the same stabilization. If showSessionError's dependencies allow a useCallback, doing it once would let both providers take the prop directly. Out of scope here — worth settling on #4315, where the duplication becomes visible.

AI use: reviewed with Claude Code (diff reading, tracing the gate and prop-injection paths through AppShell, the goals controller, and the shared Composer contract). I verified ownerActiveId's definition, openDialog's early return, showSessionError's declaration form, and the E2E path that exercises the composer injection myself against the head SHA.

Move production Goal controller ownership into GoalProvider and expose reader-local projections for the composer, indicator, and dialog host.
Keep AppShell free of Goal controller and model ownership, route composer and indicator state through separate UI-owned contexts consumed only by the authorized Desktop leaves, bind them at the authoritative Composer and ChatView props, and lock the boundary with architecture and render-scope coverage.
The Set Goal entry now waits for an owner-backed active session instead of briefly exposing an unusable action before catalog hydration.
Generated-by: Codex
@chihumyum
chihumyumforce-pushed the refactor/goals-controller-scope branch from 25f20ec to 3b47b2cCompareSeptember 1, 2026 09:14
@chihumyum

Copy link
Copy Markdown
ContributorAuthor

Addressed the review feedback in 3b47b2c35:

  • P2 fixed: removed the structurally loose cloneElement injection. The two required projection transport shapes now derive from the authoritative Composer / ChatView prop contracts, and the authorized Desktop leaves hand the values to the real JSX props explicitly. Production-consumer uniqueness, hook absence, leaf Omits, and AppShell exclusion are guarded so WorkHub/Workbar cannot inherit the Session Goal later.
  • P3-1 documented as intentional: the unusable Set Goal... entry remains hidden until the selected Session is owner-backed after catalog hydration. The commit message, Summary, screenshot note, and behavior checkbox now say so.
  • P3-2 left to refactor(desktop): move Module Hub controller below AppShell #4315: producer-side reportError stabilization is unchanged here.

The branch is rebased onto 920d7142d; the Astryx inventory conflict was regenerated against 0.5.2 and passes at 235 files / 1 exclusion.

Final-head performance remeasurement preserves the bounded claim: controller-only rendered fibers 629 -> 49 (-92.2%) and renderer busy JS 5.016 -> 1.061 ms (-78.9%), both 6/6 paired wins with disjoint ranges. Full goal.pause busy JS was directional only (24.879 -> 23.637 ms, -5.0%, 9/12, overlapping ranges), while commits/fibers did not improve, so this is not presented as an end-to-end speedup.

Local final-head verification: rebuild/typecheck/lint/format/Knip passed; Desktop 1,845/1,845; storage 1,077 passed / 8 skipped; Goal E2E 1/1; renderer architecture 61/61; AppShell guard 42 Hooks / 80 call sites; clean merge-tree. Hosted CI is rerunning on the new head.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-verified on head 3b47b2c35. The cloneElement injection is gone and both projection shapes derive from the Composer / ChatView prop contracts, so the P2 is closed.

@Astro-Han
Astro-Han merged commit a5ede6b into apache:mainSep 1, 2026
3 checks passed
@chihumyum
chihumyum deleted the refactor/goals-controller-scope branch September 1, 2026 10:48
abhinav-phi pushed a commit to abhinav-phi/maka that referenced this pull request Sep 1, 2026
…che#4316)
AppShell owned the Goal controller, so every Goal state change re-rendered the
shell and, through it, the whole chat surface. The controller's own token update
re-rendered 629 fibers to repaint an indicator.
Move production ownership into `GoalProvider` and publish the composer,
indicator, and dialog projections separately, each at the boundary that actually
reads it. The two authorized Desktop leaves take their projection from a context
consumer and hand it to the real `Composer` / `ChatView` props, so the render
scope stops at the subtree instead of the shell. The transport shapes are
indexed off those components' own prop contracts, which makes the handoff
compile-checked rather than structural, and the leaf `Omit`s remove the props
from what the parent may pass at all. WorkHub and Workbar mount their own
`Composer` / `ChatView` without the Session Goal projections and are unaffected.
Goal business behavior stays in `use-goal-controller.ts`. AppShell loses its Goal
controller and model reads along with the Goal prop plumbing, and
`useGoalController` stays out of the production barrel. Unique-controller,
unique-production-consumer, forbidden-hook, leaf-`Omit`, AppShell Hook-budget,
and renderer-architecture guards keep ownership from returning.
Controller-owned renderer work improves decisively: rendered fibers 629 -> 49
(-92.2%) and renderer busy JS 5.016 -> 1.061 ms (-78.9%), every pair improved
with disjoint ranges. The complete `goal.pause` path is directional only
(-5.0% busy JS, overlapping ranges, commits and fibers flat), so this is not an
end-to-end pause speedup.
One transient UI state changes: while the selected Session is not yet
owner-backed by the hydrated catalog, the unusable "Set Goal..." entry stays
hidden instead of briefly appearing and doing nothing.
Stabilizing the producer of `reportError` is left to apache#4315, where the
duplication is visible.
Generated-by: Codex
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/LUnder 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@chihumyum@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(desktop): move Goal controller ownership below AppShell - #4316

Merged
Astro-Han merged 1 commit into
apache:mainfrom
chihumyum:refactor/goals-controller-scope
Sep 1, 2026
Merged

refactor(desktop): move Goal controller ownership below AppShell#4316
Astro-Han merged 1 commit into
apache:mainfrom
chihumyum:refactor/goals-controller-scope

Conversation

@chihumyum

@chihumyumchihumyum commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Move production Goal controller ownership out of AppShell and into GoalProvider.

  • Publish separate composer, indicator, and dialog projections at their actual reader boundaries.
  • Tie the projection transport to the authoritative Composer / ChatView prop contracts and perform explicit, compile-checked handoff at the two authorized Desktop leaves.
  • Keep WorkHub and Workbar isolated: their Composer / ChatView instances do not consume the Session Goal projections.
  • Remove Goal controller/model reads and Goal prop plumbing from AppShell; keep useGoalController out of the production barrel.
  • Prevent ownership return with unique-controller, unique-production-consumer, forbidden-hook, leaf-Omit, AppShell Hook-budget, and renderer-architecture guards.
  • Rebase onto 920d7142d and regenerate the Astryx 0.5.2 surface inventory while resolving the documentation conflict.

This intentionally changes one transient UI state: while the selected Session is not yet owner-backed by the hydrated catalog, the unusable Set Goal... entry stays hidden instead of briefly appearing and doing nothing. Goal business behavior otherwise remains in use-goal-controller.ts unchanged. Stabilizing the producer of reportError is left to #4315, where the duplication is visible.

Performance evidence

Measured on final head 3b47b2c35 against 920d7142d in a 12-Session fixture. Each workload ran legacy/scoped ownership as alternating pairs inside the same Electron process; root-mode changes and resynchronization stayed outside the measurement window. The controller-only workload used 6 pairs and the complete goal.pause workload used 12. Renderer profiling sampled at 100 microseconds; the Fiber probe was disabled during each busy-JS window. Values below are conventional medians.

WorkloadLegacy ownerScoped ownerChangePaired result
Controller-only token update: rendered fibers62949-92.2%6/6 improved
Controller-only token update: renderer busy JS5.016 ms1.061 ms-78.9%6/6 improved
Controller-only token update: commits11unchanged6/6 tied
Complete goal.pause: renderer busy JS24.879 ms23.637 ms-5.0%9/12 improved
Complete goal.pause: summed rendered fibers3,053.53,063+0.3%6/12 improved
Complete goal.pause: commits6.57+7.7%2/12 improved, 4 tied

The controller-only result is strong and stable: both ranges are disjoint (629-629 vs 49-49 fibers; 4.199-5.633 vs 0.769-1.363 ms), and every pair improved.

The complete pause workload covers preload -> IPC -> Host -> broadcast -> get -> controller -> React. Its busy-JS ranges overlap (21.848-29.949 vs 20.729-26.598 ms), while commits and fibers did not improve. The performance claim is therefore limited to controller-owned renderer work. The complete-path result is directional only and does not establish an end-to-end pause speedup; renderer busy JS also excludes Electron main-process CPU and I/O.

The temporary dual-owner switch, probes, performance spec, results, and worktrees were removed after measurement and are not part of this PR.

Verification

  • npm run rebuild - passed, including the production renderer build.
  • npm --workspace @maka/desktop run test:dist - 1,845/1,845 passed.
  • npm exec -w @maka/desktop -- playwright test --config e2e/playwright.config.ts e2e/goal-dialog-budget.spec.ts --workers=1 --reporter=line - 1/1 passed.
  • npm run typecheck - passed.
  • npm run lint and npm run format:check - passed (3,055 linted / 1,833 formatted files).
  • npx knip --workspace @maka/desktop --no-progress and npx knip --workspace packages/ui --no-progress - passed.
  • npm run check:app-shell-hooks - passed at 42 Hooks / 80 call sites.
  • npm run check:renderer-architecture -- --base 920d7142d001e81868bcd50a28a5bb3898415e3b - 61/61 fixtures passed.
  • npm run astryx:surface-inventory - passed at 235 files / 1 exclusion.
  • NODE_NO_WARNINGS=1 npm --workspace @maka/storage run test:dist - 1,077 passed / 8 skipped.
  • git range-diff, git diff --check upstream/main...HEAD, git merge-base --is-ancestor upstream/main HEAD, and git merge-tree --write-tree --messages upstream/main HEAD - passed.

Screenshots are not applicable; the only visible change removes a transient unusable menu entry during catalog hydration.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex - renderer architecture, implementation, tests, performance measurement, review handling, conflict resolution, and documentation.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes - described under Summary above
  • No

@github-actionsgithub-actionsBot added the effort/L Under 1000 readable lines label Aug 31, 2026
@chihumyum

Copy link
Copy Markdown
ContributorAuthor

@Astro-Han Hi, can you check this out? Thanks a lot

@chihumyum
chihumyumforce-pushed the refactor/goals-controller-scope branch from 2727ac3 to 25f20ecCompareSeptember 1, 2026 05:58

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this lands cleanly, and the performance section is the most disciplined I've read here in a while. It reports that the complete goal.pause path did not improve (overlapping busy-JS ranges, commits and fibers slightly worse) and explicitly limits the claim to controller-owned render scope. That honesty is what makes the controller-only result — 629 → 63 fibers, 6.6ms → 1.6ms busy JS, disjoint ranges, 6/6 paired — actually persuasive.

The design is right too. Three separate contexts rather than one is exactly what earns the reduction: a composer-only change must not wake the indicator. And passing children through untouched so React bails on the unrelated frame is the correct way to scope this without widening Goal state back to the root.

Reviewed at head 25f20ec6. Approving. One P2 I'd like fixed here, two P3s that don't need to be.

P2 — the cloneElement injection gives up a compile-time contract, and this PR is defining a pattern that will be copied.

GoalComposerTargetProps and GoalIndicatorTargetProps declare every injected prop optional, so ReactElement<GoalComposerTargetProps> is satisfied by any element — the compiler cannot tell that the child is the one that actually reads these props. onSetGoal originates in packages/ui/src/composer.tsx:449 and reaches the child through ChatComposerRegionProps extends Omit<ComponentProps<typeof Composer>, …>, so a rename or removal on that cross-package contract would silently turn the injection into a dead prop with no type error. Before this change, onSetGoal={…} at the JSX site was checked.

Today the hole is covered by goal-dialog-budget.spec.ts — the 设定 Goal… menu item renders only when props.onSetGoal is truthy (composer.tsx:1916), so a broken injection fails a real end-to-end path. That is genuinely good coverage and it's why this isn't blocking. But a compile error is cheaper than an Electron run, and #4315 is already reproducing this provider-plus-boundary shape; every further controller that moves below AppShell will copy it. Cheapest fix: require the injected keys in the target-props types, or type the child as ReactElement<ComponentProps<typeof ChatComposerRegion>>.

P3-1 — the composer's enablement gate changed from activeId to ownerActiveId. Old: activeId && localInteractionAvailable. New: canOpenDialog && activeSessionId, where activeSessionId is ownerActiveId = activeCatalogSession && !sharedSessionActive ? activeId : undefined (app-shell.tsx:367). When activeId is set but the session isn't in the catalog yet, the old code rendered an enabled 设定 Goal… whose openDialog returns early on an empty activeSessionId (use-goal-controller.ts:216); the new gate hides the entry instead. The new behavior is better — a dead menu item is worse than none — but the PR says "No intended user-visible behavior change". No code change needed; I'll say so in the squash message unless it was unintended, in which case restoring activeId is the fix.

P3-2 — the provider re-stabilizes reportError because the producer is unstable.showSessionError is a plain function declaration inside AppShellContent (app-shell.tsx:2620), so it gets a fresh identity every render, which is why GoalProvider needs the reportErrorRef + useLayoutEffect + useCallback trio. That's correct as written, but the workaround sits on the wrong side of the boundary, and #4315's provider repeats the same stabilization. If showSessionError's dependencies allow a useCallback, doing it once would let both providers take the prop directly. Out of scope here — worth settling on #4315, where the duplication becomes visible.

AI use: reviewed with Claude Code (diff reading, tracing the gate and prop-injection paths through AppShell, the goals controller, and the shared Composer contract). I verified ownerActiveId's definition, openDialog's early return, showSessionError's declaration form, and the E2E path that exercises the composer injection myself against the head SHA.

Move production Goal controller ownership into GoalProvider and expose reader-local projections for the composer, indicator, and dialog host.
Keep AppShell free of Goal controller and model ownership, route composer and indicator state through separate UI-owned contexts consumed only by the authorized Desktop leaves, bind them at the authoritative Composer and ChatView props, and lock the boundary with architecture and render-scope coverage.
The Set Goal entry now waits for an owner-backed active session instead of briefly exposing an unusable action before catalog hydration.
Generated-by: Codex
@chihumyum
chihumyumforce-pushed the refactor/goals-controller-scope branch from 25f20ec to 3b47b2cCompareSeptember 1, 2026 09:14
@chihumyum

Copy link
Copy Markdown
ContributorAuthor

Addressed the review feedback in 3b47b2c35:

  • P2 fixed: removed the structurally loose cloneElement injection. The two required projection transport shapes now derive from the authoritative Composer / ChatView prop contracts, and the authorized Desktop leaves hand the values to the real JSX props explicitly. Production-consumer uniqueness, hook absence, leaf Omits, and AppShell exclusion are guarded so WorkHub/Workbar cannot inherit the Session Goal later.
  • P3-1 documented as intentional: the unusable Set Goal... entry remains hidden until the selected Session is owner-backed after catalog hydration. The commit message, Summary, screenshot note, and behavior checkbox now say so.
  • P3-2 left to refactor(desktop): move Module Hub controller below AppShell #4315: producer-side reportError stabilization is unchanged here.

The branch is rebased onto 920d7142d; the Astryx inventory conflict was regenerated against 0.5.2 and passes at 235 files / 1 exclusion.

Final-head performance remeasurement preserves the bounded claim: controller-only rendered fibers 629 -> 49 (-92.2%) and renderer busy JS 5.016 -> 1.061 ms (-78.9%), both 6/6 paired wins with disjoint ranges. Full goal.pause busy JS was directional only (24.879 -> 23.637 ms, -5.0%, 9/12, overlapping ranges), while commits/fibers did not improve, so this is not presented as an end-to-end speedup.

Local final-head verification: rebuild/typecheck/lint/format/Knip passed; Desktop 1,845/1,845; storage 1,077 passed / 8 skipped; Goal E2E 1/1; renderer architecture 61/61; AppShell guard 42 Hooks / 80 call sites; clean merge-tree. Hosted CI is rerunning on the new head.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-verified on head 3b47b2c35. The cloneElement injection is gone and both projection shapes derive from the Composer / ChatView prop contracts, so the P2 is closed.

@Astro-Han
Astro-Han merged commit a5ede6b into apache:mainSep 1, 2026
3 checks passed
@chihumyum
chihumyum deleted the refactor/goals-controller-scope branch September 1, 2026 10:48
abhinav-phi pushed a commit to abhinav-phi/maka that referenced this pull request Sep 1, 2026
…che#4316)
AppShell owned the Goal controller, so every Goal state change re-rendered the
shell and, through it, the whole chat surface. The controller's own token update
re-rendered 629 fibers to repaint an indicator.
Move production ownership into `GoalProvider` and publish the composer,
indicator, and dialog projections separately, each at the boundary that actually
reads it. The two authorized Desktop leaves take their projection from a context
consumer and hand it to the real `Composer` / `ChatView` props, so the render
scope stops at the subtree instead of the shell. The transport shapes are
indexed off those components' own prop contracts, which makes the handoff
compile-checked rather than structural, and the leaf `Omit`s remove the props
from what the parent may pass at all. WorkHub and Workbar mount their own
`Composer` / `ChatView` without the Session Goal projections and are unaffected.
Goal business behavior stays in `use-goal-controller.ts`. AppShell loses its Goal
controller and model reads along with the Goal prop plumbing, and
`useGoalController` stays out of the production barrel. Unique-controller,
unique-production-consumer, forbidden-hook, leaf-`Omit`, AppShell Hook-budget,
and renderer-architecture guards keep ownership from returning.
Controller-owned renderer work improves decisively: rendered fibers 629 -> 49
(-92.2%) and renderer busy JS 5.016 -> 1.061 ms (-78.9%), every pair improved
with disjoint ranges. The complete `goal.pause` path is directional only
(-5.0% busy JS, overlapping ranges, commits and fibers flat), so this is not an
end-to-end pause speedup.
One transient UI state changes: while the selected Session is not yet
owner-backed by the hydrated catalog, the unusable "Set Goal..." entry stays
hidden instead of briefly appearing and doing nothing.
Stabilizing the producer of `reportError` is left to apache#4315, where the
duplication is visible.
Generated-by: Codex
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/LUnder 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@chihumyum@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

refactor(desktop): move Goal controller ownership below AppShell - #4316

Merged
Astro-Han merged 1 commit into
apache:mainfrom
chihumyum:refactor/goals-controller-scope
Sep 1, 2026
Merged

refactor(desktop): move Goal controller ownership below AppShell#4316
Astro-Han merged 1 commit into
apache:mainfrom
chihumyum:refactor/goals-controller-scope

Conversation

@chihumyum

@chihumyumchihumyum commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Move production Goal controller ownership out of AppShell and into GoalProvider.

  • Publish separate composer, indicator, and dialog projections at their actual reader boundaries.
  • Tie the projection transport to the authoritative Composer / ChatView prop contracts and perform explicit, compile-checked handoff at the two authorized Desktop leaves.
  • Keep WorkHub and Workbar isolated: their Composer / ChatView instances do not consume the Session Goal projections.
  • Remove Goal controller/model reads and Goal prop plumbing from AppShell; keep useGoalController out of the production barrel.
  • Prevent ownership return with unique-controller, unique-production-consumer, forbidden-hook, leaf-Omit, AppShell Hook-budget, and renderer-architecture guards.
  • Rebase onto 920d7142d and regenerate the Astryx 0.5.2 surface inventory while resolving the documentation conflict.

This intentionally changes one transient UI state: while the selected Session is not yet owner-backed by the hydrated catalog, the unusable Set Goal... entry stays hidden instead of briefly appearing and doing nothing. Goal business behavior otherwise remains in use-goal-controller.ts unchanged. Stabilizing the producer of reportError is left to #4315, where the duplication is visible.

Performance evidence

Measured on final head 3b47b2c35 against 920d7142d in a 12-Session fixture. Each workload ran legacy/scoped ownership as alternating pairs inside the same Electron process; root-mode changes and resynchronization stayed outside the measurement window. The controller-only workload used 6 pairs and the complete goal.pause workload used 12. Renderer profiling sampled at 100 microseconds; the Fiber probe was disabled during each busy-JS window. Values below are conventional medians.

WorkloadLegacy ownerScoped ownerChangePaired result
Controller-only token update: rendered fibers62949-92.2%6/6 improved
Controller-only token update: renderer busy JS5.016 ms1.061 ms-78.9%6/6 improved
Controller-only token update: commits11unchanged6/6 tied
Complete goal.pause: renderer busy JS24.879 ms23.637 ms-5.0%9/12 improved
Complete goal.pause: summed rendered fibers3,053.53,063+0.3%6/12 improved
Complete goal.pause: commits6.57+7.7%2/12 improved, 4 tied

The controller-only result is strong and stable: both ranges are disjoint (629-629 vs 49-49 fibers; 4.199-5.633 vs 0.769-1.363 ms), and every pair improved.

The complete pause workload covers preload -> IPC -> Host -> broadcast -> get -> controller -> React. Its busy-JS ranges overlap (21.848-29.949 vs 20.729-26.598 ms), while commits and fibers did not improve. The performance claim is therefore limited to controller-owned renderer work. The complete-path result is directional only and does not establish an end-to-end pause speedup; renderer busy JS also excludes Electron main-process CPU and I/O.

The temporary dual-owner switch, probes, performance spec, results, and worktrees were removed after measurement and are not part of this PR.

Verification

  • npm run rebuild - passed, including the production renderer build.
  • npm --workspace @maka/desktop run test:dist - 1,845/1,845 passed.
  • npm exec -w @maka/desktop -- playwright test --config e2e/playwright.config.ts e2e/goal-dialog-budget.spec.ts --workers=1 --reporter=line - 1/1 passed.
  • npm run typecheck - passed.
  • npm run lint and npm run format:check - passed (3,055 linted / 1,833 formatted files).
  • npx knip --workspace @maka/desktop --no-progress and npx knip --workspace packages/ui --no-progress - passed.
  • npm run check:app-shell-hooks - passed at 42 Hooks / 80 call sites.
  • npm run check:renderer-architecture -- --base 920d7142d001e81868bcd50a28a5bb3898415e3b - 61/61 fixtures passed.
  • npm run astryx:surface-inventory - passed at 235 files / 1 exclusion.
  • NODE_NO_WARNINGS=1 npm --workspace @maka/storage run test:dist - 1,077 passed / 8 skipped.
  • git range-diff, git diff --check upstream/main...HEAD, git merge-base --is-ancestor upstream/main HEAD, and git merge-tree --write-tree --messages upstream/main HEAD - passed.

Screenshots are not applicable; the only visible change removes a transient unusable menu entry during catalog hydration.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex - renderer architecture, implementation, tests, performance measurement, review handling, conflict resolution, and documentation.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes - described under Summary above
  • No

@github-actionsgithub-actionsBot added the effort/L Under 1000 readable lines label Aug 31, 2026
@chihumyum

Copy link
Copy Markdown
ContributorAuthor

@Astro-Han Hi, can you check this out? Thanks a lot

@chihumyum
chihumyumforce-pushed the refactor/goals-controller-scope branch from 2727ac3 to 25f20ecCompareSeptember 1, 2026 05:58

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this lands cleanly, and the performance section is the most disciplined I've read here in a while. It reports that the complete goal.pause path did not improve (overlapping busy-JS ranges, commits and fibers slightly worse) and explicitly limits the claim to controller-owned render scope. That honesty is what makes the controller-only result — 629 → 63 fibers, 6.6ms → 1.6ms busy JS, disjoint ranges, 6/6 paired — actually persuasive.

The design is right too. Three separate contexts rather than one is exactly what earns the reduction: a composer-only change must not wake the indicator. And passing children through untouched so React bails on the unrelated frame is the correct way to scope this without widening Goal state back to the root.

Reviewed at head 25f20ec6. Approving. One P2 I'd like fixed here, two P3s that don't need to be.

P2 — the cloneElement injection gives up a compile-time contract, and this PR is defining a pattern that will be copied.

GoalComposerTargetProps and GoalIndicatorTargetProps declare every injected prop optional, so ReactElement<GoalComposerTargetProps> is satisfied by any element — the compiler cannot tell that the child is the one that actually reads these props. onSetGoal originates in packages/ui/src/composer.tsx:449 and reaches the child through ChatComposerRegionProps extends Omit<ComponentProps<typeof Composer>, …>, so a rename or removal on that cross-package contract would silently turn the injection into a dead prop with no type error. Before this change, onSetGoal={…} at the JSX site was checked.

Today the hole is covered by goal-dialog-budget.spec.ts — the 设定 Goal… menu item renders only when props.onSetGoal is truthy (composer.tsx:1916), so a broken injection fails a real end-to-end path. That is genuinely good coverage and it's why this isn't blocking. But a compile error is cheaper than an Electron run, and #4315 is already reproducing this provider-plus-boundary shape; every further controller that moves below AppShell will copy it. Cheapest fix: require the injected keys in the target-props types, or type the child as ReactElement<ComponentProps<typeof ChatComposerRegion>>.

P3-1 — the composer's enablement gate changed from activeId to ownerActiveId. Old: activeId && localInteractionAvailable. New: canOpenDialog && activeSessionId, where activeSessionId is ownerActiveId = activeCatalogSession && !sharedSessionActive ? activeId : undefined (app-shell.tsx:367). When activeId is set but the session isn't in the catalog yet, the old code rendered an enabled 设定 Goal… whose openDialog returns early on an empty activeSessionId (use-goal-controller.ts:216); the new gate hides the entry instead. The new behavior is better — a dead menu item is worse than none — but the PR says "No intended user-visible behavior change". No code change needed; I'll say so in the squash message unless it was unintended, in which case restoring activeId is the fix.

P3-2 — the provider re-stabilizes reportError because the producer is unstable.showSessionError is a plain function declaration inside AppShellContent (app-shell.tsx:2620), so it gets a fresh identity every render, which is why GoalProvider needs the reportErrorRef + useLayoutEffect + useCallback trio. That's correct as written, but the workaround sits on the wrong side of the boundary, and #4315's provider repeats the same stabilization. If showSessionError's dependencies allow a useCallback, doing it once would let both providers take the prop directly. Out of scope here — worth settling on #4315, where the duplication becomes visible.

AI use: reviewed with Claude Code (diff reading, tracing the gate and prop-injection paths through AppShell, the goals controller, and the shared Composer contract). I verified ownerActiveId's definition, openDialog's early return, showSessionError's declaration form, and the E2E path that exercises the composer injection myself against the head SHA.

Move production Goal controller ownership into GoalProvider and expose reader-local projections for the composer, indicator, and dialog host.
Keep AppShell free of Goal controller and model ownership, route composer and indicator state through separate UI-owned contexts consumed only by the authorized Desktop leaves, bind them at the authoritative Composer and ChatView props, and lock the boundary with architecture and render-scope coverage.
The Set Goal entry now waits for an owner-backed active session instead of briefly exposing an unusable action before catalog hydration.
Generated-by: Codex
@chihumyum
chihumyumforce-pushed the refactor/goals-controller-scope branch from 25f20ec to 3b47b2cCompareSeptember 1, 2026 09:14
@chihumyum

Copy link
Copy Markdown
ContributorAuthor

Addressed the review feedback in 3b47b2c35:

  • P2 fixed: removed the structurally loose cloneElement injection. The two required projection transport shapes now derive from the authoritative Composer / ChatView prop contracts, and the authorized Desktop leaves hand the values to the real JSX props explicitly. Production-consumer uniqueness, hook absence, leaf Omits, and AppShell exclusion are guarded so WorkHub/Workbar cannot inherit the Session Goal later.
  • P3-1 documented as intentional: the unusable Set Goal... entry remains hidden until the selected Session is owner-backed after catalog hydration. The commit message, Summary, screenshot note, and behavior checkbox now say so.
  • P3-2 left to refactor(desktop): move Module Hub controller below AppShell #4315: producer-side reportError stabilization is unchanged here.

The branch is rebased onto 920d7142d; the Astryx inventory conflict was regenerated against 0.5.2 and passes at 235 files / 1 exclusion.

Final-head performance remeasurement preserves the bounded claim: controller-only rendered fibers 629 -> 49 (-92.2%) and renderer busy JS 5.016 -> 1.061 ms (-78.9%), both 6/6 paired wins with disjoint ranges. Full goal.pause busy JS was directional only (24.879 -> 23.637 ms, -5.0%, 9/12, overlapping ranges), while commits/fibers did not improve, so this is not presented as an end-to-end speedup.

Local final-head verification: rebuild/typecheck/lint/format/Knip passed; Desktop 1,845/1,845; storage 1,077 passed / 8 skipped; Goal E2E 1/1; renderer architecture 61/61; AppShell guard 42 Hooks / 80 call sites; clean merge-tree. Hosted CI is rerunning on the new head.

@Astro-HanAstro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-verified on head 3b47b2c35. The cloneElement injection is gone and both projection shapes derive from the Composer / ChatView prop contracts, so the P2 is closed.

@Astro-Han
Astro-Han merged commit a5ede6b into apache:mainSep 1, 2026
3 checks passed
@chihumyum
chihumyum deleted the refactor/goals-controller-scope branch September 1, 2026 10:48
abhinav-phi pushed a commit to abhinav-phi/maka that referenced this pull request Sep 1, 2026
…che#4316)
AppShell owned the Goal controller, so every Goal state change re-rendered the
shell and, through it, the whole chat surface. The controller's own token update
re-rendered 629 fibers to repaint an indicator.
Move production ownership into `GoalProvider` and publish the composer,
indicator, and dialog projections separately, each at the boundary that actually
reads it. The two authorized Desktop leaves take their projection from a context
consumer and hand it to the real `Composer` / `ChatView` props, so the render
scope stops at the subtree instead of the shell. The transport shapes are
indexed off those components' own prop contracts, which makes the handoff
compile-checked rather than structural, and the leaf `Omit`s remove the props
from what the parent may pass at all. WorkHub and Workbar mount their own
`Composer` / `ChatView` without the Session Goal projections and are unaffected.
Goal business behavior stays in `use-goal-controller.ts`. AppShell loses its Goal
controller and model reads along with the Goal prop plumbing, and
`useGoalController` stays out of the production barrel. Unique-controller,
unique-production-consumer, forbidden-hook, leaf-`Omit`, AppShell Hook-budget,
and renderer-architecture guards keep ownership from returning.
Controller-owned renderer work improves decisively: rendered fibers 629 -> 49
(-92.2%) and renderer busy JS 5.016 -> 1.061 ms (-78.9%), every pair improved
with disjoint ranges. The complete `goal.pause` path is directional only
(-5.0% busy JS, overlapping ranges, commits and fibers flat), so this is not an
end-to-end pause speedup.
One transient UI state changes: while the selected Session is not yet
owner-backed by the hydrated catalog, the unusable "Set Goal..." entry stays
hidden instead of briefly appearing and doing nothing.
Stabilizing the producer of `reportError` is left to apache#4315, where the
duplication is visible.
Generated-by: Codex
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/LUnder 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@chihumyum@Astro-Han