@@ -41,7 +41,16 @@ const KnowledgePage = lazy(() => import("./pages/KnowledgePage"));
4141// v0.18.0:学习目标页(意图层——独立 Tab,零叙事元素)
4242const GoalsPage = lazy ( ( ) => import ( "./pages/GoalsPage" ) ) ;
4343// REQ-274(v0.19.4):全局 AI 对话面板(丙案——按需唤起 + 内容保活)
44- import AiConversationDock from "./components/AiConversationDock" ;
44+ // 批 2 包体治理:从静态 import 改为按需 import,并在它之前加一道「首开挂载」闸门。
45+ // @ai -context: 原语义(见文件末尾 dock 渲染处的注释)是「常驻挂载——开合仅切 display,
46+ // 选中态/后台任务保活」。本任务**只**把「首次打开之前」变成不挂载:dockOpen 为真过
47+ // 一次 ⇒ dockMounted 永为真 ⇒ 之后的 open/close 仍是纯 display 切换,保活语义逐字保留
48+ // (控制方 2026-09-12 裁决 2:唯一被允许的行为差异就是首访挂载时序)。
49+ // @ai -context: 收益来自它的静态依赖链 AiConversationDock → TaskConversationView →
50+ // ChatMessageMarkdown → rehype-katex + katex/dist/katex.min.css。实测这条链把
51+ // vendor-katex + vendor-md(79,916 + 50,618 = 130,534 字节 gzip)钉在首屏;摘掉后
52+ // 首屏 gzip 227.08 → 92.79 kB(预算 200 kB,来源 docs/standards/performance.md:28)。
53+ const AiConversationDock = lazy ( ( ) => import ( "./components/AiConversationDock" ) ) ;
4554import AppErrorBoundary from "./components/AppErrorBoundary" ;
4655// 批 2 包体治理:两个窗口变体面板从静态 import 改为按需 import(各自独立 chunk)。
4756// @ai -context: 这两个面板只在**自己的窗口**里渲染(App() 顶部的 ?float=1 / ?overlay=1 早返回),
@@ -181,6 +190,15 @@ function MainShell() {
181190 const [ focusChatTaskId , setFocusChatTaskId ] = useState < number | null > ( null ) ;
182191 // REQ-274:对话面板开合 + 「在对话页继续」直达会话(消费后清空)
183192 const [ dockOpen , setDockOpen ] = useState ( false ) ;
193+ // 批 2:首次打开之前的挂载闸门(打开过即常驻,见 import 处的 @ai-context)。
194+ // @ai -context: 用 effect 而不是「渲染期 if (dockOpen) setDockMounted(true)」——渲染期改
195+ // state 会让 React 19 多跑一轮渲染(StrictMode 下更明显);代价只是首开首帧 dock 仍
196+ // 缺席(本来也要等它的 chunk 下载),无观感回归。与 mountedPages 同一模式。
197+ // 副作用:仅置真、无回落路径 ⇒ 一旦挂载过就永不卸载(TD-004 保活)。
198+ const [ dockMounted , setDockMounted ] = useState ( false ) ;
199+ useEffect ( ( ) => {
200+ if ( dockOpen ) setDockMounted ( true ) ;
201+ } , [ dockOpen ] ) ;
184202 const [ focusChatId , setFocusChatId ] = useState < number | null > ( null ) ;
185203 // 全局采集状态(ADR-007:与页面解耦,徽标常驻导航栏)。
186204 // 批 2b:capturing/recovering/paused 三份本地状态删除——采集控制单一状态源
@@ -473,20 +491,27 @@ function MainShell() {
473491 < SettingsPage active = { page === "settings" } />
474492 </ PageSlot >
475493 </ main >
476- { /* REQ-274:全局 AI 对话面板(常驻挂载——开合仅切 display,选中态/后台任务保活) */ }
477- < AiConversationDock
478- open = { dockOpen }
479- onClose = { ( ) => setDockOpen ( false ) }
480- onOpenChat = { ( chatId ) => { setFocusChatId ( chatId ) ; setPage ( "chat" ) ; } }
481- onOpenTaskInChat = { ( taskId ) => { setFocusChatTaskId ( taskId ) ; setPage ( "chat" ) ; } }
482- onOpenSessions = { ( id ) => { setFocusSessionId ( id ) ; setPage ( "sessions" ) ; } }
483- onOpenNote = { ( id ) => openNotePlain ( id ) }
484- onOpenRefineWorkbench = { ( sessionId , taskId ) => {
485- setFocusSessionId ( sessionId ) ;
486- setFocusRefineTaskId ( taskId ) ;
487- setPage ( "sessions" ) ;
488- } }
489- />
494+ { /* REQ-274:全局 AI 对话面板(常驻挂载——开合仅切 display,选中态/后台任务保活)。
495+ 批 2:闸门只加在**首开之前** —— dockMounted 一旦为真永不复位,此后 open/close
496+ 仍是纯 display 切换(保活语义逐字保留);独立 Suspense(fallback=null) 与 PageSlot
497+ 同理由:chunk 首次到达前不渲染任何东西,加载失败由外层 AppErrorBoundary 兜底。 */ }
498+ { dockMounted && (
499+ < Suspense fallback = { null } >
500+ < AiConversationDock
501+ open = { dockOpen }
502+ onClose = { ( ) => setDockOpen ( false ) }
503+ onOpenChat = { ( chatId ) => { setFocusChatId ( chatId ) ; setPage ( "chat" ) ; } }
504+ onOpenTaskInChat = { ( taskId ) => { setFocusChatTaskId ( taskId ) ; setPage ( "chat" ) ; } }
505+ onOpenSessions = { ( id ) => { setFocusSessionId ( id ) ; setPage ( "sessions" ) ; } }
506+ onOpenNote = { ( id ) => openNotePlain ( id ) }
507+ onOpenRefineWorkbench = { ( sessionId , taskId ) => {
508+ setFocusSessionId ( sessionId ) ;
509+ setFocusRefineTaskId ( taskId ) ;
510+ setPage ( "sessions" ) ;
511+ } }
512+ />
513+ </ Suspense >
514+ ) }
490515 </ div >
491516 ) ;
492517}
0 commit comments