Skip to content

Commit d36ebf9

Browse files
committed
refactor(cli): improve send-message hook architecture (Commit 2.1)
- Extract useMessageExecution hook for SDK execution logic - Extract useRunStatePersistence hook for run state management - Create agent-resolution.ts utilities (resolveAgent, buildPromptWithContext) - Add send-message.ts helpers for streaming context and error handling - Reduce use-send-message.ts from ~600 to ~350 lines
1 parent eed92fb commit d36ebf9

File tree

5 files changed

+494
-141
lines changed

5 files changed

+494
-141
lines changed

cli/src/hooks/helpers/send-message.ts

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,8 @@ export const handleRunCompletion = (params: {
373373
})
374374
}
375375

376-
export const handleRunError = (params: {
377-
error: unknown
376+
export type HandleExecutionFailureParams = {
377+
errorMessage: string
378378
timerController: SendMessageTimerController
379379
updater: BatchedMessageUpdater
380380
setIsRetrying: (value: boolean) => void
@@ -383,9 +383,17 @@ export const handleRunError = (params: {
383383
updateChainInProgress: (value: boolean) => void
384384
isProcessingQueueRef?: MutableRefObject<boolean>
385385
isQueuePausedRef?: MutableRefObject<boolean>
386-
}) => {
386+
}
387+
388+
/**
389+
* Handles execution failures from executeMessage returning { success: false }.
390+
* Marks the AI message with an error, finalizes queue state, and stops the timer.
391+
*/
392+
export const handleExecutionFailure = (
393+
params: HandleExecutionFailureParams,
394+
): void => {
387395
const {
388-
error,
396+
errorMessage,
389397
timerController,
390398
updater,
391399
setIsRetrying,
@@ -396,9 +404,6 @@ export const handleRunError = (params: {
396404
isQueuePausedRef,
397405
} = params
398406

399-
const errorInfo = getErrorObject(error, { includeRawError: true })
400-
401-
logger.error({ error: errorInfo }, 'SDK client.run() failed')
402407
setIsRetrying(false)
403408
finalizeQueueState({
404409
setStreamStatus,
@@ -408,15 +413,63 @@ export const handleRunError = (params: {
408413
isQueuePausedRef,
409414
})
410415
timerController.stop('error')
416+
updater.setError(errorMessage)
417+
}
418+
419+
export const handleRunError = (params: {
420+
error: unknown
421+
timerController: SendMessageTimerController
422+
updater: BatchedMessageUpdater
423+
setIsRetrying: (value: boolean) => void
424+
setStreamStatus: (status: StreamStatus) => void
425+
setCanProcessQueue: (can: boolean) => void
426+
updateChainInProgress: (value: boolean) => void
427+
isProcessingQueueRef?: MutableRefObject<boolean>
428+
isQueuePausedRef?: MutableRefObject<boolean>
429+
}) => {
430+
const {
431+
error,
432+
timerController,
433+
updater,
434+
setIsRetrying,
435+
setStreamStatus,
436+
setCanProcessQueue,
437+
updateChainInProgress,
438+
isProcessingQueueRef,
439+
isQueuePausedRef,
440+
} = params
441+
442+
const errorInfo = getErrorObject(error, { includeRawError: true })
443+
444+
logger.error({ error: errorInfo }, 'SDK client.run() failed')
411445

412446
if (isOutOfCreditsError(error)) {
413-
updater.setError(OUT_OF_CREDITS_MESSAGE)
447+
handleExecutionFailure({
448+
errorMessage: OUT_OF_CREDITS_MESSAGE,
449+
timerController,
450+
updater,
451+
setIsRetrying,
452+
setStreamStatus,
453+
setCanProcessQueue,
454+
updateChainInProgress,
455+
isProcessingQueueRef,
456+
isQueuePausedRef,
457+
})
414458
useChatStore.getState().setInputMode('outOfCredits')
415459
invalidateActivityQuery(usageQueryKeys.current())
416460
return
417461
}
418462

419-
// Use setError for all errors so they display in UserErrorBanner consistently
420463
const errorMessage = errorInfo.message || 'An unexpected error occurred'
421-
updater.setError(errorMessage)
464+
handleExecutionFailure({
465+
errorMessage,
466+
timerController,
467+
updater,
468+
setIsRetrying,
469+
setStreamStatus,
470+
setCanProcessQueue,
471+
updateChainInProgress,
472+
isProcessingQueueRef,
473+
isQueuePausedRef,
474+
})
422475
}
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/**
2+
* Hook for core SDK message execution.
3+
* Handles agent resolution, client acquisition, and SDK run execution.
4+
*/
5+
6+
import { useCallback } from 'react'
7+
8+
import {
9+
resolveAgent,
10+
buildPromptWithContext,
11+
} from '../utils/agent-resolution'
12+
import { getCodebuffClient } from '../utils/codebuff-client'
13+
import { createEventHandlerState } from '../utils/create-event-handler-state'
14+
import { createRunConfig } from '../utils/create-run-config'
15+
import { loadAgentDefinitions } from '../utils/local-agent-registry'
16+
import { logger } from '../utils/logger'
17+
18+
import type { StreamController } from './stream-state'
19+
import type { StreamStatus } from './use-message-queue'
20+
import type { AgentMode } from '../utils/constants'
21+
import type { MessageUpdater } from '../utils/message-updater'
22+
import type { MessageContent, RunState } from '@codebuff/sdk'
23+
import type { MutableRefObject } from 'react'
24+
25+
// -----------------------------------------------------------------------------
26+
// Types
27+
// -----------------------------------------------------------------------------
28+
29+
/** Core message data to be sent */
30+
export interface MessageData {
31+
/** The final prompt content to send */
32+
prompt: string
33+
/** Optional bash context to prepend to the prompt */
34+
bashContext: string
35+
/** Message content (images, etc.) */
36+
messageContent: MessageContent[] | undefined
37+
/** Current agent mode (DEFAULT, MAX, PLAN) */
38+
agentMode: AgentMode
39+
}
40+
41+
/** Context for managing streaming state and UI updates */
42+
export interface StreamingContext {
43+
/** AI message ID for the response */
44+
aiMessageId: string
45+
/** Stream controller for managing stream state */
46+
streamRefs: StreamController
47+
/** Message updater for updating AI message blocks */
48+
updater: MessageUpdater
49+
/** Ref tracking whether content has been received */
50+
hasReceivedContentRef: MutableRefObject<boolean>
51+
}
52+
53+
/** Context for SDK execution */
54+
export interface ExecutionContext {
55+
/** Previous run state for continuation */
56+
previousRunState: RunState | null
57+
/** Abort signal for cancellation */
58+
signal: AbortSignal
59+
}
60+
61+
export interface StreamingCallbacks {
62+
setStreamingAgents: (updater: (prev: Set<string>) => Set<string>) => void
63+
setStreamStatus: (status: StreamStatus) => void
64+
setHasReceivedPlanResponse: (value: boolean) => void
65+
setIsRetrying: (value: boolean) => void
66+
}
67+
68+
export interface SubagentCallbacks {
69+
addActiveSubagent: (id: string) => void
70+
removeActiveSubagent: (id: string) => void
71+
}
72+
73+
export interface ExecuteMessageParams {
74+
/** Core message data */
75+
message: MessageData
76+
/** Streaming state and UI update context */
77+
streaming: StreamingContext
78+
/** SDK execution context */
79+
execution: ExecutionContext
80+
/** Callbacks for streaming state updates */
81+
streamingCallbacks: StreamingCallbacks
82+
/** Callbacks for subagent tracking */
83+
subagentCallbacks: SubagentCallbacks
84+
/** Callback for tracking total cost */
85+
onTotalCost?: (cost: number) => void
86+
}
87+
88+
export interface ExecuteMessageResult {
89+
success: true
90+
runState: RunState
91+
}
92+
93+
export interface ExecuteMessageError {
94+
success: false
95+
error: 'no_client' | 'execution_error'
96+
message?: string
97+
}
98+
99+
export type ExecuteMessageOutcome = ExecuteMessageResult | ExecuteMessageError
100+
101+
export interface UseMessageExecutionOptions {
102+
/** Explicit agent ID to use (overrides mode-based selection) */
103+
agentId?: string
104+
}
105+
106+
export interface UseMessageExecutionReturn {
107+
/** Execute a message and return the run state or error */
108+
executeMessage: (params: ExecuteMessageParams) => Promise<ExecuteMessageOutcome>
109+
}
110+
111+
/**
112+
* Hook for executing messages via the SDK.
113+
* Encapsulates agent resolution, client acquisition, and run execution.
114+
*/
115+
export function useMessageExecution({
116+
agentId,
117+
}: UseMessageExecutionOptions): UseMessageExecutionReturn {
118+
const executeMessage = useCallback(
119+
async (params: ExecuteMessageParams): Promise<ExecuteMessageOutcome> => {
120+
const {
121+
message,
122+
streaming,
123+
execution,
124+
streamingCallbacks,
125+
subagentCallbacks,
126+
onTotalCost,
127+
} = params
128+
129+
// Destructure from grouped objects
130+
const { prompt, bashContext, messageContent, agentMode } = message
131+
const { aiMessageId, streamRefs, updater, hasReceivedContentRef } = streaming
132+
const { previousRunState, signal } = execution
133+
134+
// Get SDK client
135+
const client = await getCodebuffClient()
136+
137+
if (!client) {
138+
logger.error(
139+
{},
140+
'[message-execution] No Codebuff client available. Please ensure you are authenticated.',
141+
)
142+
return {
143+
success: false,
144+
error: 'no_client',
145+
message:
146+
'Unable to connect to Codebuff. Please check your authentication and try again.',
147+
}
148+
}
149+
150+
// Resolve agent and build prompt
151+
const agentDefinitions = loadAgentDefinitions()
152+
const resolvedAgent = resolveAgent(agentMode, agentId, agentDefinitions)
153+
154+
const promptWithBashContext = bashContext
155+
? bashContext + prompt
156+
: prompt
157+
const effectivePrompt = buildPromptWithContext(
158+
promptWithBashContext,
159+
messageContent,
160+
)
161+
162+
// Create event handler state
163+
const eventHandlerState = createEventHandlerState({
164+
streamRefs,
165+
setStreamingAgents: streamingCallbacks.setStreamingAgents,
166+
setStreamStatus: streamingCallbacks.setStreamStatus,
167+
aiMessageId,
168+
updater,
169+
hasReceivedContentRef,
170+
addActiveSubagent: subagentCallbacks.addActiveSubagent,
171+
removeActiveSubagent: subagentCallbacks.removeActiveSubagent,
172+
agentMode,
173+
setHasReceivedPlanResponse:
174+
streamingCallbacks.setHasReceivedPlanResponse,
175+
logger,
176+
setIsRetrying: streamingCallbacks.setIsRetrying,
177+
onTotalCost,
178+
})
179+
180+
// Create run config
181+
const runConfig = createRunConfig({
182+
logger,
183+
agent: resolvedAgent,
184+
prompt: effectivePrompt,
185+
content: messageContent,
186+
previousRunState,
187+
agentDefinitions,
188+
eventHandlerState,
189+
signal,
190+
})
191+
192+
logger.info({ runConfig }, '[message-execution] Executing SDK run')
193+
194+
// Execute the run with error handling
195+
try {
196+
const runState = await client.run(runConfig)
197+
198+
return {
199+
success: true,
200+
runState,
201+
}
202+
} catch (error) {
203+
const errorMessage =
204+
error instanceof Error ? error.message : 'Unknown execution error'
205+
logger.error(
206+
{ error },
207+
'[message-execution] SDK run execution failed',
208+
)
209+
return {
210+
success: false,
211+
error: 'execution_error',
212+
message: errorMessage,
213+
}
214+
}
215+
},
216+
[agentId],
217+
)
218+
219+
return {
220+
executeMessage,
221+
}
222+
}

0 commit comments

Comments
 (0)