import { feature } from 'bun:bundle' import { useEffect, useRef } from 'react' import { getTerminalFocusState, subscribeTerminalFocus, } from '../ink/terminal-focus-state.js' import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' import { generateAwaySummary } from '../services/awaySummary.js' import type { Message } from '../types/message.js' import { createAwaySummaryMessage } from '../utils/messages.js' const BLUR_DELAY_MS = 5 * 60_000 type SetMessages = (updater: (prev: Message[]) => Message[]) => void function hasSummarySinceLastUserTurn(messages: readonly Message[]): boolean { for (let i = messages.length - 1; i >= 0; i--) { const m = messages[i]! if (m.type === 'user' && !m.isMeta && !m.isCompactSummary) return false if (m.type === 'system' && m.subtype === 'away_summary') return true } return false } /** * 在终端失焦 5 分钟后追加"离开期间摘要"消息。 * 仅在以下条件满足时触发:(a) 失焦后 5 分钟,(b) 当前无进行中的对话, * (c) 自上次用户消息以来没有已有的 away_summary。 * * Focus state 为 'unknown'(终端不支持 DECSET 1004)时无操作。 */ export function useAwaySummary( messages: readonly Message[], setMessages: SetMessages, isLoading: boolean, ): void { const timerRef = useRef | null>(null) const abortRef = useRef(null) const messagesRef = useRef(messages) const isLoadingRef = useRef(isLoading) const pendingRef = useRef(false) const generateRef = useRef<(() => Promise) | null>(null) messagesRef.current = messages isLoadingRef.current = isLoading // 3P 默认值: false const gbEnabled = getFeatureValue_CACHED_MAY_BE_STALE( 'tengu_sedge_lantern', false, ) useEffect(() => { if (!feature('AWAY_SUMMARY')) return if (!gbEnabled) return function clearTimer(): void { if (timerRef.current !== null) { clearTimeout(timerRef.current) timerRef.current = null } } function abortInFlight(): void { abortRef.current?.abort() abortRef.current = null } async function generate(): Promise { pendingRef.current = false if (hasSummarySinceLastUserTurn(messagesRef.current)) return abortInFlight() const controller = new AbortController() abortRef.current = controller const text = await generateAwaySummary( messagesRef.current, controller.signal, ) if (controller.signal.aborted || text === null) return setMessages(prev => [...prev, createAwaySummaryMessage(text)]) } function onBlurTimerFire(): void { timerRef.current = null if (isLoadingRef.current) { pendingRef.current = true return } void generate() } function onFocusChange(): void { const state = getTerminalFocusState() if (state === 'blurred') { clearTimer() timerRef.current = setTimeout(onBlurTimerFire, BLUR_DELAY_MS) } else if (state === 'focused') { clearTimer() abortInFlight() pendingRef.current = false } // 'unknown' → 无操作 } const unsubscribe = subscribeTerminalFocus(onFocusChange) // 处理效果挂载时已经失焦的情况 onFocusChange() generateRef.current = generate return () => { unsubscribe() clearTimer() abortInFlight() generateRef.current = null } }, [gbEnabled, setMessages]) // 定时器在对话期间触发 → 在对话结束时触发(如果仍然失焦) useEffect(() => { if (isLoading) return if (!pendingRef.current) return if (getTerminalFocusState() !== 'blurred') return void generateRef.current?.() }, [isLoading]) }