Skip to content

Commit e3744f7

Browse files
committed
refactor(cli): extract chat state management from chat.tsx (Commit 1.1a)
- Create cli/src/hooks/use-chat-state.ts: encapsulates Zustand store selectors, streamingAgents stabilization, refs (activeAgentStreamsRef, isChainInProgressRef, activeSubagentsRef, abortControllerRef, sendMessageRef), and sync effects - Create cli/src/hooks/use-chat-messages.ts: extracts message tree building, pagination (MESSAGE_BATCH_SIZE, visibleMessageCount), collapse toggle handling, and isUserCollapsing ref management - Create cli/src/types/chat-state.ts: re-exports types from extracted hooks - Update cli/src/chat.tsx to use new hooks, reducing component complexity Part of Wave 2 refactoring plan - Phase 1 critical path
1 parent ba5871f commit e3744f7

File tree

8 files changed

+631
-206
lines changed

8 files changed

+631
-206
lines changed

REFACTORING_PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ This document outlines a prioritized refactoring plan for the 51 issues identifi
2121
### Phase 1 Progress
2222
| Commit | Description | Status | Completed By |
2323
|--------|-------------|--------|-------------|
24-
| 1.1a | Extract chat state management | ⬜ Not Started | - |
24+
| 1.1a | Extract chat state management | ✅ Complete | Codex CLI |
2525
| 1.1b | Extract chat UI and orchestration | ⬜ Not Started | - |
2626
| 1.2 | Refactor context-pruner god function | ✅ Complete | Codex CLI |
2727
| 1.3 | Split old-constants.ts god module | ✅ Complete | Codex CLI |

cli/src/chat.tsx

Lines changed: 29 additions & 198 deletions
Original file line numberDiff line numberDiff line change
@@ -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'
3739
import { useClipboard } from './hooks/use-clipboard'
3840
import { useConnectionStatus } from './hooks/use-connection-status'
3941
import { useElapsedTime } from './hooks/use-elapsed-time'
@@ -75,7 +77,7 @@ import {
7577
createDefaultChatKeyboardState,
7678
} from './utils/keyboard-actions'
7779
import { loadLocalAgents } from './utils/local-agent-registry'
78-
import { buildMessageTree } from './utils/message-tree-utils'
80+
// buildMessageTree is now used internally by useChatMessages hook
7981
import {
8082
getStatusIndicatorState,
8183
type AuthStatus,
@@ -90,8 +92,8 @@ import { logger } from './utils/logger'
9092

9193
import type { CommandResult } from './commands/command-registry'
9294
import 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
9597
import type { User } from './utils/auth'
9698
import type { AgentMode } from './utils/constants'
9799
import 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

Comments
 (0)