Migrate the web app to Next 16, eslint-config-next 16, and TypeScript 6 - #332
Merged
Conversation
These three cannot land separately. TypeScript 6 fails under Next 15 because app/layout.tsx reports TS2882 on the side-effect import of globals.css, and it only typechecks once Next 16 is present. Next 16 in turn requires eslint-config-next 16, which ships native flat config, so routing it through FlatCompat now dies inside the eslintrc validator with a circular structure error. eslint.config.mjs imports eslint-config-next/core-web-vitals directly instead. That combination enables react-hooks/set-state-in-effect, which found 13 real violations across five components. All 13 are fixed on their merits using lazy useState initializers or guarded render-time state adjustment. No disable comment, rule downgrade, or lint-glob narrowing was added. Mount-path equivalence was checked per fix, since useEffect runs on mount and a render-time guard does not; in each case the initializer already holds the value the effect would have set. One behaviour difference remains and is deliberate. On the initial live mount the status line now reads "Loading live vNext workspace from the trusted API." for the whole request, where it previously flipped to "Refreshing live vNext workspace...". The spinner, aria-busy, and disabled states are unchanged because isRefreshing initializes from liveModeReady. Reporting a first load as a refresh was the older, weaker behaviour. Next 16 grows first-load JS by about 43.7 KB gzip on every route, which is framework weight rather than application code. The route budgets are raised by a uniform 44,000 bytes so the gate keeps its previous headroom instead of hiding the growth. Measured before and after, in gzip bytes: / 106247 -> 149940, headroom 13753 -> 14060 /continuity 113660 -> 156942, headroom 16340 -> 17058 /vnext 137901 -> 181747, headroom 17099 -> 17253 check-bundle-budget.mjs now reads either the legacy app build manifest or Next 16's .next/diagnostics/route-bundle-stats.json, so it works across both. The tsconfig jsx switch to react-jsx and the .next/dev/types include are both written by Next 16 itself during build, verified by reverting them and rebuilding, so they are kept rather than fought. Closes #300 Closes #239 Closes #214
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
This was referenced Jul 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces #300, #239 and #214, which cannot be merged separately.
Why one PR
app/layout.tsxraises TS2882 on the side-effect import ofglobals.css. It only typechecks once Next 16 is present.FlatCompatbridge dies in the eslintrc validator with a circular structure error.eslint.config.mjsnow importseslint-config-next/core-web-vitalsdirectly.react-hooks/set-state-in-effect, which surfaces 13 real violations.The 13 hook fixes
Fixed on their merits with lazy
useStateinitializers or guarded render-time state adjustment, acrossvnext-brain-workspace(7),calendar-event-ingest-form(2),gmail-message-ingest-form(2),approval-detail(1) andworkflow-memory-writeback-form(1).Zero suppressions: no disable comments, no rule downgrade, no lint-glob narrowing. The rule is enforced and
pnpm lintis clean.Mount-path equivalence was checked per fix, because
useEffectfires on mount and a render-time guard does not. In each case the initializer already holds the value the effect would have set.Behaviour
One deliberate difference. On initial live mount the status line stays "Loading live vNext workspace from the trusted API." for the whole request instead of flipping to "Refreshing live vNext workspace...". The spinner,
aria-busyand disabled states are unchanged becauseisRefreshinginitializes fromliveModeReady. Reporting a first load as a refresh was the weaker prior behaviour.No existing test assertion was deleted, softened or retargeted. One assertion was extracted to a variable and is now asserted three times rather than once, proving the browser-clipper capability survives clip edits and URL changes.
Bundle budget
Next 16 adds about 43.7 KB gzip of framework weight to every route. Budgets are raised by a uniform 44,000 bytes so the gate keeps its previous headroom rather than concealing the growth. Measured by building both the base commit and this branch, in gzip bytes:
//continuity/vnextcheck-bundle-budget.mjsreads either the legacy app build manifest or Next 16's.next/diagnostics/route-bundle-stats.json.Generated config
The
tsconfig.jsonswitch to"jsx": "react-jsx"and the.next/dev/types/**/*.tsinclude are written by Next 16 itself during build. Verified by reverting both and rebuilding, which restores them. Kept rather than fought.Verification
All eight web gates run individually:
test,test:coverage:core,test:coverage:vnext,typecheck,lint,build,test:budget,test:advisory-audit. Both advisory audit invocations pass (93 production packages clean; 553 full, one live allowlisted exception, zero unexcepted at high). Full Python unit suite passes at 4102 passed and 2 skipped.Application versions stay at 0.14.0.
Known and out of scope
Next 16 warns that the
middlewarefile convention is deprecated in favour ofproxy. The build passes. Renaming it is not required by this migration and is left for a separate change.