| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { useEffect, useRef } from 'react' |
| import { friendService } from '../friend/FriendService.js' |
| import { getContentText } from '../utils/messages.js' |
| import type { Message } from '../types/message.js' |
|
|
| type Props = { |
| messages: Message[] |
| isLoading: boolean |
| } |
|
|
| const FRIEND_CHANNEL_SERVER = 'friend' |
|
|
| type ActiveFriendTurn = { |
| responseParts: string[] |
| |
| |
| hasToolUse: boolean |
| } |
|
|
| export function useFriendBridge({ messages, isLoading }: Props): void { |
| const pendingInboundRef = useRef<number>(0) |
| const activeTurnRef = useRef<ActiveFriendTurn | null>(null) |
| const lastProcessedMessageCountRef = useRef(messages.length) |
| const previousLoadingRef = useRef(isLoading) |
|
|
| |
| useEffect(() => { |
| return friendService.subscribeToInbound(event => { |
| pendingInboundRef.current++ |
| }) |
| }, []) |
|
|
| |
| |
| useEffect(() => { |
| const newMessages = messages.slice(lastProcessedMessageCountRef.current) |
|
|
| for (const message of newMessages) { |
| if ( |
| message.type === 'user' && |
| typeof message.origin === 'object' && |
| message.origin !== null && |
| (message.origin as Record<string, unknown>).kind === 'channel' && |
| (message.origin as Record<string, unknown>).server === FRIEND_CHANNEL_SERVER |
| ) { |
| |
| if (pendingInboundRef.current > 0) { |
| pendingInboundRef.current-- |
| activeTurnRef.current = { |
| responseParts: [], |
| hasToolUse: false, |
| } |
| } |
| continue |
| } |
|
|
| if (message.type === 'assistant' && activeTurnRef.current) { |
| const content = message.message.content |
| const text = getContentText(content) |
| if (text) { |
| activeTurnRef.current.responseParts.push(text) |
| } |
| |
| activeTurnRef.current.hasToolUse = Array.isArray(content) && |
| content.some((b: any) => b.type === 'tool_use') |
| continue |
| } |
|
|
| if ( |
| message.type === 'system' && |
| message.subtype === 'local_command' && |
| activeTurnRef.current |
| ) { |
| const text = message.content |
| if (text) { |
| activeTurnRef.current.responseParts.push(text) |
| } |
| } |
| } |
|
|
| lastProcessedMessageCountRef.current = messages.length |
| }, [messages]) |
|
|
| |
| useEffect(() => { |
| const wasLoading = previousLoadingRef.current |
| previousLoadingRef.current = isLoading |
|
|
| if (!wasLoading || isLoading || !activeTurnRef.current) return |
|
|
| |
| |
| if (activeTurnRef.current.hasToolUse) return |
|
|
| const completedTurn = activeTurnRef.current |
| activeTurnRef.current = null |
|
|
| void (async () => { |
| try { |
| const reply = |
| completedTurn.responseParts.join('\n\n').trim() |
|
|
| if (reply) { |
| await friendService.broadcastResponse(reply) |
| } |
| } catch (error) { |
| console.warn( |
| '[friend] failed to broadcast reply:', |
| error instanceof Error ? error.message : String(error), |
| ) |
| } |
| })() |
| }, [isLoading]) |
| } |
|
|