修复:更早历史的世代号在渲染期推进,切换项目的守卫不等 effect

- 换项目时 historyLoadTokenRef 与 projectPathRef 同一处推进,复位 effect 不再推进
- 旧读取在「切换提交完成、复位 effect 未跑」的窗口落地时立刻失效,不再合并条目或写游标
- ADR 第 1 条与决策记录说明推进时机与其安全性依据(该窗口内不会新起带旧游标的读取)

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
2026-10-02 22:27:33 +08:00
parent 2b01476891
commit 0ae94caf16
3 changed files with 12 additions and 4 deletions
@@ -208,10 +208,18 @@ export function useDirectProjectChatController({
*
* 只比项目路径不够——A→B→A 之后在飞的旧 A 读取又落回同一个路径,会把新一代的加载态与游标
* 一起改掉。世代号让任何非最新一次读取在落地时整段失效(不合并、不写游标、不关加载态)。
*
* 换项目的推进放在**渲染期**,与 `projectPathRef` 同一处:守卫要在切换提交之后立刻生效。
* 放进下面的复位 effect 会晚一步——React 的 passive effect 走宏任务,promise 续体走微任务,
* 旧读取可能在「提交完成、复位 effect 还没跑」的窗口里落地,那时世代号还是旧的,于是照常合并
* 条目、写游标、关加载态(复位 effect 随后会清掉状态,但这仍是一次白跑的跨项目读取)。
*/
const historyLoadTokenRef = useRef(0);
const projectPathRef = useRef<string | null>(projectPath);
projectPathRef.current = projectPath;
if (projectPathRef.current !== projectPath) {
historyLoadTokenRef.current += 1;
projectPathRef.current = projectPath;
}
const directTurnRunningRef = useRef(currentTurnRunning);
directTurnRunningRef.current = currentTurnRunning;
const commandInFlightRef = useRef(commandInFlight);
@@ -227,8 +235,6 @@ export function useDirectProjectChatController({
historyErrorRef.current = null;
setHistoryError(null);
historyOldestItemIdRef.current = null;
// 换项目即换代:上一项目的在飞读取(停在旧世代号上)落地后整段失效。
historyLoadTokenRef.current += 1;
}, [projectPath]);
// 回合期间的平台会话保活由 Rust 持有:Direct 回合的占用登记时启动,占用释放(回合收口)即停止;