Repository navigation
chore(deps): re-upgrade ink 6 → 7.0.3 (upstream Static remount fix landed) #4119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
5bb49d9
b25831b
e631f48
1266adc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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) { | ||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] Splitting the model-change handler into two effects causes progressive replay to be bypassed during model switches. Root cause: Impact: On sessions with >100 history items, every model switch triggers a full unframed
Suggested change
— DeepSeek/deepseek-v4-pro via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verdict: correct, real bug. Verified by tracing the four-step interleaving you described against the actual code:
So splitting the handler into two effects DID reintroduce the #3899 freeze on every model switch. My refactor was strictly worse: the StrictMode double-invoke concern was real but smaller than this regression. Fix in e631f48: kept the side-effect-out-of-updater principle but moved both const lastNotifiedModelRef = useRef(currentModel);
useEffect(() => {
const unsubscribe = config.onModelChange((model) => {
if (lastNotifiedModelRef.current === model) return;
lastNotifiedModelRef.current = model;
refreshStatic();
setCurrentModel(model);
});
return unsubscribe;
}, [config, refreshStatic]);This is the "third approach":
Your suggestion of Regression-pinned by:
Verified: Thanks for the careful read — this is a much better resting state than my previous refactor. |
||||||||||||||||||||||||||||||
| prevCurrentModelRef.current = currentModel; | ||||||||||||||||||||||||||||||
| refreshStatic(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }, [currentModel, refreshStatic]); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||||||
| isThemeDialogOpen, | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] The entire model-change-to-refreshStatic path has no test coverage.
AppContainer.test.tsxexercisesrefreshStaticvia manually exposedcapturedUIActions.refreshStatic(), which bypasses the new two-effect architecture entirely.Missing coverage:
refreshStatic(viaclearTerminal+historyRemountKeybump)refreshStaticclearTerminalConsider adding tests that spy on the
config.onModelChangecallback and verify the end-to-end path from model change notification to terminal output.— DeepSeek/deepseek-v4-pro via Qwen Code /review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in e631f48 by adding four focused tests in
AppContainer.test.tsxunder the newdescribe('Model change refreshStatic wiring', ...)block, plus one inMainContent.test.tsx:AppContainer.test.tsx — the harness captures
config.onModelChangelisteners via a direct method override (captureModelChangeListener), letting each test fire the listener and observe the synchronous side effect:mockStdout.writewas called withansiEscapes.clearTerminalSYNCHRONOUSLY (noflush/await) aftertrigger.notify('model-b'). Under the broken two-effect refactor this fails: the clearTerminal write is queued behind a React commit + useEffect tick. Under the corrected code it passes becauserefreshStatic()runs in-handler.clearTerminalis written andhistoryRemountKeyis untouched.unmount().MainContent.test.tsx — added "does NOT reset progressive replay when only currentModel changes (PR #4119 regression guard)" which directly pins the MainContent invariant: when
currentModelchanges buthistoryRemountKeyis held constant,replayCountmust NOT reset. This is the post-condition that the broken two-effect AppContainer code accidentally violated. Any future "split into two effects" refactor will trip this assertion the moment someone correctly drives the reset off the model dimension instead.The new AppContainer tests are intentionally end-to-end on the
config.onModelChangenotification path (your original ask) rather than going throughcapturedUIActions.refreshStatic()— so we're now exercising the same code path a real model swap (e.g. VLM auto-switch, fallback,/modelcommand) would hit.Verified: 82/82 tests green in
AppContainer.test.tsx+MainContent.test.tsx.