chore: fix exhaustive-deps lint warnings

This commit is contained in:
daggerhashimoto 2026-04-15 23:42:11 +02:00
parent 7700b1fd58
commit 4cf147f163
2 changed files with 140 additions and 107 deletions

View file

@ -144,6 +144,41 @@ export function ChatProvider({ children }: { children: ReactNode }) {
setStream: streamHook.setStream,
});
const {
loadHistory,
getAllMessages,
applyMessageWindow,
} = msgHook;
const {
triggerRecovery,
clearDisconnectState,
captureDisconnectState,
wasGeneratingOnDisconnect,
isRecoveryInFlight,
isRecoveryPending,
incrementGeneration,
getGeneration,
} = recoveryHook;
const {
lastEventTimestamp,
setProcessingStage,
setLastEventTimestamp,
setActivityLog,
addActivityEntry,
completeActivityEntry,
startThinking,
captureThinkingDuration,
scheduleStreamingUpdate,
clearStreamBuffer,
getThinkingDuration,
resetThinking,
} = streamHook;
const {
playCompletionPing,
resetPlayedSounds,
handleFinalTTS,
} = ttsHook;
// ─── Reset transient state on session switch ──────────────────────────────
useEffect(() => {
setIsGenerating(false);
@ -167,31 +202,29 @@ export function ChatProvider({ children }: { children: ReactNode }) {
const prevConnection = previousConnectionStateRef.current;
if (connectionState === 'connected') {
if (prevConnection === 'reconnecting' && recoveryHook.wasGeneratingOnDisconnect()) {
recoveryHook.triggerRecovery('reconnect');
if (prevConnection === 'reconnecting' && wasGeneratingOnDisconnect()) {
triggerRecovery('reconnect');
}
recoveryHook.clearDisconnectState();
clearDisconnectState();
}
if (connectionState === 'reconnecting' && prevConnection === 'connected') {
recoveryHook.captureDisconnectState();
captureDisconnectState();
}
previousConnectionStateRef.current = connectionState;
}, [
connectionState,
currentSession,
msgHook.loadHistory,
recoveryHook.wasGeneratingOnDisconnect,
recoveryHook.triggerRecovery,
recoveryHook.clearDisconnectState,
recoveryHook.captureDisconnectState,
wasGeneratingOnDisconnect,
triggerRecovery,
clearDisconnectState,
captureDisconnectState,
]);
useEffect(() => {
if (connectionState !== 'connected' || !currentSession) return;
msgHook.loadHistory(currentSession);
}, [connectionState, currentSession, msgHook.loadHistory]);
loadHistory(currentSession);
}, [connectionState, currentSession, loadHistory]);
// ─── Periodic history poll for sub-agent sessions ─────────────────────────
const isSubagentSession = currentSession ? isSubagentSessionKey(currentSession) : false;
@ -212,14 +245,14 @@ export function ChatProvider({ children }: { children: ReactNode }) {
const sk = currentSessionRef.current;
const result = await loadChatHistory({ rpc, sessionKey: sk, limit: 500 });
if (sk !== currentSessionRef.current) return;
const prev = msgHook.getAllMessages();
const prev = getAllMessages();
if (
result.length === prev.length &&
result.length > 0 &&
result[result.length - 1]?.rawText === prev[prev.length - 1]?.rawText &&
result[result.length - 1]?.role === prev[prev.length - 1]?.role
) return;
msgHook.applyMessageWindow(result, false);
applyMessageWindow(result, false);
} catch { /* best-effort */ } finally {
subagentPollInFlightRef.current = false;
}
@ -229,26 +262,26 @@ export function ChatProvider({ children }: { children: ReactNode }) {
clearInterval(pollInterval);
subagentPollInFlightRef.current = false;
};
}, [isSubagentActive, connectionState, currentSession, rpc, msgHook.applyMessageWindow, msgHook.getAllMessages]);
}, [isSubagentActive, connectionState, currentSession, rpc, applyMessageWindow, getAllMessages]);
// ─── Watchdog: if stream stalls, recover once ─────────────────────────────
useEffect(() => {
if (!isGenerating || !streamHook.lastEventTimestamp) return;
if (!isGenerating || !lastEventTimestamp) return;
const timer = setTimeout(() => {
const elapsed = Date.now() - streamHook.lastEventTimestamp;
if (elapsed >= 12_000 && !recoveryHook.isRecoveryInFlight() && !recoveryHook.isRecoveryPending()) {
recoveryHook.triggerRecovery('chat-gap');
const elapsed = Date.now() - lastEventTimestamp;
if (elapsed >= 12_000 && !isRecoveryInFlight() && !isRecoveryPending()) {
triggerRecovery('chat-gap');
}
}, 12_000);
return () => clearTimeout(timer);
}, [
isGenerating,
streamHook.lastEventTimestamp,
recoveryHook.isRecoveryInFlight,
recoveryHook.isRecoveryPending,
recoveryHook.triggerRecovery,
lastEventTimestamp,
isRecoveryInFlight,
isRecoveryPending,
triggerRecovery,
]);
// ─── Subscribe to streaming events ────────────────────────────────────────
@ -258,7 +291,7 @@ export function ChatProvider({ children }: { children: ReactNode }) {
const triggerRecoveryOnce = (reason: RecoveryReason) => {
if (recoveryTriggeredThisEvent) return;
recoveryTriggeredThisEvent = true;
recoveryHook.triggerRecovery(reason);
triggerRecovery(reason);
};
const classified = classifyStreamEvent(msg);
@ -273,7 +306,7 @@ export function ChatProvider({ children }: { children: ReactNode }) {
isRootChildSession(classified.sessionKey, getRootAgentSessionKey(currentSk) || currentSk) &&
(classified.type === 'chat_final' || classified.type === 'lifecycle_end')
) {
recoveryHook.triggerRecovery('subagent-complete');
triggerRecovery('subagent-complete');
}
return;
}
@ -294,32 +327,32 @@ export function ChatProvider({ children }: { children: ReactNode }) {
if (type === 'lifecycle_start') {
setIsGenerating(true);
streamHook.setProcessingStage('thinking');
streamHook.setLastEventTimestamp(Date.now());
setProcessingStage('thinking');
setLastEventTimestamp(Date.now());
return;
}
if (type === 'lifecycle_end') {
setIsGenerating(false);
streamHook.setProcessingStage(null);
streamHook.setActivityLog([]);
streamHook.setLastEventTimestamp(0);
ttsHook.playCompletionPing();
setProcessingStage(null);
setActivityLog([]);
setLastEventTimestamp(0);
playCompletionPing();
recoveryHook.incrementGeneration();
incrementGeneration();
const activeRun = activeRunIdRef.current;
const runFinalized = activeRun ? runsRef.current.get(activeRun)?.finalized : false;
if (!runFinalized) {
recoveryHook.triggerRecovery('reconnect');
triggerRecovery('reconnect');
}
activeRunIdRef.current = null;
return;
}
if (type === 'assistant_stream') {
streamHook.setProcessingStage('streaming');
streamHook.setLastEventTimestamp(Date.now());
setProcessingStage('streaming');
setLastEventTimestamp(Date.now());
return;
}
@ -328,30 +361,30 @@ export function ChatProvider({ children }: { children: ReactNode }) {
setIsGenerating(true);
}
streamHook.setLastEventTimestamp(Date.now());
setLastEventTimestamp(Date.now());
if (type === 'agent_tool_start') {
streamHook.setProcessingStage('tool_use');
streamHook.addActivityEntry(ap);
setProcessingStage('tool_use');
addActivityEntry(ap);
return;
}
if (type === 'agent_tool_result') {
const completedId = ap.data?.toolCallId;
if (completedId) streamHook.completeActivityEntry(completedId);
if (completedId) completeActivityEntry(completedId);
if (toolResultRefreshRef.current) clearTimeout(toolResultRefreshRef.current);
const capturedSession = currentSessionRef.current;
const capturedGeneration = recoveryHook.getGeneration();
const capturedGeneration = getGeneration();
toolResultRefreshRef.current = setTimeout(async () => {
toolResultRefreshRef.current = null;
try {
const recovered = await loadChatHistory({ rpc, sessionKey: capturedSession, limit: 100 });
if (capturedSession !== currentSessionRef.current) return;
if (capturedGeneration !== recoveryHook.getGeneration()) return;
if (capturedGeneration !== getGeneration()) return;
if (recovered.length > 0) {
const merged = mergeRecoveredTail(msgHook.getAllMessages(), recovered);
msgHook.applyMessageWindow(merged, false);
const merged = mergeRecoveredTail(getAllMessages(), recovered);
applyMessageWindow(merged, false);
}
} catch { /* best-effort */ }
}, 300);
@ -360,7 +393,7 @@ export function ChatProvider({ children }: { children: ReactNode }) {
if (type === 'agent_state' && agentState) {
const stage = deriveProcessingStage(agentState);
if (stage) streamHook.setProcessingStage(stage);
if (stage) setProcessingStage(stage);
}
return;
}
@ -385,7 +418,7 @@ export function ChatProvider({ children }: { children: ReactNode }) {
const prevRunSeq = run.lastChatSeq;
run.lastChatSeq = updateHighestSeq(run.lastChatSeq, classified.chatSeq);
streamHook.setLastEventTimestamp(Date.now());
setLastEventTimestamp(Date.now());
if (type === 'chat_started') {
activeRunIdRef.current = runId;
@ -397,10 +430,10 @@ export function ChatProvider({ children }: { children: ReactNode }) {
run.bufferText = '';
setIsGenerating(true);
ttsHook.resetPlayedSounds();
streamHook.setProcessingStage('thinking');
streamHook.setActivityLog([]);
streamHook.startThinking(runId);
resetPlayedSounds();
setProcessingStage('thinking');
setActivityLog([]);
startThinking(runId);
return;
}
@ -411,14 +444,14 @@ export function ChatProvider({ children }: { children: ReactNode }) {
if (!isGeneratingRef.current) setIsGenerating(true);
if (!activeRunIdRef.current) activeRunIdRef.current = runId;
streamHook.captureThinkingDuration();
captureThinkingDuration();
const delta = extractStreamDelta(cp);
if (delta) {
run.bufferRaw = delta.text;
run.bufferText = delta.cleaned;
streamHook.scheduleStreamingUpdate(runId, run.bufferText);
streamHook.setProcessingStage('streaming');
scheduleStreamingUpdate(runId, run.bufferText);
setProcessingStage('streaming');
}
return;
}
@ -435,32 +468,32 @@ export function ChatProvider({ children }: { children: ReactNode }) {
run.bufferText = '';
if (activeRunIdRef.current === runId) activeRunIdRef.current = null;
recoveryHook.incrementGeneration();
incrementGeneration();
if (isActiveRun) {
setIsGenerating(false);
streamHook.setProcessingStage(null);
streamHook.setActivityLog([]);
streamHook.setLastEventTimestamp(0);
streamHook.clearStreamBuffer();
setProcessingStage(null);
setActivityLog([]);
setLastEventTimestamp(0);
clearStreamBuffer();
}
const finalData = extractFinalMessage(cp);
const finalMessages = processChatMessages(extractFinalMessages(cp));
if (finalMessages.length > 0) {
const merged = mergeFinalMessages(msgHook.getAllMessages(), finalMessages);
const thinkingDuration = streamHook.getThinkingDuration(runId);
const merged = mergeFinalMessages(getAllMessages(), finalMessages);
const thinkingDuration = getThinkingDuration(runId);
const withDuration = thinkingDuration
? patchThinkingDuration(merged, thinkingDuration)
: merged;
msgHook.applyMessageWindow(withDuration, false);
applyMessageWindow(withDuration, false);
} else {
recoveryHook.triggerRecovery('unrenderable-final');
triggerRecovery('unrenderable-final');
}
ttsHook.handleFinalTTS(finalData, isActiveRun);
streamHook.resetThinking();
handleFinalTTS(finalData, isActiveRun);
resetThinking();
pruneRunRegistry(runsRef.current, activeRunIdRef.current);
return;
}
@ -477,27 +510,27 @@ export function ChatProvider({ children }: { children: ReactNode }) {
run.bufferText = '';
if (activeRunIdRef.current === runId) activeRunIdRef.current = null;
recoveryHook.incrementGeneration();
incrementGeneration();
const partialMessagesRaw = extractFinalMessages(cp);
if (partialMessagesRaw.length > 0) {
const partialMessages = processChatMessages(partialMessagesRaw);
if (partialMessages.length > 0) {
const merged = mergeFinalMessages(msgHook.getAllMessages(), partialMessages);
msgHook.applyMessageWindow(merged, false);
const merged = mergeFinalMessages(getAllMessages(), partialMessages);
applyMessageWindow(merged, false);
}
}
if (isActiveRun) {
setIsGenerating(false);
streamHook.setProcessingStage(null);
streamHook.setActivityLog([]);
streamHook.setLastEventTimestamp(0);
streamHook.clearStreamBuffer();
ttsHook.playCompletionPing();
setProcessingStage(null);
setActivityLog([]);
setLastEventTimestamp(0);
clearStreamBuffer();
playCompletionPing();
}
streamHook.resetThinking();
resetThinking();
pruneRunRegistry(runsRef.current, activeRunIdRef.current);
return;
}
@ -514,44 +547,44 @@ export function ChatProvider({ children }: { children: ReactNode }) {
run.bufferText = '';
if (activeRunIdRef.current === runId) activeRunIdRef.current = null;
recoveryHook.incrementGeneration();
incrementGeneration();
if (isActiveRun) {
setIsGenerating(false);
streamHook.setProcessingStage(null);
streamHook.setActivityLog([]);
streamHook.setLastEventTimestamp(0);
streamHook.clearStreamBuffer();
setProcessingStage(null);
setActivityLog([]);
setLastEventTimestamp(0);
clearStreamBuffer();
}
if (isActiveRun) {
recoveryHook.triggerRecovery('unrenderable-final');
triggerRecovery('unrenderable-final');
}
streamHook.resetThinking();
resetThinking();
pruneRunRegistry(runsRef.current, activeRunIdRef.current);
}
});
}, [
msgHook.getAllMessages,
msgHook.applyMessageWindow,
streamHook.setProcessingStage,
streamHook.setLastEventTimestamp,
streamHook.setActivityLog,
streamHook.addActivityEntry,
streamHook.completeActivityEntry,
streamHook.startThinking,
streamHook.captureThinkingDuration,
streamHook.scheduleStreamingUpdate,
streamHook.clearStreamBuffer,
streamHook.getThinkingDuration,
streamHook.resetThinking,
recoveryHook.triggerRecovery,
recoveryHook.incrementGeneration,
recoveryHook.getGeneration,
ttsHook.playCompletionPing,
ttsHook.resetPlayedSounds,
ttsHook.handleFinalTTS,
getAllMessages,
applyMessageWindow,
setProcessingStage,
setLastEventTimestamp,
setActivityLog,
addActivityEntry,
completeActivityEntry,
startThinking,
captureThinkingDuration,
scheduleStreamingUpdate,
clearStreamBuffer,
getThinkingDuration,
resetThinking,
triggerRecovery,
incrementGeneration,
getGeneration,
playCompletionPing,
resetPlayedSounds,
handleFinalTTS,
subscribe,
rpc,
]);
@ -562,14 +595,14 @@ export function ChatProvider({ children }: { children: ReactNode }) {
const { msg: userMsg, tempId } = buildUserMessage({ text, images, uploadPayload });
recoveryHook.incrementGeneration();
incrementGeneration();
// Optimistic insert (functional updater to avoid read-then-write race)
msgHook.setAllMessages(prev => [...prev, userMsg]);
msgHook.setMessages((prev: ChatMsg[]) => [...prev, userMsg]);
setIsGenerating(true);
streamHook.setStream((prev: ChatStreamState) => ({ ...prev, html: '', runId: undefined }));
streamHook.setProcessingStage('thinking');
setProcessingStage('thinking');
const idempotencyKey = crypto.randomUUID ? crypto.randomUUID() : 'ik-' + Date.now();
try {
@ -587,7 +620,7 @@ export function ChatProvider({ children }: { children: ReactNode }) {
run.status = ack.status;
run.finalized = false;
activeRunIdRef.current = ack.runId;
streamHook.startThinking(ack.runId);
startThinking(ack.runId);
}
// Confirm the message (functional updater to avoid race after await)
@ -612,7 +645,7 @@ export function ChatProvider({ children }: { children: ReactNode }) {
msgHook.setMessages((prev: ChatMsg[]) => [...prev, errMsgBubble]);
setIsGenerating(false);
}
}, [rpc, msgHook, streamHook, ttsHook, recoveryHook]);
}, [rpc, msgHook, streamHook, ttsHook, incrementGeneration, setProcessingStage, startThinking]);
// ─── Abort / Reset ────────────────────────────────────────────────────────
const handleAbort = useCallback(async () => {
@ -664,13 +697,13 @@ export function ChatProvider({ children }: { children: ReactNode }) {
isGenerating,
stream: streamHook.stream,
processingStage: streamHook.processingStage,
lastEventTimestamp: streamHook.lastEventTimestamp,
lastEventTimestamp: lastEventTimestamp,
activityLog: streamHook.activityLog,
currentToolDescription: streamHook.currentToolDescription,
handleSend,
handleAbort,
handleReset,
loadHistory: msgHook.loadHistory,
loadHistory: loadHistory,
loadMore: msgHook.loadMore,
hasMore: msgHook.hasMore,
showResetConfirm,
@ -681,13 +714,13 @@ export function ChatProvider({ children }: { children: ReactNode }) {
isGenerating,
streamHook.stream,
streamHook.processingStage,
streamHook.lastEventTimestamp,
lastEventTimestamp,
streamHook.activityLog,
streamHook.currentToolDescription,
handleSend,
handleAbort,
handleReset,
msgHook.loadHistory,
loadHistory,
msgHook.loadMore,
msgHook.hasMore,
showResetConfirm,

View file

@ -198,7 +198,7 @@ export function useDashboardData(options: DashboardDataOptions = {}): DashboardD
clearInterval(memIv);
clearInterval(tokIv);
};
}, [refreshMemories, refreshTokens]);
}, [activeAgentId, refreshMemories, refreshTokens]);
return {
memories,