fix(deps,cli): add @types/react overrides + move refreshStatic out of setCurrentModel updater

Two follow-ups from the multi-round audit of the ink 7.0.3 re-upgrade:

1. @types/react / @types/react-dom now pinned to ^19.2.0 in root
   overrides. packages/web-templates still declares @types/react ^18.2.0
   in its devDeps. Today the CLI build is unaffected (web-templates's
   18.x types are nested in its own node_modules and the React-using
   src/insight and src/export-html files are excluded from its tsconfig
   build), but a future reincludes-or-hoist accident would land
   conflicting global JSX namespaces in the CLI compile graph. Match
   the dep dedup we already enforce for `react` and `react-dom` so the
   type graph stays as deduped as the runtime graph.

2. AppContainer's onModelChange handler was calling refreshStatic() as
   a side-effect inside the setCurrentModel updater. React.StrictMode
   double-invokes state updaters in dev, so model swaps fired two
   clearTerminal writes + two <Static> key bumps. The double work was
   masked under ink 6 (key changes were no-ops on <Static>), but ink
   7.0.3 honors key changes — the doubled work is now potentially
   visible as a faster flash-flash on every model switch.

   Refactor: setCurrentModel becomes a pure setter; refreshStatic
   moves into a useEffect keyed on currentModel with a ref-comparison
   guard so the first render doesn't fire. Single clearTerminal write
   per real model change, even under StrictMode.

Verified: npm ls ink → single 7.0.3, npm ls react → single 19.2.4,
npm ls @types/react → 19.2.10 hoisted (npm flags web-templates's 18.x
constraint as overridden, which is the intended behavior). Typecheck
clean across cli + core workspaces.
This commit is contained in:
秦奇 2026-05-14 10:02:56 +08:00
parent 5bb49d945d
commit b25831b0ed
2 changed files with 18 additions and 10 deletions

View file

@ -81,7 +81,9 @@
"baseline-browser-mapping": "^2.9.19",
"normalize-package-data": "^7.0.1",
"react": "^19.2.4",
"react-dom": "^19.2.4"
"react-dom": "^19.2.4",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0"
},
"bin": {
"qwen": "dist/cli.js"

View file

@ -591,19 +591,25 @@ export const AppContainer = (props: AppContainerProps) => {
// Keep the static header in sync with model changes without polling.
// Ink's <Static> output is append-only, so model changes must explicitly
// clear and remount the static region to redraw the banner at the top.
//
// refreshStatic() is fired from an effect (not inside the setState updater)
// so React.StrictMode's double-invoke of state updaters doesn't translate
// into two clearTerminal writes per model change. Side-effects belong in
// effects; the updater stays pure.
useEffect(() => {
const unsubscribe = config.onModelChange((model) => {
setCurrentModel((prev) => {
if (prev === model) {
return prev;
}
refreshStatic();
return model;
});
setCurrentModel(model);
});
return unsubscribe;
}, [config, refreshStatic]);
}, [config]);
const prevCurrentModelRef = useRef(currentModel);
useEffect(() => {
if (prevCurrentModelRef.current !== currentModel) {
prevCurrentModelRef.current = currentModel;
refreshStatic();
}
}, [currentModel, refreshStatic]);
const {
isThemeDialogOpen,