11import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
2- import { RECONNECTION_MESSAGE_DURATION_MS } from '@codebuff/sdk'
32import open from 'open'
4- import { useQueryClient } from '@tanstack/react-query'
53import {
64 useCallback ,
75 useEffect ,
86 useLayoutEffect ,
97 useMemo ,
108 useRef ,
119 useState ,
12- useTransition ,
1310} from 'react'
1411import { useShallow } from 'zustand/react/shallow'
1512
@@ -27,7 +24,6 @@ import { TopBanner } from './components/top-banner'
2724import { SLASH_COMMANDS } from './data/slash-commands'
2825import { useAgentValidation } from './hooks/use-agent-validation'
2926import { useAskUserBridge } from './hooks/use-ask-user-bridge'
30- import { authQueryKeys } from './hooks/use-auth-query'
3127import { useChatInput } from './hooks/use-chat-input'
3228import { useClaudeQuotaQuery } from './hooks/use-claude-quota-query'
3329import {
@@ -36,24 +32,16 @@ import {
3632} from './hooks/use-chat-keyboard'
3733import { useChatMessages } from './hooks/use-chat-messages'
3834import { useChatState } from './hooks/use-chat-state'
35+ import { useChatStreaming } from './hooks/use-chat-streaming'
36+ import { useChatUI } from './hooks/use-chat-ui'
3937import { useClipboard } from './hooks/use-clipboard'
40- import { useConnectionStatus } from './hooks/use-connection-status'
41- import { useElapsedTime } from './hooks/use-elapsed-time'
4238import { useGravityAd } from './hooks/use-gravity-ad'
4339import { useEvent } from './hooks/use-event'
44- import { useExitHandler } from './hooks/use-exit-handler'
4540import { useInputHistory } from './hooks/use-input-history'
46- import { useMessageQueue , type QueuedMessage } from './hooks/use-message-queue'
41+ import { type QueuedMessage } from './hooks/use-message-queue'
4742import { usePublishMutation } from './hooks/use-publish-mutation'
48- import { useQueueControls } from './hooks/use-queue-controls'
49- import { useQueueUi } from './hooks/use-queue-ui'
50- import { useChatScrollbox } from './hooks/use-scroll-management'
5143import { useSendMessage } from './hooks/use-send-message'
5244import { useSuggestionEngine } from './hooks/use-suggestion-engine'
53- import { useTerminalDimensions } from './hooks/use-terminal-dimensions'
54- import { useTerminalLayout } from './hooks/use-terminal-layout'
55- import { useTheme } from './hooks/use-theme'
56- import { useTimeout } from './hooks/use-timeout'
5745import { useUsageMonitor } from './hooks/use-usage-monitor'
5846import { WEBSITE_URL } from './login/constants'
5947import { getProjectRoot } from './project-files'
@@ -65,10 +53,8 @@ import { usePublishStore } from './state/publish-store'
6553import {
6654 addClipboardPlaceholder ,
6755 addPendingImageFromFile ,
68- capturePendingAttachments ,
6956 validateAndAddImage ,
7057} from './utils/pending-attachments'
71- import { createChatScrollAcceleration } from './utils/chat-scroll-accel'
7258import { showClipboardMessage } from './utils/clipboard'
7359import { readClipboardImage } from './utils/clipboard-image'
7460import { getInputModeConfig } from './utils/input-modes'
@@ -77,23 +63,19 @@ import {
7763 createDefaultChatKeyboardState ,
7864} from './utils/keyboard-actions'
7965import { loadLocalAgents } from './utils/local-agent-registry'
80- // buildMessageTree is now used internally by useChatMessages hook
8166import {
8267 getStatusIndicatorState ,
8368 type AuthStatus ,
8469} from './utils/status-indicator-state'
8570import { getClaudeOAuthStatus } from './utils/claude-oauth'
8671import { createPasteHandler } from './utils/strings'
8772import { computeInputLayoutMetrics } from './utils/text-layout'
88- import { createMarkdownPalette } from './utils/theme-system'
8973import { reportActivity } from './utils/activity-tracker'
9074import { trackEvent } from './utils/analytics'
9175import { logger } from './utils/logger'
9276
9377import type { CommandResult } from './commands/command-registry'
9478import type { MultilineInputHandle } from './components/multiline-input'
95-
96- // SendMessageFn type is now used internally by useChatState hook
9779import type { User } from './utils/auth'
9880import type { AgentMode } from './utils/constants'
9981import type { FileTreeNode } from '@codebuff/common/util/file'
@@ -132,29 +114,8 @@ export const Chat = ({
132114 gitRoot ?: string | null
133115 onSwitchToGitRoot ?: ( ) => void
134116} ) => {
135- const scrollRef = useRef < ScrollBoxRenderable | null > ( null )
136- const [ hasOverflow , setHasOverflow ] = useState ( false )
137- const hasOverflowRef = useRef ( false )
138-
139- // Message handling extracted to useChatMessages hook (initialized below after streamStatus is available)
140-
141- const queryClient = useQueryClient ( )
142- const [ , startUiTransition ] = useTransition ( )
143-
144- const [ showReconnectionMessage , setShowReconnectionMessage ] = useState ( false )
145- const reconnectionTimeout = useTimeout ( )
146117 const [ forceFileOnlyMentions , setForceFileOnlyMentions ] = useState ( false )
147118
148- const { separatorWidth, terminalWidth, terminalHeight } =
149- useTerminalDimensions ( )
150- const { height : heightLayout , width : widthLayout } = useTerminalLayout ( )
151- const isCompactHeight = heightLayout . is ( 'xs' )
152- const isNarrowWidth = widthLayout . is ( 'xs' )
153- const messageAvailableWidth = separatorWidth
154-
155- const theme = useTheme ( )
156- const markdownPalette = useMemo ( ( ) => createMarkdownPalette ( theme ) , [ theme ] )
157-
158119 const { validate : validateAgents } = useAgentValidation ( )
159120
160121 // Subscribe to ask_user bridge to trigger form display
@@ -197,35 +158,7 @@ export const Chat = ({
197158 } = useChatState ( )
198159
199160 const { statusMessage } = useClipboard ( )
200-
201- const handleReconnection = useCallback (
202- ( isInitialConnection : boolean ) => {
203- // Invalidate auth queries so we refetch with current credentials
204- queryClient . invalidateQueries ( { queryKey : authQueryKeys . all } )
205-
206- startUiTransition ( ( ) => {
207- if ( ! isInitialConnection ) {
208- setShowReconnectionMessage ( true )
209- reconnectionTimeout . setTimeout (
210- 'reconnection-message' ,
211- ( ) => {
212- startUiTransition ( ( ) => {
213- setShowReconnectionMessage ( false )
214- } )
215- } ,
216- RECONNECTION_MESSAGE_DURATION_MS ,
217- )
218- }
219- } )
220- } ,
221- [ queryClient , reconnectionTimeout , startUiTransition ] ,
222- )
223-
224- const isConnected = useConnectionStatus ( handleReconnection )
225- const mainAgentTimer = useElapsedTime ( )
226161 const { ad } = useGravityAd ( )
227- // Use startTime for active timer display; when paused, timer hook maintains frozen value
228- const timerStartTime = mainAgentTimer . startTime
229162
230163 // Set initial mode from CLI flag on mount
231164 useEffect ( ( ) => {
@@ -245,61 +178,30 @@ export const Chat = ({
245178 handleLoadPreviousMessages,
246179 } = useChatMessages ( { messages, setMessages } )
247180
248- const { scrollToLatest, scrollUp, scrollDown, scrollboxProps, isAtBottom } = useChatScrollbox (
181+ // Use extracted UI hook for scroll, terminal dimensions, and theme
182+ const {
249183 scrollRef,
250- messages ,
251- isUserCollapsing ,
252- )
253-
254- // Check if content has overflowed and needs scrolling
255- useEffect ( ( ) => {
256- const scrollbox = scrollRef . current
257- if ( ! scrollbox ) return
258-
259- const checkOverflow = ( ) => {
260- const contentHeight = scrollbox . scrollHeight
261- const viewportHeight = scrollbox . viewport . height
262- const isOverflowing = contentHeight > viewportHeight
263-
264- // Only update state if overflow status actually changed
265- if ( hasOverflowRef . current !== isOverflowing ) {
266- hasOverflowRef . current = isOverflowing
267- setHasOverflow ( isOverflowing )
268- }
269- }
270-
271- // Check initially and whenever scroll state changes
272- checkOverflow ( )
273- scrollbox . verticalScrollBar . on ( 'change' , checkOverflow )
274-
275- return ( ) => {
276- scrollbox . verticalScrollBar . off ( 'change' , checkOverflow )
277- }
278- } , [ ] )
279-
280- const inertialScrollAcceleration = useMemo (
281- ( ) => createChatScrollAcceleration ( ) ,
282- [ ] ,
283- )
284-
285- const appliedScrollboxProps = inertialScrollAcceleration
286- ? { ...scrollboxProps , scrollAcceleration : inertialScrollAcceleration }
287- : scrollboxProps
184+ scrollToLatest,
185+ scrollUp,
186+ scrollDown,
187+ appliedScrollboxProps,
188+ isAtBottom,
189+ hasOverflow,
190+ terminalWidth,
191+ terminalHeight,
192+ separatorWidth,
193+ messageAvailableWidth,
194+ isCompactHeight,
195+ isNarrowWidth,
196+ theme,
197+ markdownPalette,
198+ } = useChatUI ( { messages, isUserCollapsing } )
288199
289200 const localAgents = useMemo ( ( ) => loadLocalAgents ( agentMode ) , [ agentMode ] )
290201 const inputMode = useChatStore ( ( state ) => state . inputMode )
291202 const setInputMode = useChatStore ( ( state ) => state . setInputMode )
292203 const askUserState = useChatStore ( ( state ) => state . askUserState )
293204
294- // Pause/resume timer when ask_user tool becomes active/inactive
295- useEffect ( ( ) => {
296- if ( askUserState !== null ) {
297- mainAgentTimer . pause ( )
298- } else if ( mainAgentTimer . isPaused ) {
299- mainAgentTimer . resume ( )
300- }
301- } , [ askUserState , mainAgentTimer ] )
302-
303205 // Filter slash commands based on current ads state - only show the option that changes state
304206 const filteredSlashCommands = useMemo ( ( ) => {
305207 const adsEnabled = getAdsEnabled ( )
@@ -421,62 +323,46 @@ export const Chat = ({
421323 { inputMode, setInputMode } ,
422324 )
423325
326+ // Use extracted streaming hook for connection, timer, queue, and exit handling
424327 const {
425- queuedMessages,
328+ isConnected,
329+ showReconnectionMessage,
330+ mainAgentTimer,
331+ timerStartTime,
426332 streamStatus,
333+ isWaitingForResponse,
334+ isStreaming,
335+ setStreamStatus,
336+ queuedMessages,
427337 queuePaused,
428338 streamMessageIdRef,
429339 addToQueue,
430340 stopStreaming,
431- setStreamStatus,
432341 setCanProcessQueue,
433342 pauseQueue,
434343 resumeQueue,
435344 clearQueue,
436345 isQueuePausedRef,
437346 isProcessingQueueRef,
438- } = useMessageQueue (
439- ( message : QueuedMessage ) =>
440- sendMessageRef . current ?.( {
441- content : message . content ,
442- agentMode,
443- attachments : message . attachments ,
444- } ) ?? Promise . resolve ( ) ,
445- isChainInProgressRef ,
446- activeAgentStreamsRef ,
447- )
448-
449- const {
450347 queuedCount,
451348 shouldShowQueuePreview,
452349 queuePreviewTitle,
453350 pausedQueueText,
454351 inputPlaceholder,
455- } = useQueueUi ( {
456- queuePaused,
457- queuedMessages,
458- separatorWidth,
459- terminalWidth,
460- } )
461-
462- const { handleCtrlC : baseHandleCtrlC , nextCtrlCWillExit } = useExitHandler ( {
352+ handleCtrlC,
353+ ensureQueueActiveBeforeSubmit,
354+ nextCtrlCWillExit,
355+ } = useChatStreaming ( {
356+ agentMode,
463357 inputValue,
464358 setInputValue,
359+ terminalWidth,
360+ separatorWidth,
361+ isChainInProgressRef,
362+ activeAgentStreamsRef,
363+ sendMessageRef,
465364 } )
466365
467- const { handleCtrlC, ensureQueueActiveBeforeSubmit } = useQueueControls ( {
468- queuePaused,
469- queuedCount,
470- clearQueue,
471- resumeQueue,
472- inputHasText : Boolean ( inputValue ) ,
473- baseHandleCtrlC,
474- } )
475-
476- // Derive boolean flags from streamStatus for convenience
477- const isWaitingForResponse = streamStatus === 'waiting'
478- const isStreaming = streamStatus !== 'idle'
479-
480366 // When streaming completes, flush any pending bash commands into history (ghost mode only)
481367 // Non-ghost mode commands are already in history and will be cleared when user sends next message
482368 useEffect ( ( ) => {
@@ -516,9 +402,6 @@ export const Chat = ({
516402 }
517403 } , [ isStreaming , pendingBashMessages , setMessages ] )
518404
519- // Timer events are currently tracked but not used for UI updates
520- // Future: Could be used for analytics or debugging
521-
522405 const { sendMessage, clearMessages } = useSendMessage ( {
523406 inputRef,
524407 activeSubagentsRef,
@@ -530,7 +413,7 @@ export const Chat = ({
530413 onBeforeMessageSend : validateAgents ,
531414 mainAgentTimer,
532415 scrollToLatest,
533- onTimerEvent : ( ) => { } , // No-op for now
416+ onTimerEvent : ( ) => { } ,
534417 isQueuePausedRef,
535418 isProcessingQueueRef,
536419 resumeQueue,
@@ -1207,8 +1090,6 @@ export const Chat = ({
12071090 disabled : askUserState !== null ,
12081091 } )
12091092
1210- // messageTree and topLevelMessages now come from useChatMessages hook
1211-
12121093 // Sync message block context to zustand store for child components
12131094 const setMessageBlockContext = useMessageBlockStore (
12141095 ( state ) => state . setContext ,
@@ -1256,8 +1137,6 @@ export const Chat = ({
12561137 setMessageBlockCallbacks ,
12571138 ] )
12581139
1259- // visibleTopLevelMessages, hiddenMessageCount, handleLoadPreviousMessages come from useChatMessages hook
1260-
12611140 const modeConfig = getInputModeConfig ( inputMode )
12621141 const hasSlashSuggestions =
12631142 slashContext . active &&
@@ -1355,7 +1234,7 @@ export const Chat = ({
13551234 } }
13561235 >
13571236 < scrollbox
1358- ref = { scrollRef }
1237+ ref = { scrollRef as React . Ref < ScrollBoxRenderable > }
13591238 stickyScroll
13601239 stickyStart = "bottom"
13611240 scrollX = { false }
0 commit comments