功能:DirectProject 聊天控制器暴露更早历史的加载态、失败态与重试入口

控制器新增 historyLoading / historyError 两个可渲染状态,与并发判据用的 ref 同步镜像。
loadEarlierHistory 失败时写入 historyError 并挂起自动加载,成功路径清掉失败记录。
新增 retryEarlierHistory:先解挂起再走同一条加载入口,供内联错误行的重试按钮调用。
加载失败不再写顶部状态行(状态行留给首屏读取失败等其它用途)。

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
2026-10-02 17:42:14 +08:00
parent f0ae0bd5d7
commit 72ad9b8cdb
@@ -137,7 +137,8 @@ export type DirectProjectChatControllerProps = {
*
* 状态变量归属:reducer 的回合字段、待发消息投影、收口计数与 `history` / `live` 只由
* `directThreadChat.ts` 写;本文件的 `commandInFlight`(唯一入口 `beginCommandInFlight` /
* `endCommandInFlight`,只覆盖 IPC 在飞)、`localMessages`、埋点句柄与分页 ref 只服务发送与展示;
* `endCommandInFlight`,只覆盖 IPC 在飞)、`localMessages`、分页 ref 与更早历史的
* `historyLoading` / `historyError`(渲染层读的加载行与内联错误行判据)只服务发送与展示;
* 界面上的「这一轮在跑吗」只有一个派生入口 `useDirectProjectTurnStatus()`,两态判据与真值表在
* `../conversation/directTurnPresentation.ts` 的 `DirectChatTurnState`,渲染时否定式读
* `state !== 'finished'`、肯定式读 `state === 'running'`(见 `DirectProjectTurn.tsx`)。
@@ -182,8 +183,17 @@ export function useDirectProjectChatController({
*/
const pendingTurns = directThread.state.pendingTurns;
const [historyHasMore, setHistoryHasMore] = useState(false);
const historyOldestItemIdRef = useRef<string | null>(null);
/**
* 更早历史的加载态与失败态:渲染层要拿它决定加载行与内联错误行的显隐。
*
* `historyLoadingRef` 是同一件事的同步副本:并发判据必须在同一次事件处理里立刻生效,
* 而 `setState` 要等下一次渲染,直接用 state 判会放过第二次触发。
*/
const [historyLoading, setHistoryLoading] = useState(false);
const [historyError, setHistoryError] = useState<string | null>(null);
const historyLoadingRef = useRef(false);
const historyErrorRef = useRef<string | null>(null);
const historyOldestItemIdRef = useRef<string | null>(null);
const projectPathRef = useRef<string | null>(projectPath);
projectPathRef.current = projectPath;
const directTurnRunningRef = useRef(currentTurnRunning);
@@ -196,6 +206,10 @@ export function useDirectProjectChatController({
setStatusNotice('');
setLocalMessages([]);
setHistoryHasMore(false);
setHistoryLoading(false);
historyLoadingRef.current = false;
historyErrorRef.current = null;
setHistoryError(null);
historyOldestItemIdRef.current = null;
}, [projectPath]);
@@ -649,18 +663,27 @@ export function useDirectProjectChatController({
}
}
/**
* 自动加载更早历史(滚动触顶与填充视口共用这一条入口)。
*
* 失败即挂起:`historyErrorRef` 非空期间这里直接返回,只有手动重试(先清掉错误)或切换
* 项目才会重新开始。挂起态一定要能渲染出来——原来的常驻按钮删掉之后,内联错误行是用户
* 唯一还能自己补救的入口。
*/
async function loadEarlierHistory() {
const nextProjectPath = projectPath;
if (
!enabled ||
!historyHasMore ||
!nextProjectPath ||
historyLoadingRef.current
historyLoadingRef.current ||
historyErrorRef.current
)
return;
const invoke = resolveTauriInvoke();
if (!invoke) return;
historyLoadingRef.current = true;
setHistoryLoading(true);
try {
const pages = await readDirectHistoryPages({
existingEntries: directEntries,
@@ -679,19 +702,27 @@ export function useDirectProjectChatController({
historyOldestItemIdRef.current =
pages.firstItemId ?? historyOldestItemIdRef.current;
if (pages.error) throw pages.error;
historyErrorRef.current = null;
setHistoryError(null);
} catch (error) {
// 与加载成功路径同样按项目守卫:切项目后在飞的读取失败不能算到新项目头上。
if (projectPathRef.current !== nextProjectPath) return;
setStatusNotice(
`读取更早的对话历史失败:${
error instanceof Error ? error.message : String(error)
}`,
);
const message = error instanceof Error ? error.message : String(error);
historyErrorRef.current = message;
setHistoryError(message);
} finally {
historyLoadingRef.current = false;
setHistoryLoading(false);
}
}
/** 手动重试更早历史:先解挂起,再走同一条加载入口。 */
function retryEarlierHistory() {
historyErrorRef.current = null;
setHistoryError(null);
void loadEarlierHistory();
}
return {
appendLocalMessage,
attachmentNotice,
@@ -702,8 +733,11 @@ export function useDirectProjectChatController({
directEntries,
directTurnRunning: currentTurnRunning,
directTurnStartedAt: currentTurnStartedAt,
historyError,
historyHasMore,
historyLoading,
loadEarlierHistory,
retryEarlierHistory,
commandInFlight,
localMessages,
pendingTurns,