1111 */
1212import { useCallback , useEffect , useRef , useState } from "react" ;
1313import { invoke } from "@tauri-apps/api/core" ;
14+ import { listen } from "@tauri-apps/api/event" ;
1415import { confirm } from "@tauri-apps/plugin-dialog" ;
1516import type { AiTaskRecord , AiProviderView , ChatMessage , ChatSession , ChatStreamEvent , NoteGroup } from "../types" ;
1617import ChatSidebar from "../components/ChatSidebar" ;
@@ -25,9 +26,14 @@ import { buildConversationMarkdown } from "../utils/chatTranscript";
2526import TaskLaunchDialog , { type LaunchTargetRow } from "../components/TaskLaunchDialog" ;
2627import TaskThreadCard from "../components/TaskThreadCard" ;
2728import { buildTaskFollowUpPrompt } from "../utils/taskFollowUp" ;
28- import { refLabel } from "../utils/entityLabel" ;
29+ // 2026-09-09 批 1:任务标题统一按类别解析(taskRefLabel——笔记级精修
30+ // ref_id=笔记 id,仅按 opType 会错查会话标题表;enrich 恒笔记级)
31+ import { taskRefLabel } from "../utils/entityLabel" ;
2932
3033interface Props {
34+ /** 页面可见门控(App 层注入——保活挂载下切回重同步;2026-09-09 批 1:
35+ 别页发起/完成的 AI 任务本页无感知,切回必须重拉。SessionsPage 同款语义) */
36+ active : boolean ;
3137 /** 跨页直达(任务对话引用跳转) */
3238 onOpenSessions : ( sessionId : number ) => void ;
3339 onOpenNote : ( noteId : number ) => void ;
@@ -53,11 +59,15 @@ interface SessionRow {
5359/** 首次发送云端提示的记忆键(一次性确认) */
5460const CLOUD_NOTICE_KEY = "entropy-ai-chat-cloud-notice" ;
5561
62+ /** 静态数据时效(ms):active 切回距上次拉取 <10s 则跳过——列表不抖动
63+ * (AiConversationDock F9 同口径) */
64+ const STATIC_TTL_MS = 10_000 ;
65+
5666const CLOUD_NOTICE_TEXT =
5767 "对话内容(纯文本)将发送至所选模型的云端服务商;本地音视频/图片/笔记永不出本机。是否同意?" ;
5868
5969export default function ChatPage ( props : Props ) {
60- const { onOpenSessions, onOpenNote, onOpenNoteHighlight, onOpenSettings, focusTaskId, onFocusTaskConsumed, focusChatId, onFocusChatConsumed, onOpenRefineWorkbench } = props ;
70+ const { active , onOpenSessions, onOpenNote, onOpenNoteHighlight, onOpenSettings, focusTaskId, onFocusTaskConsumed, focusChatId, onFocusChatConsumed, onOpenRefineWorkbench } = props ;
6171 const [ sessions , setSessions ] = useState < ChatSession [ ] > ( [ ] ) ;
6272 const [ tasks , setTasks ] = useState < AiTaskRecord [ ] > ( [ ] ) ;
6373 const [ providers , setProviders ] = useState < AiProviderView [ ] > ( [ ] ) ;
@@ -71,7 +81,6 @@ export default function ChatPage(props: Props) {
7181 const [ retryBusy , setRetryBusy ] = useState ( false ) ;
7282 const [ sessionTitles , setSessionTitles ] = useState < Map < number , string > > ( new Map ( ) ) ;
7383 const [ noteTitles , setNoteTitles ] = useState < Map < number , string > > ( new Map ( ) ) ;
74- const [ staticLoaded , setStaticLoaded ] = useState ( false ) ;
7584 // v0.16.1:对话转笔记——组列表(目标组下拉)与保存对话框态
7685 const [ noteGroups , setNoteGroups ] = useState < NoteGroup [ ] > ( [ ] ) ;
7786 const [ saveDialog , setSaveDialog ] = useState < { initialTitle : string ; content : string } | null > ( null ) ;
@@ -83,6 +92,10 @@ export default function ChatPage(props: Props) {
8392 const [ noteRows , setNoteRows ] = useState < LaunchTargetRow [ ] > ( [ ] ) ;
8493 const activeChatRef = useRef < number | null > ( null ) ;
8594 activeChatRef . current = activeChatId ;
95+ // 2026-09-09 批 1:activeTaskId ref 镜像——ai:task-update 监听只注册一次,
96+ // 回调读最新选中任务(AiConversationDock F2 同款——选中切换不触发重订阅)
97+ const activeTaskIdRef = useRef < number | null > ( null ) ;
98+ activeTaskIdRef . current = activeTaskId ;
8699 // F3(审查竞态):消息装载序号——并发装载只认最新(快速切会话时旧会话的
87100 // 异步返回不得覆盖新会话消息;与 selectChat 的同步 set 无关,纯防错序)
88101 const loadSeq = useRef ( 0 ) ;
@@ -101,34 +114,44 @@ export default function ChatPage(props: Props) {
101114
102115 const { view, setActive, isStreaming, launch, stop } = useChatStream ( ( sid , ev ) => void onStreamSettled ( sid , ev ) ) ;
103116
104- // 静态数据(会话/Provider/标题映射)只加载一次——审查优化:原 6s 全量刷新
105- useEffect ( ( ) => {
106- if ( staticLoaded ) return ;
107- void ( async ( ) => {
108- setSessions ( await invoke < ChatSession [ ] > ( "chat_list_sessions" ) . catch ( ( ) => [ ] as ChatSession [ ] ) ) ;
109- setProviders ( await invoke < AiProviderView [ ] > ( "ai_provider_list" ) . catch ( ( ) => [ ] as AiProviderView [ ] ) ) ;
110- const sessRows = await invoke < SessionRow [ ] > ( "list_sessions" , { limit : 500 } ) . catch ( ( ) => [ ] as SessionRow [ ] ) ;
111- setSessionTitles ( new Map ( sessRows . map ( ( s ) => [ s . id , s . title ] ) ) ) ;
112- setSessionRows ( sessRows . map ( ( s ) => ( { id : s . id , title : s . title } ) ) ) ;
113- const notes = await invoke < { id : number ; title : string } [ ] > ( "search_notes" , { keyword : "" , tag : null as string | null } ) . catch ( ( ) => [ ] as { id : number ; title : string } [ ] ) ;
114- setNoteTitles ( new Map ( notes . map ( ( n ) => [ n . id , n . title ] ) ) ) ;
115- setNoteRows ( notes . map ( ( n ) => ( { id : n . id , title : n . title } ) ) ) ;
116- // v0.16.1:组列表(保存对话框目标组下拉——失败静默仅无组可选)
117- setNoteGroups ( await invoke < NoteGroup [ ] > ( "list_note_groups" , { terrain : null } ) . catch ( ( ) => [ ] as NoteGroup [ ] ) ) ;
118- setStaticLoaded ( true ) ;
119- } ) ( ) ;
120- } , [ staticLoaded ] ) ;
121-
122- // 任务列表:初始 + 仅当存在未终态任务时轮询(审查优化:无进行中任务零轮询)
117+ // 静态数据(会话/Provider/标题/组映射)——2026-09-09 批 1:原「挂载即拉
118+ // 一次」在保活挂载(TD-004)下等于应用启动即在隐藏页拉取;改为首激活拉取
119+ // + 切回超 STATIC_TTL_MS 时效才重拉(AiConversationDock F9 同口径防抖动)。
120+ // 标题映射覆盖会话/笔记全量——AI 任务侧栏条目按 ref_id 查表(taskRefLabel)。
121+ const staticAtRef = useRef ( 0 ) ;
122+ const loadStatic = useCallback ( async ( ) => {
123+ staticAtRef . current = Date . now ( ) ;
124+ setSessions ( await invoke < ChatSession [ ] > ( "chat_list_sessions" ) . catch ( ( ) => [ ] as ChatSession [ ] ) ) ;
125+ setProviders ( await invoke < AiProviderView [ ] > ( "ai_provider_list" ) . catch ( ( ) => [ ] as AiProviderView [ ] ) ) ;
126+ const sessRows = await invoke < SessionRow [ ] > ( "list_sessions" , { limit : 500 } ) . catch ( ( ) => [ ] as SessionRow [ ] ) ;
127+ setSessionTitles ( new Map ( sessRows . map ( ( s ) => [ s . id , s . title ] ) ) ) ;
128+ setSessionRows ( sessRows . map ( ( s ) => ( { id : s . id , title : s . title } ) ) ) ;
129+ const notes = await invoke < { id : number ; title : string } [ ] > ( "search_notes" , { keyword : "" , tag : null as string | null } ) . catch ( ( ) => [ ] as { id : number ; title : string } [ ] ) ;
130+ setNoteTitles ( new Map ( notes . map ( ( n ) => [ n . id , n . title ] ) ) ) ;
131+ setNoteRows ( notes . map ( ( n ) => ( { id : n . id , title : n . title } ) ) ) ;
132+ // v0.16.1:组列表(保存对话框目标组下拉——失败静默仅无组可选)
133+ setNoteGroups ( await invoke < NoteGroup [ ] > ( "list_note_groups" , { terrain : null } ) . catch ( ( ) => [ ] as NoteGroup [ ] ) ) ;
134+ } , [ ] ) ;
135+
136+ // 任务列表拉取(AI 任务段 + 线程卡数据源);拉取时机见下方 active 切回/
137+ // ai:task-update 终态事件,进行中任务由轮询 effect 兜底
123138 const reloadTasks = useCallback ( async ( ) => {
124139 const refine = await invoke < AiTaskRecord [ ] > ( "ai_task_history" , { opType : "refine" , limit : 30 } ) . catch ( ( ) => [ ] as AiTaskRecord [ ] ) ;
125140 const enrich = await invoke < AiTaskRecord [ ] > ( "ai_task_history" , { opType : "enrich" , limit : 30 } ) . catch ( ( ) => [ ] as AiTaskRecord [ ] ) ;
126141 setTasks ( [ ...refine , ...enrich ] . sort ( ( a , b ) => b . createdAt - a . createdAt ) . slice ( 0 , 60 ) ) ;
127142 } , [ ] ) ;
128143
144+ // 2026-09-09 批 1(刷新缺口根治):active 门控切回重同步——本页保活挂载、
145+ // 隐藏期别页(会话页精修/笔记级 AI)发起/完成的任务本页无感知;每次切回
146+ // 重拉任务列表,静态数据(会话/笔记标题映射)超时效才重拉(隐藏期可能有
147+ // 新建/改名实体——标题查表要拿得到,否则任务条目回落中性占位)
129148 useEffect ( ( ) => {
149+ if ( ! active ) return ;
150+ if ( staticAtRef . current === 0 || Date . now ( ) - staticAtRef . current >= STATIC_TTL_MS ) {
151+ void loadStatic ( ) ;
152+ }
130153 void reloadTasks ( ) ;
131- } , [ reloadTasks ] ) ;
154+ } , [ active , loadStatic , reloadTasks ] ) ;
132155
133156 const hasActiveTask = tasks . some ( ( t ) => t . state === "pending" || t . state === "running" ) ;
134157 useEffect ( ( ) => {
@@ -162,16 +185,38 @@ export default function ChatPage(props: Props) {
162185 }
163186 } , [ setActive ] ) ;
164187
188+ // 2026-09-09 批 1(刷新缺口根治):订阅 ai:task-update **终态**事件 → 重拉
189+ // 任务列表——会话页/笔记页发起的精修/补充在本页(保活挂载)不可见的问题
190+ // 根源是本页无事件通道;成功/失败判定同 App.tsx 全局 toast 口径(进行中
191+ // 按片进度事件不刷列表,省无谓 IPC)。选中任务详情打开时终态后重取详情
192+ // (轨迹/adopted 回写同 AiConversationDock F2——ref 镜像读最新选中)。
193+ // 注:Rust 侧事件先于 DB 终态落库发出(set_task → finish_ai_task)——理论
194+ // 竞态下本次刷新读到 running,由下方 hasActiveTask 轮询兜底收敛(不额外延时)
195+ useEffect ( ( ) => {
196+ let disposed = false ;
197+ let unlisten : ( ( ) => void ) | undefined ;
198+ void listen < [ number , import ( "../types" ) . AiTaskState ] > ( "ai:task-update" , ( e ) => {
199+ if ( disposed ) return ;
200+ const st = e . payload [ 1 ] ;
201+ const terminal = st === "Succeeded" || ( typeof st === "object" && st !== null && "Failed" in st ) ;
202+ if ( ! terminal ) return ;
203+ void reloadTasks ( ) ;
204+ const tid = activeTaskIdRef . current ;
205+ if ( tid != null ) void selectTask ( tid ) ;
206+ } ) . then ( ( u ) => {
207+ if ( disposed ) u ( ) ; else unlisten = u ;
208+ } ) ;
209+ return ( ) => { disposed = true ; unlisten ?.( ) ; } ;
210+ } , [ reloadTasks , selectTask ] ) ;
211+
165212 const refreshSessions = useCallback ( async ( ) => {
166213 setSessions ( await invoke < ChatSession [ ] > ( "chat_list_sessions" ) . catch ( ( ) => [ ] as ChatSession [ ] ) ) ;
167214 } , [ ] ) ;
168215
169216 // v0.16.1:任务对话化——目标名解析 / 追问预填 / 启动成功(REQ-277:
170217 // 标题缺失语义占位——绝不回退裸 `#id`)
171218 const taskRefTitle = useCallback ( ( t : import ( "../types" ) . AiTaskRecord ) : string =>
172- t . opType === "refine"
173- ? refLabel ( "session" , sessionTitles . get ( t . refId ) )
174- : refLabel ( "note" , noteTitles . get ( t . refId ) ) ,
219+ taskRefLabel ( t , sessionTitles , noteTitles ) ,
175220 [ sessionTitles , noteTitles ] ) ;
176221 const followUpTask = useCallback ( ( t : import ( "../types" ) . AiTaskRecord ) => {
177222 setDraft ( buildTaskFollowUpPrompt ( t , taskRefTitle ( t ) ) ) ;
0 commit comments