@@ -34,6 +34,8 @@ import {
3434 useChatKeyboard ,
3535 type ChatKeyboardHandlers ,
3636} from './hooks/use-chat-keyboard'
37+ import { useChatMessages } from './hooks/use-chat-messages'
38+ import { useChatState } from './hooks/use-chat-state'
3739import { useClipboard } from './hooks/use-clipboard'
3840import { useConnectionStatus } from './hooks/use-connection-status'
3941import { useElapsedTime } from './hooks/use-elapsed-time'
@@ -75,7 +77,7 @@ import {
7577 createDefaultChatKeyboardState ,
7678} from './utils/keyboard-actions'
7779import { loadLocalAgents } from './utils/local-agent-registry'
78- import { buildMessageTree } from './utils/message-tree-utils'
80+ // buildMessageTree is now used internally by useChatMessages hook
7981import {
8082 getStatusIndicatorState ,
8183 type AuthStatus ,
@@ -90,8 +92,8 @@ import { logger } from './utils/logger'
9092
9193import type { CommandResult } from './commands/command-registry'
9294import type { MultilineInputHandle } from './components/multiline-input'
93- import type { ContentBlock } from './types/chat'
94- import type { SendMessageFn } from './types/contracts/send-message'
95+
96+ // SendMessageFn type is now used internally by useChatState hook
9597import type { User } from './utils/auth'
9698import type { AgentMode } from './utils/constants'
9799import type { FileTreeNode } from '@codebuff/common/util/file'
@@ -134,10 +136,7 @@ export const Chat = ({
134136 const [ hasOverflow , setHasOverflow ] = useState ( false )
135137 const hasOverflowRef = useRef ( false )
136138
137- // Message pagination - show last N messages with "Load previous" button
138- const MESSAGE_BATCH_SIZE = 15
139- const [ visibleMessageCount , setVisibleMessageCount ] =
140- useState ( MESSAGE_BATCH_SIZE )
139+ // Message handling extracted to useChatMessages hook (initialized below after streamStatus is available)
141140
142141 const queryClient = useQueryClient ( )
143142 const [ , startUiTransition ] = useTransition ( )
@@ -164,6 +163,7 @@ export const Chat = ({
164163 // Monitor usage data and auto-show banner when thresholds are crossed
165164 useUsageMonitor ( )
166165
166+ // Get chat state from extracted hook
167167 const {
168168 inputValue,
169169 cursorPosition,
@@ -175,7 +175,7 @@ export const Chat = ({
175175 setSlashSelectedIndex,
176176 agentSelectedIndex,
177177 setAgentSelectedIndex,
178- streamingAgents : rawStreamingAgents ,
178+ streamingAgents,
179179 focusedAgentId,
180180 setFocusedAgentId,
181181 messages,
@@ -186,49 +186,15 @@ export const Chat = ({
186186 setAgentMode,
187187 toggleAgentMode,
188188 isRetrying,
189- } = useChatStore (
190- useShallow ( ( store ) => ( {
191- inputValue : store . inputValue ,
192- cursorPosition : store . cursorPosition ,
193- lastEditDueToNav : store . lastEditDueToNav ,
194- setInputValue : store . setInputValue ,
195- inputFocused : store . inputFocused ,
196- setInputFocused : store . setInputFocused ,
197- slashSelectedIndex : store . slashSelectedIndex ,
198- setSlashSelectedIndex : store . setSlashSelectedIndex ,
199- agentSelectedIndex : store . agentSelectedIndex ,
200- setAgentSelectedIndex : store . setAgentSelectedIndex ,
201- streamingAgents : store . streamingAgents ,
202- focusedAgentId : store . focusedAgentId ,
203- setFocusedAgentId : store . setFocusedAgentId ,
204- messages : store . messages ,
205- setMessages : store . setMessages ,
206- activeSubagents : store . activeSubagents ,
207- isChainInProgress : store . isChainInProgress ,
208- agentMode : store . agentMode ,
209- setAgentMode : store . setAgentMode ,
210- toggleAgentMode : store . toggleAgentMode ,
211- isRetrying : store . isRetrying ,
212- } ) ) ,
213- )
214-
215- // Stabilize streamingAgents reference - only create new Set when content changes
216- const streamingAgentsKey = useMemo (
217- ( ) => Array . from ( rawStreamingAgents ) . sort ( ) . join ( ',' ) ,
218- [ rawStreamingAgents ] ,
219- )
220- const streamingAgents = useMemo (
221- ( ) => rawStreamingAgents ,
222- [ streamingAgentsKey ] ,
223- )
224- const pendingBashMessages = useChatStore ( ( state ) => state . pendingBashMessages )
225-
226- // Refs for tracking state across renders
227- const activeAgentStreamsRef = useRef < number > ( 0 )
228- const isChainInProgressRef = useRef < boolean > ( isChainInProgress )
229- const activeSubagentsRef = useRef < Set < string > > ( activeSubagents )
230- const abortControllerRef = useRef < AbortController | null > ( null )
231- const sendMessageRef = useRef < SendMessageFn > ( )
189+ pendingBashMessages,
190+ refs : {
191+ activeAgentStreamsRef,
192+ isChainInProgressRef,
193+ activeSubagentsRef,
194+ abortControllerRef,
195+ sendMessageRef,
196+ } ,
197+ } = useChatState ( )
232198
233199 const { statusMessage } = useClipboard ( )
234200
@@ -268,135 +234,16 @@ export const Chat = ({
268234 }
269235 } , [ initialMode , setAgentMode ] )
270236
271- // Sync refs with state
272- useEffect ( ( ) => {
273- isChainInProgressRef . current = isChainInProgress
274- } , [ isChainInProgress ] )
275-
276- useEffect ( ( ) => {
277- activeSubagentsRef . current = activeSubagents
278- } , [ activeSubagents ] )
279-
280- // Reset visible message count when messages are cleared or conversation changes
281- useEffect ( ( ) => {
282- if ( messages . length <= MESSAGE_BATCH_SIZE ) {
283- setVisibleMessageCount ( MESSAGE_BATCH_SIZE )
284- }
285- } , [ messages . length ] )
286-
287- const isUserCollapsingRef = useRef < boolean > ( false )
288-
289- const handleCollapseToggle = useCallback (
290- ( id : string ) => {
291- // Set flag to prevent auto-scroll during user-initiated collapse
292- isUserCollapsingRef . current = true
293-
294- // Find and toggle the block's isCollapsed property
295- setMessages ( ( prevMessages ) => {
296- return prevMessages . map ( ( message ) => {
297- // Handle agent variant messages
298- if ( message . variant === 'agent' && message . id === id ) {
299- const wasCollapsed = message . metadata ?. isCollapsed ?? false
300- return {
301- ...message ,
302- metadata : {
303- ...message . metadata ,
304- isCollapsed : ! wasCollapsed ,
305- userOpened : wasCollapsed , // Mark as user-opened if expanding
306- } ,
307- }
308- }
309-
310- // Handle blocks within messages
311- if ( ! message . blocks ) return message
312-
313- const updateBlocksRecursively = (
314- blocks : ContentBlock [ ] ,
315- ) : ContentBlock [ ] => {
316- let foundTarget = false
317- const result = blocks . map ( ( block ) => {
318- // Handle thinking blocks - just match by thinkingId
319- if ( block . type === 'text' && block . thinkingId === id ) {
320- foundTarget = true
321- const wasCollapsed = block . isCollapsed ?? false
322- return {
323- ...block ,
324- isCollapsed : ! wasCollapsed ,
325- userOpened : wasCollapsed , // Mark as user-opened if expanding
326- }
327- }
328-
329- // Handle agent blocks
330- if ( block . type === 'agent' && block . agentId === id ) {
331- foundTarget = true
332- const wasCollapsed = block . isCollapsed ?? false
333- return {
334- ...block ,
335- isCollapsed : ! wasCollapsed ,
336- userOpened : wasCollapsed , // Mark as user-opened if expanding
337- }
338- }
339-
340- // Handle tool blocks
341- if ( block . type === 'tool' && block . toolCallId === id ) {
342- foundTarget = true
343- const wasCollapsed = block . isCollapsed ?? false
344- return {
345- ...block ,
346- isCollapsed : ! wasCollapsed ,
347- userOpened : wasCollapsed , // Mark as user-opened if expanding
348- }
349- }
350-
351- // Handle agent-list blocks
352- if ( block . type === 'agent-list' && block . id === id ) {
353- foundTarget = true
354- const wasCollapsed = block . isCollapsed ?? false
355- return {
356- ...block ,
357- isCollapsed : ! wasCollapsed ,
358- userOpened : wasCollapsed , // Mark as user-opened if expanding
359- }
360- }
361-
362- // Recursively update nested blocks inside agent blocks
363- if ( block . type === 'agent' && block . blocks ) {
364- const updatedBlocks = updateBlocksRecursively ( block . blocks )
365- // Only create new block if nested blocks actually changed
366- if ( updatedBlocks !== block . blocks ) {
367- foundTarget = true
368- return {
369- ...block ,
370- blocks : updatedBlocks ,
371- }
372- }
373- }
374-
375- return block
376- } )
377-
378- // Return original array reference if nothing changed
379- return foundTarget ? result : blocks
380- }
381-
382- return {
383- ...message ,
384- blocks : updateBlocksRecursively ( message . blocks ) ,
385- }
386- } )
387- } )
388-
389- // Reset flag after state update completes
390- setTimeout ( ( ) => {
391- isUserCollapsingRef . current = false
392- } , 0 )
393- } ,
394- [ setMessages ] ,
395- )
396-
397- const isUserCollapsing = useCallback ( ( ) => {
398- return isUserCollapsingRef . current
399- } , [ ] )
237+ // Use extracted chat messages hook for message tree and pagination
238+ const {
239+ messageTree,
240+ topLevelMessages,
241+ visibleTopLevelMessages,
242+ hiddenMessageCount,
243+ handleCollapseToggle,
244+ isUserCollapsing,
245+ handleLoadPreviousMessages,
246+ } = useChatMessages ( { messages, setMessages } )
400247
401248 const { scrollToLatest, scrollUp, scrollDown, scrollboxProps, isAtBottom } = useChatScrollbox (
402249 scrollRef ,
@@ -1360,10 +1207,7 @@ export const Chat = ({
13601207 disabled : askUserState !== null ,
13611208 } )
13621209
1363- const { tree : messageTree , topLevelMessages } = useMemo (
1364- ( ) => buildMessageTree ( messages ) ,
1365- [ messages ] ,
1366- )
1210+ // messageTree and topLevelMessages now come from useChatMessages hook
13671211
13681212 // Sync message block context to zustand store for child components
13691213 const setMessageBlockContext = useMessageBlockStore (
@@ -1412,20 +1256,7 @@ export const Chat = ({
14121256 setMessageBlockCallbacks ,
14131257 ] )
14141258
1415- // Compute visible messages slice (from the end)
1416- const visibleTopLevelMessages = useMemo ( ( ) => {
1417- if ( topLevelMessages . length <= visibleMessageCount ) {
1418- return topLevelMessages
1419- }
1420- return topLevelMessages . slice ( - visibleMessageCount )
1421- } , [ topLevelMessages , visibleMessageCount ] )
1422-
1423- const hiddenMessageCount =
1424- topLevelMessages . length - visibleTopLevelMessages . length
1425-
1426- const handleLoadPreviousMessages = useCallback ( ( ) => {
1427- setVisibleMessageCount ( ( prev ) => prev + MESSAGE_BATCH_SIZE )
1428- } , [ ] )
1259+ // visibleTopLevelMessages, hiddenMessageCount, handleLoadPreviousMessages come from useChatMessages hook
14291260
14301261 const modeConfig = getInputModeConfig ( inputMode )
14311262 const hasSlashSuggestions =
0 commit comments