| import { randomUUID } from 'crypto' |
| import { useCallback, useEffect, useRef } from 'react' |
| import { useInterval } from 'usehooks-ts' |
| import type { ToolUseConfirm } from '../components/permissions/PermissionRequest.js' |
| import { TEAMMATE_MESSAGE_TAG } from '../constants/xml.js' |
| import { useTerminalNotification } from '../ink/useTerminalNotification.js' |
| import { sendNotification } from '../services/notifier.js' |
| import { |
| type AppState, |
| useAppState, |
| useAppStateStore, |
| useSetAppState, |
| } from '../state/AppState.js' |
| import { findToolByName } from '../Tool.js' |
| import { isInProcessTeammateTask } from '../tasks/InProcessTeammateTask/types.js' |
| import { getAllBaseTools } from '../tools.js' |
| import type { PermissionUpdate } from '../types/permissions.js' |
| import { logForDebugging } from '../utils/debug.js' |
| import { |
| findInProcessTeammateTaskId, |
| handlePlanApprovalResponse, |
| } from '../utils/inProcessTeammateHelpers.js' |
| import { createAssistantMessage } from '../utils/messages.js' |
| import { |
| permissionModeFromString, |
| toExternalPermissionMode, |
| } from '../utils/permissions/PermissionMode.js' |
| import { applyPermissionUpdate } from '../utils/permissions/PermissionUpdate.js' |
| import { jsonStringify } from '../utils/slowOperations.js' |
| import { isInsideTmux } from '../utils/swarm/backends/detection.js' |
| import { |
| ensureBackendsRegistered, |
| getBackendByType, |
| } from '../utils/swarm/backends/registry.js' |
| import type { PaneBackendType } from '../utils/swarm/backends/types.js' |
| import { TEAM_LEAD_NAME } from '../utils/swarm/constants.js' |
| import { getLeaderToolUseConfirmQueue } from '../utils/swarm/leaderPermissionBridge.js' |
| import { sendPermissionResponseViaMailbox } from '../utils/swarm/permissionSync.js' |
| import { |
| removeTeammateFromTeamFile, |
| setMemberMode, |
| } from '../utils/swarm/teamHelpers.js' |
| import { unassignTeammateTasks } from '../utils/tasks.js' |
| import { |
| getAgentName, |
| isPlanModeRequired, |
| isTeamLead, |
| isTeammate, |
| } from '../utils/teammate.js' |
| import { isInProcessTeammate } from '../utils/teammateContext.js' |
| import { |
| isModeSetRequest, |
| isPermissionRequest, |
| isPermissionResponse, |
| isPlanApprovalRequest, |
| isPlanApprovalResponse, |
| isSandboxPermissionRequest, |
| isSandboxPermissionResponse, |
| isShutdownApproved, |
| isShutdownRequest, |
| isTeamPermissionUpdate, |
| markMessagesAsRead, |
| readUnreadMessages, |
| type TeammateMessage, |
| writeToMailbox, |
| } from '../utils/teammateMailbox.js' |
| import { |
| hasPermissionCallback, |
| hasSandboxPermissionCallback, |
| processMailboxPermissionResponse, |
| processSandboxPermissionResponse, |
| } from './useSwarmPermissionPoller.js' |
|
|
| |
| |
| |
| |
| |
| |
| |
| function getAgentNameToPoll(appState: AppState): string | undefined { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if (isInProcessTeammate()) { |
| return undefined |
| } |
| if (isTeammate()) { |
| return getAgentName() |
| } |
| |
| if (isTeamLead(appState.teamContext)) { |
| const leadAgentId = appState.teamContext!.leadAgentId |
| |
| const leadName = appState.teamContext!.teammates[leadAgentId]?.name |
| return leadName || 'team-lead' |
| } |
| return undefined |
| } |
|
|
| const INBOX_POLL_INTERVAL_MS = 1000 |
|
|
| type Props = { |
| enabled: boolean |
| isLoading: boolean |
| focusedInputDialog: string | undefined |
| |
| |
| onSubmitMessage: (formatted: string) => boolean |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function useInboxPoller({ |
| enabled, |
| isLoading, |
| focusedInputDialog, |
| onSubmitMessage, |
| }: Props): void { |
| |
| const onSubmitTeammateMessage = onSubmitMessage |
| const store = useAppStateStore() |
| const setAppState = useSetAppState() |
| const inboxMessageCount = useAppState(s => s.inbox.messages.length) |
| const terminal = useTerminalNotification() |
|
|
| const poll = useCallback(async () => { |
| if (!enabled) return |
|
|
| |
| const currentAppState = store.getState() |
| const agentName = getAgentNameToPoll(currentAppState) |
| if (!agentName) return |
|
|
| const unread = await readUnreadMessages( |
| agentName, |
| currentAppState.teamContext?.teamName, |
| ) |
|
|
| if (unread.length === 0) return |
|
|
| logForDebugging(`[InboxPoller] Found ${unread.length} unread message(s)`) |
|
|
| |
| |
| if (isTeammate() && isPlanModeRequired()) { |
| for (const msg of unread) { |
| const approvalResponse = isPlanApprovalResponse(msg.text) |
| |
| if (approvalResponse && msg.from === 'team-lead') { |
| logForDebugging( |
| `[InboxPoller] Received plan approval response from team-lead: approved=${approvalResponse.approved}`, |
| ) |
| if (approvalResponse.approved) { |
| |
| const targetMode = approvalResponse.permissionMode ?? 'default' |
|
|
| |
| setAppState(prev => ({ |
| ...prev, |
| toolPermissionContext: applyPermissionUpdate( |
| prev.toolPermissionContext, |
| { |
| type: 'setMode', |
| mode: toExternalPermissionMode(targetMode), |
| destination: 'session', |
| }, |
| ), |
| })) |
| logForDebugging( |
| `[InboxPoller] Plan approved by team lead, exited plan mode to ${targetMode}`, |
| ) |
| } else { |
| logForDebugging( |
| `[InboxPoller] Plan rejected by team lead: ${approvalResponse.feedback || 'No feedback provided'}`, |
| ) |
| } |
| } else if (approvalResponse) { |
| logForDebugging( |
| `[InboxPoller] Ignoring plan approval response from non-team-lead: ${msg.from}`, |
| ) |
| } |
| } |
| } |
|
|
| |
| |
| const markRead = () => { |
| void markMessagesAsRead(agentName, currentAppState.teamContext?.teamName) |
| } |
|
|
| |
| const permissionRequests: TeammateMessage[] = [] |
| const permissionResponses: TeammateMessage[] = [] |
| const sandboxPermissionRequests: TeammateMessage[] = [] |
| const sandboxPermissionResponses: TeammateMessage[] = [] |
| const shutdownRequests: TeammateMessage[] = [] |
| const shutdownApprovals: TeammateMessage[] = [] |
| const teamPermissionUpdates: TeammateMessage[] = [] |
| const modeSetRequests: TeammateMessage[] = [] |
| const planApprovalRequests: TeammateMessage[] = [] |
| const regularMessages: TeammateMessage[] = [] |
|
|
| for (const m of unread) { |
| const permReq = isPermissionRequest(m.text) |
| const permResp = isPermissionResponse(m.text) |
| const sandboxReq = isSandboxPermissionRequest(m.text) |
| const sandboxResp = isSandboxPermissionResponse(m.text) |
| const shutdownReq = isShutdownRequest(m.text) |
| const shutdownApproval = isShutdownApproved(m.text) |
| const teamPermUpdate = isTeamPermissionUpdate(m.text) |
| const modeSetReq = isModeSetRequest(m.text) |
| const planApprovalReq = isPlanApprovalRequest(m.text) |
|
|
| if (permReq) { |
| permissionRequests.push(m) |
| } else if (permResp) { |
| permissionResponses.push(m) |
| } else if (sandboxReq) { |
| sandboxPermissionRequests.push(m) |
| } else if (sandboxResp) { |
| sandboxPermissionResponses.push(m) |
| } else if (shutdownReq) { |
| shutdownRequests.push(m) |
| } else if (shutdownApproval) { |
| shutdownApprovals.push(m) |
| } else if (teamPermUpdate) { |
| teamPermissionUpdates.push(m) |
| } else if (modeSetReq) { |
| modeSetRequests.push(m) |
| } else if (planApprovalReq) { |
| planApprovalRequests.push(m) |
| } else { |
| regularMessages.push(m) |
| } |
| } |
|
|
| |
| if ( |
| permissionRequests.length > 0 && |
| isTeamLead(currentAppState.teamContext) |
| ) { |
| logForDebugging( |
| `[InboxPoller] Found ${permissionRequests.length} permission request(s)`, |
| ) |
|
|
| const setToolUseConfirmQueue = getLeaderToolUseConfirmQueue() |
| const teamName = currentAppState.teamContext?.teamName |
|
|
| for (const m of permissionRequests) { |
| const parsed = isPermissionRequest(m.text) |
| if (!parsed) continue |
|
|
| if (setToolUseConfirmQueue) { |
| |
| |
| |
| const tool = findToolByName(getAllBaseTools(), parsed.tool_name) |
| if (!tool) { |
| logForDebugging( |
| `[InboxPoller] Unknown tool ${parsed.tool_name}, skipping permission request`, |
| ) |
| continue |
| } |
|
|
| const entry: ToolUseConfirm = { |
| assistantMessage: createAssistantMessage({ content: '' }), |
| tool, |
| description: parsed.description, |
| input: parsed.input, |
| toolUseContext: {} as ToolUseConfirm['toolUseContext'], |
| toolUseID: parsed.tool_use_id, |
| permissionResult: { |
| behavior: 'ask', |
| message: parsed.description, |
| }, |
| permissionPromptStartTimeMs: Date.now(), |
| workerBadge: { |
| name: parsed.agent_id, |
| color: 'cyan', |
| }, |
| onUserInteraction() { |
| |
| }, |
| onAbort() { |
| void sendPermissionResponseViaMailbox( |
| parsed.agent_id, |
| { decision: 'rejected', resolvedBy: 'leader' }, |
| parsed.request_id, |
| teamName, |
| ) |
| }, |
| onAllow( |
| updatedInput: Record<string, unknown>, |
| permissionUpdates: PermissionUpdate[], |
| ) { |
| void sendPermissionResponseViaMailbox( |
| parsed.agent_id, |
| { |
| decision: 'approved', |
| resolvedBy: 'leader', |
| updatedInput, |
| permissionUpdates, |
| }, |
| parsed.request_id, |
| teamName, |
| ) |
| }, |
| onReject(feedback?: string) { |
| void sendPermissionResponseViaMailbox( |
| parsed.agent_id, |
| { |
| decision: 'rejected', |
| resolvedBy: 'leader', |
| feedback, |
| }, |
| parsed.request_id, |
| teamName, |
| ) |
| }, |
| async recheckPermission() { |
| |
| }, |
| } |
|
|
| |
| |
| setToolUseConfirmQueue(queue => { |
| if (queue.some(q => q.toolUseID === parsed.tool_use_id)) { |
| return queue |
| } |
| return [...queue, entry] |
| }) |
| } else { |
| logForDebugging( |
| `[InboxPoller] ToolUseConfirmQueue unavailable, dropping permission request from ${parsed.agent_id}`, |
| ) |
| } |
| } |
|
|
| |
| const firstParsed = isPermissionRequest(permissionRequests[0]?.text ?? '') |
| if (firstParsed && !isLoading && !focusedInputDialog) { |
| void sendNotification( |
| { |
| message: `${firstParsed.agent_id} needs permission for ${firstParsed.tool_name}`, |
| notificationType: 'worker_permission_prompt', |
| }, |
| terminal, |
| ) |
| } |
| } |
|
|
| |
| if (permissionResponses.length > 0 && isTeammate()) { |
| logForDebugging( |
| `[InboxPoller] Found ${permissionResponses.length} permission response(s)`, |
| ) |
|
|
| for (const m of permissionResponses) { |
| const parsed = isPermissionResponse(m.text) |
| if (!parsed) continue |
|
|
| if (hasPermissionCallback(parsed.request_id)) { |
| logForDebugging( |
| `[InboxPoller] Processing permission response for ${parsed.request_id}: ${parsed.subtype}`, |
| ) |
|
|
| if (parsed.subtype === 'success') { |
| processMailboxPermissionResponse({ |
| requestId: parsed.request_id, |
| decision: 'approved', |
| updatedInput: parsed.response?.updated_input, |
| permissionUpdates: parsed.response?.permission_updates, |
| }) |
| } else { |
| processMailboxPermissionResponse({ |
| requestId: parsed.request_id, |
| decision: 'rejected', |
| feedback: parsed.error, |
| }) |
| } |
| } |
| } |
| } |
|
|
| |
| if ( |
| sandboxPermissionRequests.length > 0 && |
| isTeamLead(currentAppState.teamContext) |
| ) { |
| logForDebugging( |
| `[InboxPoller] Found ${sandboxPermissionRequests.length} sandbox permission request(s)`, |
| ) |
|
|
| const newSandboxRequests: Array<{ |
| requestId: string |
| workerId: string |
| workerName: string |
| workerColor?: string |
| host: string |
| createdAt: number |
| }> = [] |
|
|
| for (const m of sandboxPermissionRequests) { |
| const parsed = isSandboxPermissionRequest(m.text) |
| if (!parsed) continue |
|
|
| |
| if (!parsed.hostPattern?.host) { |
| logForDebugging( |
| `[InboxPoller] Invalid sandbox permission request: missing hostPattern.host`, |
| ) |
| continue |
| } |
|
|
| newSandboxRequests.push({ |
| requestId: parsed.requestId, |
| workerId: parsed.workerId, |
| workerName: parsed.workerName, |
| workerColor: parsed.workerColor, |
| host: parsed.hostPattern.host, |
| createdAt: parsed.createdAt, |
| }) |
| } |
|
|
| if (newSandboxRequests.length > 0) { |
| setAppState(prev => ({ |
| ...prev, |
| workerSandboxPermissions: { |
| ...prev.workerSandboxPermissions, |
| queue: [ |
| ...prev.workerSandboxPermissions.queue, |
| ...newSandboxRequests, |
| ], |
| }, |
| })) |
|
|
| |
| const firstRequest = newSandboxRequests[0] |
| if (firstRequest && !isLoading && !focusedInputDialog) { |
| void sendNotification( |
| { |
| message: `${firstRequest.workerName} needs network access to ${firstRequest.host}`, |
| notificationType: 'worker_permission_prompt', |
| }, |
| terminal, |
| ) |
| } |
| } |
| } |
|
|
| |
| if (sandboxPermissionResponses.length > 0 && isTeammate()) { |
| logForDebugging( |
| `[InboxPoller] Found ${sandboxPermissionResponses.length} sandbox permission response(s)`, |
| ) |
|
|
| for (const m of sandboxPermissionResponses) { |
| const parsed = isSandboxPermissionResponse(m.text) |
| if (!parsed) continue |
|
|
| |
| if (hasSandboxPermissionCallback(parsed.requestId)) { |
| logForDebugging( |
| `[InboxPoller] Processing sandbox permission response for ${parsed.requestId}: allow=${parsed.allow}`, |
| ) |
|
|
| |
| processSandboxPermissionResponse({ |
| requestId: parsed.requestId, |
| host: parsed.host, |
| allow: parsed.allow, |
| }) |
|
|
| |
| setAppState(prev => ({ |
| ...prev, |
| pendingSandboxRequest: null, |
| })) |
| } |
| } |
| } |
|
|
| |
| if (teamPermissionUpdates.length > 0 && isTeammate()) { |
| logForDebugging( |
| `[InboxPoller] Found ${teamPermissionUpdates.length} team permission update(s)`, |
| ) |
|
|
| for (const m of teamPermissionUpdates) { |
| const parsed = isTeamPermissionUpdate(m.text) |
| if (!parsed) { |
| logForDebugging( |
| `[InboxPoller] Failed to parse team permission update: ${m.text.substring(0, 100)}`, |
| ) |
| continue |
| } |
|
|
| |
| if ( |
| !parsed.permissionUpdate?.rules || |
| !parsed.permissionUpdate?.behavior |
| ) { |
| logForDebugging( |
| `[InboxPoller] Invalid team permission update: missing permissionUpdate.rules or permissionUpdate.behavior`, |
| ) |
| continue |
| } |
|
|
| |
| logForDebugging( |
| `[InboxPoller] Applying team permission update: ${parsed.toolName} allowed in ${parsed.directoryPath}`, |
| ) |
| logForDebugging( |
| `[InboxPoller] Permission update rules: ${jsonStringify(parsed.permissionUpdate.rules)}`, |
| ) |
|
|
| setAppState(prev => { |
| const updated = applyPermissionUpdate(prev.toolPermissionContext, { |
| type: 'addRules', |
| rules: parsed.permissionUpdate.rules, |
| behavior: parsed.permissionUpdate.behavior, |
| destination: 'session', |
| }) |
| logForDebugging( |
| `[InboxPoller] Updated session allow rules: ${jsonStringify(updated.alwaysAllowRules.session)}`, |
| ) |
| return { |
| ...prev, |
| toolPermissionContext: updated, |
| } |
| }) |
| } |
| } |
|
|
| |
| if (modeSetRequests.length > 0 && isTeammate()) { |
| logForDebugging( |
| `[InboxPoller] Found ${modeSetRequests.length} mode set request(s)`, |
| ) |
|
|
| for (const m of modeSetRequests) { |
| |
| if (m.from !== 'team-lead') { |
| logForDebugging( |
| `[InboxPoller] Ignoring mode set request from non-team-lead: ${m.from}`, |
| ) |
| continue |
| } |
|
|
| const parsed = isModeSetRequest(m.text) |
| if (!parsed) { |
| logForDebugging( |
| `[InboxPoller] Failed to parse mode set request: ${m.text.substring(0, 100)}`, |
| ) |
| continue |
| } |
|
|
| const targetMode = permissionModeFromString(parsed.mode) |
| logForDebugging( |
| `[InboxPoller] Applying mode change from team-lead: ${targetMode}`, |
| ) |
|
|
| |
| setAppState(prev => ({ |
| ...prev, |
| toolPermissionContext: applyPermissionUpdate( |
| prev.toolPermissionContext, |
| { |
| type: 'setMode', |
| mode: toExternalPermissionMode(targetMode), |
| destination: 'session', |
| }, |
| ), |
| })) |
|
|
| |
| const teamName = currentAppState.teamContext?.teamName |
| const agentName = getAgentName() |
| if (teamName && agentName) { |
| setMemberMode(teamName, agentName, targetMode) |
| } |
| } |
| } |
|
|
| |
| if ( |
| planApprovalRequests.length > 0 && |
| isTeamLead(currentAppState.teamContext) |
| ) { |
| logForDebugging( |
| `[InboxPoller] Found ${planApprovalRequests.length} plan approval request(s), auto-approving`, |
| ) |
|
|
| const teamName = currentAppState.teamContext?.teamName |
| const leaderExternalMode = toExternalPermissionMode( |
| currentAppState.toolPermissionContext.mode, |
| ) |
| const modeToInherit = |
| leaderExternalMode === 'plan' ? 'default' : leaderExternalMode |
|
|
| for (const m of planApprovalRequests) { |
| const parsed = isPlanApprovalRequest(m.text) |
| if (!parsed) continue |
|
|
| |
| const approvalResponse = { |
| type: 'plan_approval_response', |
| requestId: parsed.requestId, |
| approved: true, |
| timestamp: new Date().toISOString(), |
| permissionMode: modeToInherit, |
| } |
|
|
| void writeToMailbox( |
| m.from, |
| { |
| from: TEAM_LEAD_NAME, |
| text: jsonStringify(approvalResponse), |
| timestamp: new Date().toISOString(), |
| }, |
| teamName, |
| ) |
|
|
| |
| const taskId = findInProcessTeammateTaskId(m.from, currentAppState) |
| if (taskId) { |
| handlePlanApprovalResponse( |
| taskId, |
| { |
| type: 'plan_approval_response', |
| requestId: parsed.requestId, |
| approved: true, |
| timestamp: new Date().toISOString(), |
| permissionMode: modeToInherit, |
| }, |
| setAppState, |
| ) |
| } |
|
|
| logForDebugging( |
| `[InboxPoller] Auto-approved plan from ${m.from} (request ${parsed.requestId})`, |
| ) |
|
|
| |
| |
| regularMessages.push(m) |
| } |
| } |
|
|
| |
| if (shutdownRequests.length > 0 && isTeammate()) { |
| logForDebugging( |
| `[InboxPoller] Found ${shutdownRequests.length} shutdown request(s)`, |
| ) |
|
|
| |
| |
| for (const m of shutdownRequests) { |
| regularMessages.push(m) |
| } |
| } |
|
|
| |
| if ( |
| shutdownApprovals.length > 0 && |
| isTeamLead(currentAppState.teamContext) |
| ) { |
| logForDebugging( |
| `[InboxPoller] Found ${shutdownApprovals.length} shutdown approval(s)`, |
| ) |
|
|
| for (const m of shutdownApprovals) { |
| const parsed = isShutdownApproved(m.text) |
| if (!parsed) continue |
|
|
| |
| if (parsed.paneId && parsed.backendType) { |
| void (async () => { |
| try { |
| |
| await ensureBackendsRegistered() |
| const insideTmux = await isInsideTmux() |
| const backend = getBackendByType( |
| parsed.backendType as PaneBackendType, |
| ) |
| const success = await backend?.killPane( |
| parsed.paneId!, |
| !insideTmux, |
| ) |
| logForDebugging( |
| `[InboxPoller] Killed pane ${parsed.paneId} for ${parsed.from}: ${success}`, |
| ) |
| } catch (error) { |
| logForDebugging( |
| `[InboxPoller] Failed to kill pane for ${parsed.from}: ${error}`, |
| ) |
| } |
| })() |
| } |
|
|
| |
| const teammateToRemove = parsed.from |
| if (teammateToRemove && currentAppState.teamContext?.teammates) { |
| |
| const teammateId = Object.entries( |
| currentAppState.teamContext.teammates, |
| ).find(([, t]) => t.name === teammateToRemove)?.[0] |
|
|
| if (teammateId) { |
| |
| const teamName = currentAppState.teamContext?.teamName |
| if (teamName) { |
| removeTeammateFromTeamFile(teamName, { |
| agentId: teammateId, |
| name: teammateToRemove, |
| }) |
| } |
|
|
| |
| const { notificationMessage } = teamName |
| ? await unassignTeammateTasks( |
| teamName, |
| teammateId, |
| teammateToRemove, |
| 'shutdown', |
| ) |
| : { notificationMessage: `${teammateToRemove} has shut down.` } |
|
|
| setAppState(prev => { |
| if (!prev.teamContext?.teammates) return prev |
| if (!(teammateId in prev.teamContext.teammates)) return prev |
| const { [teammateId]: _, ...remainingTeammates } = |
| prev.teamContext.teammates |
|
|
| |
| |
| |
| |
| const updatedTasks = { ...prev.tasks } |
| for (const [tid, task] of Object.entries(updatedTasks)) { |
| if ( |
| isInProcessTeammateTask(task) && |
| task.identity.agentId === teammateId |
| ) { |
| updatedTasks[tid] = { |
| ...task, |
| status: 'completed' as const, |
| endTime: Date.now(), |
| } |
| } |
| } |
|
|
| return { |
| ...prev, |
| tasks: updatedTasks, |
| teamContext: { |
| ...prev.teamContext, |
| teammates: remainingTeammates, |
| }, |
| inbox: { |
| messages: [ |
| ...prev.inbox.messages, |
| { |
| id: randomUUID(), |
| from: 'system', |
| text: jsonStringify({ |
| type: 'teammate_terminated', |
| message: notificationMessage, |
| }), |
| timestamp: new Date().toISOString(), |
| status: 'pending' as const, |
| }, |
| ], |
| }, |
| } |
| }) |
| logForDebugging( |
| `[InboxPoller] Removed ${teammateToRemove} (${teammateId}) from teamContext`, |
| ) |
| } |
| } |
|
|
| |
| regularMessages.push(m) |
| } |
| } |
|
|
| |
| if (regularMessages.length === 0) { |
| |
| |
| markRead() |
| return |
| } |
|
|
| |
| |
| const formatted = regularMessages |
| .map(m => { |
| const colorAttr = m.color ? ` color="${m.color}"` : '' |
| const summaryAttr = m.summary ? ` summary="${m.summary}"` : '' |
| const messageContent = m.text |
|
|
| return `<${TEAMMATE_MESSAGE_TAG} teammate_id="${m.from}"${colorAttr}${summaryAttr}>\n${messageContent}\n</${TEAMMATE_MESSAGE_TAG}>` |
| }) |
| .join('\n\n') |
|
|
| |
| const queueMessages = () => { |
| setAppState(prev => ({ |
| ...prev, |
| inbox: { |
| messages: [ |
| ...prev.inbox.messages, |
| ...regularMessages.map(m => ({ |
| id: randomUUID(), |
| from: m.from, |
| text: m.text, |
| timestamp: m.timestamp, |
| status: 'pending' as const, |
| color: m.color, |
| summary: m.summary, |
| })), |
| ], |
| }, |
| })) |
| } |
|
|
| if (!isLoading && !focusedInputDialog) { |
| |
| logForDebugging(`[InboxPoller] Session idle, submitting immediately`) |
| const submitted = onSubmitTeammateMessage(formatted) |
| if (!submitted) { |
| |
| logForDebugging( |
| `[InboxPoller] Submission rejected, queuing for later delivery`, |
| ) |
| queueMessages() |
| } |
| } else { |
| |
| logForDebugging(`[InboxPoller] Session busy, queuing for later delivery`) |
| queueMessages() |
| } |
|
|
| |
| |
| |
| |
| markRead() |
| }, [ |
| enabled, |
| isLoading, |
| focusedInputDialog, |
| onSubmitTeammateMessage, |
| setAppState, |
| terminal, |
| store, |
| ]) |
|
|
| |
| useEffect(() => { |
| if (!enabled) return |
|
|
| |
| if (isLoading || focusedInputDialog) { |
| return |
| } |
|
|
| |
| const currentAppState = store.getState() |
| const agentName = getAgentNameToPoll(currentAppState) |
| if (!agentName) return |
|
|
| const pendingMessages = currentAppState.inbox.messages.filter( |
| m => m.status === 'pending', |
| ) |
| const processedMessages = currentAppState.inbox.messages.filter( |
| m => m.status === 'processed', |
| ) |
|
|
| |
| if (processedMessages.length > 0) { |
| logForDebugging( |
| `[InboxPoller] Cleaning up ${processedMessages.length} processed message(s) that were delivered mid-turn`, |
| ) |
| const processedIds = new Set(processedMessages.map(m => m.id)) |
| setAppState(prev => ({ |
| ...prev, |
| inbox: { |
| messages: prev.inbox.messages.filter(m => !processedIds.has(m.id)), |
| }, |
| })) |
| } |
|
|
| |
| if (pendingMessages.length === 0) return |
|
|
| logForDebugging( |
| `[InboxPoller] Session idle, delivering ${pendingMessages.length} pending message(s)`, |
| ) |
|
|
| |
| const formatted = pendingMessages |
| .map(m => { |
| const colorAttr = m.color ? ` color="${m.color}"` : '' |
| const summaryAttr = m.summary ? ` summary="${m.summary}"` : '' |
| return `<${TEAMMATE_MESSAGE_TAG} teammate_id="${m.from}"${colorAttr}${summaryAttr}>\n${m.text}\n</${TEAMMATE_MESSAGE_TAG}>` |
| }) |
| .join('\n\n') |
|
|
| |
| const submitted = onSubmitTeammateMessage(formatted) |
| if (submitted) { |
| |
| const submittedIds = new Set(pendingMessages.map(m => m.id)) |
| setAppState(prev => ({ |
| ...prev, |
| inbox: { |
| messages: prev.inbox.messages.filter(m => !submittedIds.has(m.id)), |
| }, |
| })) |
| } else { |
| logForDebugging( |
| `[InboxPoller] Submission rejected, keeping messages queued`, |
| ) |
| } |
| }, [ |
| enabled, |
| isLoading, |
| focusedInputDialog, |
| onSubmitTeammateMessage, |
| setAppState, |
| inboxMessageCount, |
| store, |
| ]) |
|
|
| |
| const shouldPoll = enabled && !!getAgentNameToPoll(store.getState()) |
| useInterval(() => void poll(), shouldPoll ? INBOX_POLL_INTERVAL_MS : null) |
|
|
| |
| const hasDoneInitialPollRef = useRef(false) |
| useEffect(() => { |
| if (!enabled) return |
| if (hasDoneInitialPollRef.current) return |
| |
| if (getAgentNameToPoll(store.getState())) { |
| hasDoneInitialPollRef.current = true |
| void poll() |
| } |
| |
| |
| }, [enabled, poll, store]) |
| } |
|
|