1- import { Box } from 'ink' ;
2- import React from 'react' ;
1+ import { Box , Static } from 'ink' ;
2+ import React , { ReactNode , useMemo } from 'react' ;
33import type { TodoItem } from '../../tools/builtin/todo/types.js' ;
4- import type { SessionState } from '../contexts/SessionContext.js' ;
4+ import type { SessionMessage , SessionState } from '../contexts/SessionContext.js' ;
5+ import { Header } from './Header.js' ;
56import { MessageRenderer } from './MessageRenderer.js' ;
67import { TodoPanel } from './TodoPanel.js' ;
78
@@ -15,32 +16,83 @@ interface MessageAreaProps {
1516/**
1617 * 消息区域组件
1718 * 负责显示消息列表
19+ *
20+ * 性能优化:
21+ * - 使用 Ink 的 Static 组件将已完成的消息隔离,避免重新渲染
22+ * - 只有正在流式传输的消息会重新渲染
23+ * - 使用 useMemo 缓存计算结果
24+ *
25+ * 布局优化:
26+ * - Header 作为 Static 的第一个子项,确保永远在历史消息顶部
1827 */
1928export const MessageArea : React . FC < MessageAreaProps > = React . memo (
2029 ( { sessionState, terminalWidth, todos = [ ] , showTodoPanel = false } ) => {
21- // 找到最后一条用户消息的索引(TodoPanel 将显示在这之后)
22- const lastUserMessageIndex = sessionState . messages . findLastIndex (
23- ( msg ) => msg . role === 'user'
30+ // 分离已完成的消息和正在流式传输的消息
31+ const { completedMessages, streamingMessage } = useMemo ( ( ) => {
32+ const messages = sessionState . messages ;
33+ const isStreaming = sessionState . isThinking ;
34+
35+ // 如果正在思考,最后一条消息视为流式传输中
36+ if ( isStreaming && messages . length > 0 ) {
37+ return {
38+ completedMessages : messages . slice ( 0 , - 1 ) ,
39+ streamingMessage : messages [ messages . length - 1 ] ,
40+ } ;
41+ }
42+
43+ // 否则所有消息都是已完成的
44+ return {
45+ completedMessages : messages ,
46+ streamingMessage : null ,
47+ } ;
48+ } , [ sessionState . messages , sessionState . isThinking ] ) ;
49+
50+ // 找到最后一条用户消息的索引(用于 TodoPanel 定位)
51+ const lastUserMessageIndex = useMemo ( ( ) => {
52+ return sessionState . messages . findLastIndex ( ( msg ) => msg . role === 'user' ) ;
53+ } , [ sessionState . messages ] ) ;
54+
55+ // 渲染单个消息(用于 Static 和 dynamic 区域)
56+ const renderMessage = ( msg : SessionMessage , index : number , isPending = false ) => (
57+ < Box key = { msg . id } flexDirection = "column" >
58+ < MessageRenderer
59+ content = { msg . content }
60+ role = { msg . role }
61+ terminalWidth = { terminalWidth }
62+ metadata = { msg . metadata as Record < string , unknown > }
63+ isPending = { isPending }
64+ />
65+ { /* 在最后一条用户消息后显示 TodoPanel */ }
66+ { index === lastUserMessageIndex && showTodoPanel && todos . length > 0 && (
67+ < TodoPanel todos = { todos } visible = { true } compact = { false } />
68+ ) }
69+ </ Box >
2470 ) ;
2571
72+ // 构建 Static items:Header + 已完成的消息
73+ const staticItems = useMemo ( ( ) => {
74+ const items : ReactNode [ ] = [ ] ;
75+
76+ // 1. Header 作为第一个子项(永远在顶部)
77+ items . push ( < Header key = "header" /> ) ;
78+
79+ // 2. 已完成的消息
80+ completedMessages . forEach ( ( msg , index ) => {
81+ items . push ( renderMessage ( msg , index ) ) ;
82+ } ) ;
83+
84+ return items ;
85+ } , [ completedMessages , lastUserMessageIndex , showTodoPanel , todos ] ) ;
86+
2687 return (
2788 < Box flexDirection = "column" flexGrow = { 1 } paddingX = { 2 } >
2889 < Box flexDirection = "column" flexGrow = { 1 } >
29- { /* 渲染消息列表 */ }
30- { sessionState . messages . map ( ( msg , index ) => (
31- < Box key = { msg . id } flexDirection = "column" >
32- < MessageRenderer
33- content = { msg . content }
34- role = { msg . role }
35- terminalWidth = { terminalWidth }
36- metadata = { msg . metadata as Record < string , unknown > }
37- />
38- { /* 在最后一条用户消息后显示 TodoPanel */ }
39- { index === lastUserMessageIndex && showTodoPanel && todos . length > 0 && (
40- < TodoPanel todos = { todos } visible = { true } compact = { false } />
41- ) }
42- </ Box >
43- ) ) }
90+ { /* 静态区域:Header + 已完成的消息永不重新渲染 */ }
91+ < Static items = { staticItems } > { ( item ) => item } </ Static >
92+
93+ { /* 动态区域:只有流式传输的消息会重新渲染 */ }
94+ { streamingMessage &&
95+ renderMessage ( streamingMessage , completedMessages . length , true ) }
4496 </ Box >
4597 </ Box >
4698 ) ;
0 commit comments