Skip to content

Commit fd33bc7

Browse files
committed
refactor(cli): extract chat UI and streaming hooks (Commit 1.1b)
- Create use-chat-ui.ts: scroll behavior, terminal dimensions, theme - Create use-chat-streaming.ts: connection status, timer, queue management - Update chat.tsx to use the new extracted hooks - Uses existing useExitHandler hook (not reimplementing) - Omit chat-orchestrator.tsx (reviewers agreed it was dead code) - Mark Commit 1.1b complete in REFACTORING_PLAN.md
1 parent dc74cce commit fd33bc7

File tree

4 files changed

+408
-163
lines changed

4 files changed

+408
-163
lines changed

REFACTORING_PLAN.md

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

cli/src/chat.tsx

Lines changed: 41 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
2-
import { RECONNECTION_MESSAGE_DURATION_MS } from '@codebuff/sdk'
32
import open from 'open'
4-
import { useQueryClient } from '@tanstack/react-query'
53
import {
64
useCallback,
75
useEffect,
86
useLayoutEffect,
97
useMemo,
108
useRef,
119
useState,
12-
useTransition,
1310
} from 'react'
1411
import { useShallow } from 'zustand/react/shallow'
1512

@@ -27,7 +24,6 @@ import { TopBanner } from './components/top-banner'
2724
import { SLASH_COMMANDS } from './data/slash-commands'
2825
import { useAgentValidation } from './hooks/use-agent-validation'
2926
import { useAskUserBridge } from './hooks/use-ask-user-bridge'
30-
import { authQueryKeys } from './hooks/use-auth-query'
3127
import { useChatInput } from './hooks/use-chat-input'
3228
import { useClaudeQuotaQuery } from './hooks/use-claude-quota-query'
3329
import {
@@ -36,24 +32,16 @@ import {
3632
} from './hooks/use-chat-keyboard'
3733
import { useChatMessages } from './hooks/use-chat-messages'
3834
import { useChatState } from './hooks/use-chat-state'
35+
import { useChatStreaming } from './hooks/use-chat-streaming'
36+
import { useChatUI } from './hooks/use-chat-ui'
3937
import { useClipboard } from './hooks/use-clipboard'
40-
import { useConnectionStatus } from './hooks/use-connection-status'
41-
import { useElapsedTime } from './hooks/use-elapsed-time'
4238
import { useGravityAd } from './hooks/use-gravity-ad'
4339
import { useEvent } from './hooks/use-event'
44-
import { useExitHandler } from './hooks/use-exit-handler'
4540
import { 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'
4742
import { 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'
5143
import { useSendMessage } from './hooks/use-send-message'
5244
import { 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'
5745
import { useUsageMonitor } from './hooks/use-usage-monitor'
5846
import { WEBSITE_URL } from './login/constants'
5947
import { getProjectRoot } from './project-files'
@@ -65,10 +53,8 @@ import { usePublishStore } from './state/publish-store'
6553
import {
6654
addClipboardPlaceholder,
6755
addPendingImageFromFile,
68-
capturePendingAttachments,
6956
validateAndAddImage,
7057
} from './utils/pending-attachments'
71-
import { createChatScrollAcceleration } from './utils/chat-scroll-accel'
7258
import { showClipboardMessage } from './utils/clipboard'
7359
import { readClipboardImage } from './utils/clipboard-image'
7460
import { getInputModeConfig } from './utils/input-modes'
@@ -77,23 +63,19 @@ import {
7763
createDefaultChatKeyboardState,
7864
} from './utils/keyboard-actions'
7965
import { loadLocalAgents } from './utils/local-agent-registry'
80-
// buildMessageTree is now used internally by useChatMessages hook
8166
import {
8267
getStatusIndicatorState,
8368
type AuthStatus,
8469
} from './utils/status-indicator-state'
8570
import { getClaudeOAuthStatus } from './utils/claude-oauth'
8671
import { createPasteHandler } from './utils/strings'
8772
import { computeInputLayoutMetrics } from './utils/text-layout'
88-
import { createMarkdownPalette } from './utils/theme-system'
8973
import { reportActivity } from './utils/activity-tracker'
9074
import { trackEvent } from './utils/analytics'
9175
import { logger } from './utils/logger'
9276

9377
import type { CommandResult } from './commands/command-registry'
9478
import type { MultilineInputHandle } from './components/multiline-input'
95-
96-
// SendMessageFn type is now used internally by useChatState hook
9779
import type { User } from './utils/auth'
9880
import type { AgentMode } from './utils/constants'
9981
import 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

Comments
 (0)