"use client"; import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { MessageSquare, ChevronDown, Mic } from "lucide-react"; import { RetroOffice3D } from "@/features/retro-office/RetroOffice3D"; import type { OfficeAgent } from "@/features/retro-office/core/types"; import { GatewayConnectScreen } from "@/features/agents/components/GatewayConnectScreen"; import { useAgentStore, type AgentState } from "@/features/agents/state/store"; import { useGatewayConnection, type EventFrame, isSameSessionKey, parseAgentIdFromSessionKey, } from "@/lib/gateway/GatewayClient"; import { createStudioSettingsCoordinator, type StudioSettingsLoadOptions, } from "@/lib/studio/coordinator"; import { resolveDeskAssignments } from "@/lib/studio/settings"; import { renameGatewayAgent } from "@/lib/gateway/agentConfig"; import { runStudioBootstrapLoadOperation, executeStudioBootstrapLoadCommands, } from "@/features/agents/operations/studioBootstrapOperation"; import { isGatewayDisconnectLikeError } from "@/lib/gateway/GatewayClient"; import { createGatewayRuntimeEventHandler } from "@/features/agents/state/gatewayRuntimeEventHandler"; import { buildHistoryLines, classifyGatewayEventKind, isReasoningRuntimeAgentStream, type AgentEventPayload, type ChatEventPayload, type SummaryPreviewSnapshot, } from "@/features/agents/state/runtimeEventBridge"; import { extractText, extractThinking, extractToolLines, isHeartbeatPrompt, stripUiMetadata, } from "@/lib/text/message-extract"; import { resolveOfficeIntentSnapshot } from "@/lib/office/deskDirectives"; import { AgentChatPanel } from "@/features/agents/components/AgentChatPanel"; import { AgentEditorModal, type AgentEditorSection, } from "@/features/agents/components/AgentEditorModal"; import { useChatInteractionController } from "@/features/agents/operations/useChatInteractionController"; import { executeHistorySyncCommands, runHistorySyncOperation, } from "@/features/agents/operations/historySyncOperation"; import { RUNTIME_SYNC_DEFAULT_HISTORY_LIMIT, RUNTIME_SYNC_MAX_HISTORY_LIMIT, } from "@/features/agents/operations/runtimeSyncControlWorkflow"; import { TRANSCRIPT_V2_ENABLED, logTranscriptDebugMetric, } from "@/features/agents/state/transcript"; import { buildGatewayModelChoices, type GatewayModelChoice, } from "@/lib/gateway/models"; import type { GatewayModelPolicySnapshot } from "@/lib/gateway/models"; import type { AgentAvatarProfile } from "@/lib/avatars/profile"; import { randomUUID } from "@/lib/uuid"; import { HQSidebar, type HQSidebarTab, } from "@/features/office/components/HQSidebar"; import { AnalyticsPanel } from "@/features/office/components/panels/AnalyticsPanel"; import { HistoryPanel } from "@/features/office/components/panels/HistoryPanel"; import { InboxPanel } from "@/features/office/components/panels/InboxPanel"; import { PlaybooksPanel } from "@/features/office/components/panels/PlaybooksPanel"; import { SkillsMarketplacePanel } from "@/features/office/components/panels/SkillsMarketplacePanel"; import { useOfficeSkillsMarketplace } from "@/features/office/hooks/useOfficeSkillsMarketplace"; import { useOfficeStandupController } from "@/features/office/hooks/useOfficeStandupController"; import { useRunLog } from "@/features/office/hooks/useRunLog"; import { useFinalizedAssistantReplyListener } from "@/hooks/useFinalizedAssistantReplyListener"; import { useStudioOfficePreference } from "@/hooks/useStudioOfficePreference"; import { useStudioVoiceRepliesPreference } from "@/hooks/useStudioVoiceRepliesPreference"; import { useVoiceRecorder, type VoiceSendPayload, } from "@/hooks/useVoiceRecorder"; import { useVoiceReplyPlayback } from "@/hooks/useVoiceReplyPlayback"; import { buildOfficeAnimationState, clearOfficeAnimationTriggerHold, createOfficeAnimationTriggerState, reconcileOfficeAnimationTriggerState, reduceOfficeAnimationTriggerEvent, type OfficePhoneCallRequest, type OfficeTextMessageRequest, } from "@/lib/office/eventTriggers"; import type { MockPhoneCallScenario } from "@/lib/office/call/types"; import type { MockTextMessageScenario } from "@/lib/office/text/types"; import { buildOfficeDeskMonitor, type OfficeDeskMonitor, } from "@/lib/office/deskMonitor"; import type { StandupAgentSnapshot } from "@/lib/office/standup/types"; import type { SkillStatusEntry } from "@/lib/skills/types"; const stringToColor = (str: string) => { let hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } const c = (hash & 0x00ffffff).toString(16).toUpperCase(); return "#" + "00000".substring(0, 6 - c.length) + c; }; const ITEMS = [ "globe", "books", "coffee", "palette", "camera", "waveform", "shield", "fire", "plant", "laptop", ]; const GYM_WORKOUT_LATCH_MS = 60_000; const MAIN_AGENT_ID = "main"; const MAX_OPENCLAW_LOG_ENTRIES = 200; const MAX_OPENCLAW_AGENT_OUTPUT_LINES = 12; type OpenClawLogEntry = { id: string; timestamp: string; eventName: string; eventKind: string; summary: string; role: string | null; messageText: string | null; thinkingText: string | null; streamText: string | null; toolText: string | null; payloadText: string; }; type PreparedPhoneCallEntry = { requestKey: string; scenario: MockPhoneCallScenario; }; type PreparedTextMessageEntry = { requestKey: string; scenario: MockTextMessageScenario; }; type PhoneCallSpeakPayload = { agentId: string; requestKey: string; scenario: MockPhoneCallScenario; }; const createOpenClawLogEntry = (params: { eventName: string; eventKind: string; summary: string; payload?: unknown; role?: string | null; messageText?: string | null; thinkingText?: string | null; streamText?: string | null; toolText?: string | null; }): OpenClawLogEntry => ({ id: randomUUID(), timestamp: formatOpenClawTimestamp(Date.now()), eventName: params.eventName, eventKind: params.eventKind, summary: params.summary, role: params.role ?? null, messageText: params.messageText ?? null, thinkingText: params.thinkingText ?? null, streamText: params.streamText ?? null, toolText: params.toolText ?? null, payloadText: safeJsonStringify(params.payload ?? null), }); const formatOpenClawTimestamp = (timestampMs: number) => { const date = new Date(timestampMs); const hh = String(date.getHours()).padStart(2, "0"); const mm = String(date.getMinutes()).padStart(2, "0"); const ss = String(date.getSeconds()).padStart(2, "0"); const ms = String(date.getMilliseconds()).padStart(3, "0"); return `${hh}:${mm}:${ss}.${ms}`; }; const formatOpenClawValue = (value: string | null | undefined) => { const trimmed = value?.trim() ?? ""; return trimmed || "-"; }; const buildPhoneCallOutputLine = (text: string) => `[phone booth] ${text}`; const buildTextMessageOutputLine = (text: string) => `[messaging booth] ${text}`; const PHONE_BOOTH_ASSISTANT_FALLBACK_RE = /\b(?:i\s+)?can(?:not|['’]t)\s+(?:place|make)\s+(?:phone\s+)?calls?\b/i; const shouldSuppressPhoneBoothAssistantReply = (params: { agents: AgentState[]; event: EventFrame; phoneCallByAgentId: Record; }): boolean => { if (classifyGatewayEventKind(params.event.event) !== "runtime-chat") return false; const payload = params.event.payload as ChatEventPayload | undefined; if (!payload?.sessionKey) return false; const message = typeof payload.message === "object" && payload.message !== null ? (payload.message as Record) : null; const role = typeof message?.role === "string" ? message.role : null; if (role !== "assistant") return false; const text = extractText(payload.message)?.trim() ?? ""; if (!text || !PHONE_BOOTH_ASSISTANT_FALLBACK_RE.test(text)) return false; const agentId = params.agents.find((agent) => agent.sessionKey === payload.sessionKey)?.agentId ?? parseAgentIdFromSessionKey(payload.sessionKey); if (!agentId) return false; return Boolean(params.phoneCallByAgentId[agentId]); }; const safeJsonStringify = (value: unknown) => { try { return JSON.stringify(value, null, 2) ?? String(value); } catch (error) { return `[unserializable payload: ${error instanceof Error ? error.message : "unknown error"}]`; } }; const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const renderOpenClawHighlightedText = ( value: string, query: string, ): ReactNode => { if (!query) return value; const trimmedQuery = query.trim(); if (!trimmedQuery) return value; const pattern = new RegExp(`(${escapeRegExp(trimmedQuery)})`, "gi"); return value.split(pattern).map((part, index) => part.toLowerCase() === trimmedQuery.toLowerCase() ? ( {part} ) : ( part ), ); }; const resolveMessageRole = (message: unknown) => message && typeof message === "object" ? ((message as Record).role ?? null) : null; const formatOpenClawEventLogEntry = (event: EventFrame): OpenClawLogEntry => { const eventKind = classifyGatewayEventKind(event.event); const baseSummary = `seq=${event.seq ?? "-"} stateVersion=${safeJsonStringify(event.stateVersion ?? null)}`; let summary = baseSummary; let role: string | null = null; let messageText: string | null = null; let thinkingText: string | null = null; let streamText: string | null = null; let toolText: string | null = null; if (eventKind === "runtime-chat") { const payload = event.payload as ChatEventPayload | undefined; if (payload) { role = typeof resolveMessageRole(payload.message) === "string" ? String(resolveMessageRole(payload.message)) : null; const text = extractText(payload.message); const thinking = extractThinking(payload.message ?? payload); const toolLines = extractToolLines(payload.message ?? payload); summary = `chat session=${payload.sessionKey || "-"} run=${payload.runId || "-"} state=${payload.state} role=${String(role ?? "-")} stopReason=${payload.stopReason ?? "-"} | ${baseSummary}`; if (text) { messageText = stripUiMetadata(text).trim() || text.trim(); } if (thinking) { thinkingText = thinking.trim(); } if (toolLines.length > 0) { toolText = toolLines.join(" | "); } } } else if (eventKind === "runtime-agent") { const payload = event.payload as AgentEventPayload | undefined; if (payload) { const data = payload.data && typeof payload.data === "object" ? (payload.data as Record) : null; const phase = typeof data?.phase === "string" ? data.phase : "-"; const text = typeof data?.text === "string" ? data.text : typeof data?.delta === "string" ? data.delta : ""; const extractedThinking = extractThinking(data ?? payload); summary = `agent session=${payload.sessionKey || "-"} run=${payload.runId || "-"} stream=${payload.stream || "-"} phase=${phase} reasoning=${String(isReasoningRuntimeAgentStream(payload.stream ?? ""))} | ${baseSummary}`; if (extractedThinking) { thinkingText = extractedThinking.trim(); } else if (text.trim()) { streamText = text.trim(); } } } return createOpenClawLogEntry({ eventName: event.event, eventKind, summary, role, messageText, thinkingText, streamText, toolText, payload: event.payload ?? null, }); }; const resolveLatestUserTextFromPreview = ( previewResult: SummaryPreviewSnapshot | null | undefined, sessionKey: string, ): string | null => { const previews = Array.isArray(previewResult?.previews) ? previewResult.previews : []; const preview = previews.find((entry) => entry.key === sessionKey); if (!preview || !Array.isArray(preview.items)) return null; for (let index = preview.items.length - 1; index >= 0; index -= 1) { const item = preview.items[index]; if (!item) continue; if (item.role === "assistant") continue; if (item.role === "user") { const text = item.text.trim(); if (text) return text; } } return null; }; const getDeterministicItem = (id: string) => { let hash = 0; for (let i = 0; i < id.length; i++) { hash = id.charCodeAt(i) + ((hash << 5) - hash); } return ITEMS[Math.abs(hash) % ITEMS.length]; }; const mapAgentToOffice = (agent: AgentState): OfficeAgent => { if (agent.status === "error") { return { id: agent.agentId, name: agent.name || "Unknown", status: "error", color: stringToColor(agent.agentId), item: getDeterministicItem(agent.agentId), avatarProfile: agent.avatarProfile ?? null, }; } const isWorking = agent.status === "running" || Boolean(agent.runId); return { id: agent.agentId, name: agent.name || "Unknown", status: isWorking ? "working" : "idle", color: stringToColor(agent.agentId), item: getDeterministicItem(agent.agentId), avatarProfile: agent.avatarProfile ?? null, }; }; type ChatHistoryResult = { messages?: Array>; }; type SessionsListEntry = { key?: string; updatedAt?: number | null; origin?: { label?: string | null } | null; }; type SessionsListResult = { sessions?: SessionsListEntry[]; }; type SessionsPreviewItem = { role?: string; text?: string; }; type SessionsPreviewEntry = { key?: string; status?: string; items?: SessionsPreviewItem[]; }; type SessionsPreviewResult = { previews?: SessionsPreviewEntry[]; }; type HistoryInferenceResult = { inferredRunning: boolean; lastRole: string; lastText: string; messageCount: number; }; type OfficeDebugRow = { agentId: string; name: string; storeStatus: AgentState["status"]; runId: string | null; inferredRunning: boolean; lastRole: string; lastText: string; messageCount: number; detectedSessionKey: string; inspectedSessions: string; inferenceSource: string; at: string; }; type OfficeFeedEvent = { id: string; name: string; text: string; ts: number; kind?: "status" | "reply"; }; const normalizeOfficeFeedText = ( value: string | null | undefined, maxChars?: number, ): string => { const normalized = (value ?? "").replace(/\s+/g, " ").trim(); if (!normalized) return ""; if ( typeof maxChars !== "number" || !Number.isFinite(maxChars) || maxChars <= 0 ) { return normalized; } if (normalized.length <= maxChars) return normalized; return `${normalized.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`; }; const resolveHistoryInference = ( messages: Array>, ): HistoryInferenceResult => { for (let index = messages.length - 1; index >= 0; index -= 1) { const entry = messages[index]; const role = typeof entry.role === "string" ? entry.role : ""; if (role === "system" || role === "tool" || role === "toolResult") continue; const text = typeof entry.text === "string" ? entry.text.trim() : typeof entry.content === "string" ? entry.content.trim() : ""; if (role === "assistant") { return { inferredRunning: false, lastRole: "assistant", lastText: text.slice(0, 120), messageCount: messages.length, }; } if (role === "user") { const rawText = typeof entry.text === "string" ? entry.text : text; if (rawText && isHeartbeatPrompt(rawText)) continue; return { inferredRunning: true, lastRole: "user", lastText: rawText.slice(0, 120), messageCount: messages.length, }; } } return { inferredRunning: false, lastRole: "none", lastText: "", messageCount: messages.length, }; }; const inferRunningFromAgentSessions = async (params: { client: { call: (method: string, args: unknown) => Promise; }; agentId: string; }): Promise<{ inferredRunning: boolean; sessionKey: string; lastRole: string; lastText: string; messageCount: number; latestSessionUpdatedAtMs: number; inspectedSessions: string[]; inferenceSource: string; }> => { const sessionsResult = await params.client.call( "sessions.list", { agentId: params.agentId, includeGlobal: false, includeUnknown: true, limit: 8, }, ); const sessions = ( Array.isArray(sessionsResult.sessions) ? sessionsResult.sessions : [] ) .filter( (entry) => typeof entry.key === "string" && entry.key.trim().length > 0, ) .sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0)) .slice(0, 4); const latestSessionUpdatedAtMs = typeof sessions[0]?.updatedAt === "number" && Number.isFinite(sessions[0].updatedAt) ? sessions[0].updatedAt : 0; const inspectedSessions = sessions.map((entry) => { const key = entry.key?.trim() ?? ""; const label = entry.origin?.label?.trim() ?? ""; const updatedAt = typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? new Date(entry.updatedAt).toISOString() : "n/a"; const base = label ? `${key} [${label}]` : key; return `${base} @${updatedAt}`; }); const sessionKeys = sessions .map((entry) => entry.key?.trim() ?? "") .filter((key) => key.length > 0); if (sessionKeys.length > 0) { const previewResult = await params.client.call( "sessions.preview", { keys: sessionKeys, limit: 8, maxChars: 240, }, ); const previews = Array.isArray(previewResult.previews) ? previewResult.previews : []; for (const preview of previews) { const key = typeof preview.key === "string" ? preview.key.trim() : ""; const items = Array.isArray(preview.items) ? preview.items : []; for (let index = items.length - 1; index >= 0; index -= 1) { const item = items[index]; if (!item) continue; const role = typeof item.role === "string" ? item.role : ""; const text = typeof item.text === "string" ? item.text.trim() : ""; if (role === "system" || role === "tool" || role === "toolResult") { continue; } if (role === "assistant") break; if (role === "user") { if (text && isHeartbeatPrompt(text)) continue; return { inferredRunning: true, sessionKey: key, lastRole: "user", lastText: text.slice(0, 120), messageCount: items.length, latestSessionUpdatedAtMs, inspectedSessions, inferenceSource: "sessions.preview.user-tail", }; } } } } for (const session of sessions) { const key = session.key?.trim() ?? ""; if (!key) continue; const history = await params.client.call( "chat.history", { sessionKey: key, limit: 24, }, ); const messages = Array.isArray(history.messages) ? history.messages : []; const inference = resolveHistoryInference(messages); if (inference.inferredRunning) { return { inferredRunning: true, sessionKey: key, lastRole: inference.lastRole, lastText: inference.lastText, messageCount: inference.messageCount, latestSessionUpdatedAtMs, inspectedSessions, inferenceSource: "chat.history.user-tail", }; } } return { inferredRunning: false, sessionKey: "", lastRole: "assistant", lastText: "", messageCount: 0, latestSessionUpdatedAtMs, inspectedSessions, inferenceSource: "none", }; }; type OfficeScreenProps = { showOpenClawConsole?: boolean; }; export function OfficeScreen({ showOpenClawConsole = true, }: OfficeScreenProps) { const searchParams = useSearchParams(); const debugEnabled = searchParams.get("officeDebug") === "1"; const [settingsCoordinator] = useState(() => createStudioSettingsCoordinator(), ); const { client, status, connectPromptReady, shouldPromptForConnect, gatewayUrl, token, localGatewayDefaults, error: gatewayError, connect, disconnect, useLocalGatewayDefaults, setGatewayUrl, setToken, } = useGatewayConnection(settingsCoordinator); const { state, dispatch, hydrateAgents, setError, setLoading } = useAgentStore(); const [agentsLoaded, setAgentsLoaded] = useState(false); const [didAttemptGatewayConnect, setDidAttemptGatewayConnect] = useState(false); const [clockTick, setClockTick] = useState(0); const [debugRows, setDebugRows] = useState([]); const [feedEvents, setFeedEvents] = useState([]); const officeAgentCacheRef = useRef< Map< string, { agent: AgentState; deskHeld: boolean; gymHeld: boolean; latchedWorking: boolean; officeAgent: OfficeAgent; phoneBoothHeld: boolean; qaHeld: boolean; smsBoothHeld: boolean; } > >(new Map()); const deskMonitorCacheRef = useRef< Map >(new Map()); const [openClawLogEntries, setOpenClawLogEntries] = useState< OpenClawLogEntry[] >([]); const [openClawConsoleCollapsed, setOpenClawConsoleCollapsed] = useState(true); const [openClawConsoleSearch, setOpenClawConsoleSearch] = useState(""); const [openClawConsoleCopyStatus, setOpenClawConsoleCopyStatus] = useState< "idle" | "copied" | "error" >("idle"); const [officeTriggerState, setOfficeTriggerState] = useState(() => createOfficeAnimationTriggerState(), ); const prevWorkingRef = useRef>({}); const prevAssistantPreviewRef = useRef< Record >({}); const [runCountByAgentId, setRunCountByAgentId] = useState< Record >({}); const [lastSeenByAgentId, setLastSeenByAgentId] = useState< Record >({}); const [marketplaceGymHoldByAgentId, setMarketplaceGymHoldByAgentId] = useState>({}); const [gymCooldownUntilByAgentId, setGymCooldownUntilByAgentId] = useState< Record >({}); const prevImmediateGymHoldRef = useRef>({}); const [monitorAgentId, setMonitorAgentId] = useState(null); const [githubReviewAgentId, setGithubReviewAgentId] = useState( null, ); const [qaTestingAgentId, setQaTestingAgentId] = useState(null); const gatewayConfigSnapshot = useRef(null); const loadAgentsInFlightRef = useRef | null>(null); const connectionEpochRef = useRef(0); const lastLoadAgentsStartedAtRef = useRef(0); const lastGatewayActivityAtRef = useRef(0); const stateRef = useRef(state); const officeTriggerStateRef = useRef(officeTriggerState); const historyInFlightRef = useRef>(new Set()); const lastTransportHistoryRefreshKeyRef = useRef>({}); const [chatOpen, setChatOpen] = useState(false); const [selectedChatAgentId, setSelectedChatAgentId] = useState( null, ); const [agentEditorAgentId, setAgentEditorAgentId] = useState(null); const [agentEditorInitialSection, setAgentEditorInitialSection] = useState("avatar"); const [preparedPhoneCallsByAgentId, setPreparedPhoneCallsByAgentId] = useState< Record >({}); const [preparedTextMessagesByAgentId, setPreparedTextMessagesByAgentId] = useState< Record >({}); const promptedPhoneCallKeysRef = useRef>(new Set()); const preparedPhoneCallKeysRef = useRef>(new Set()); const spokenPhoneCallKeysRef = useRef>(new Set()); const promptedTextMessageKeysRef = useRef>(new Set()); const preparedTextMessageKeysRef = useRef>(new Set()); const [deskAssignmentByDeskUid, setDeskAssignmentByDeskUid] = useState< Record >({}); const [gatewayModels, setGatewayModels] = useState([]); const [sidebarOpen, setSidebarOpen] = useState(false); const [activeSidebarTab, setActiveSidebarTab] = useState("inbox"); const router = useRouter(); const { loaded: officeTitleLoaded, title: officeTitle, setTitle: setOfficeTitle, } = useStudioOfficePreference({ gatewayUrl, settingsCoordinator, }); const { loaded: voiceRepliesLoaded, preference: voiceRepliesPreference, enabled: voiceRepliesEnabled, voiceId: voiceRepliesVoiceId, speed: voiceRepliesSpeed, setEnabled: setVoiceRepliesEnabled, setVoiceId: setVoiceRepliesVoiceId, setSpeed: setVoiceRepliesSpeed, } = useStudioVoiceRepliesPreference({ gatewayUrl, settingsCoordinator, }); const { enqueue: enqueueVoiceReply, preview: previewVoiceReply, stop: stopVoiceReplyPlayback, } = useVoiceReplyPlayback({ enabled: voiceRepliesEnabled, provider: voiceRepliesPreference.provider, voiceId: voiceRepliesPreference.voiceId, speed: voiceRepliesPreference.speed, }); const handleAvatarProfileSave = useCallback( (agentId: string, profile: AgentAvatarProfile) => { dispatch({ type: "updateAgent", agentId, patch: { avatarProfile: profile, avatarSeed: profile.seed }, }); const key = gatewayUrl.trim(); if (!key) return; settingsCoordinator.schedulePatch( { avatars: { [key]: { [agentId]: profile } } }, 0, ); }, [dispatch, gatewayUrl, settingsCoordinator], ); const openAgentEditor = useCallback( (agentId: string, initialSection: AgentEditorSection = "avatar") => { setAgentEditorAgentId(agentId); setAgentEditorInitialSection(initialSection); setSelectedChatAgentId(agentId); dispatch({ type: "selectAgent", agentId }); }, [dispatch], ); const handleDeskAssignmentChange = useCallback( (deskUid: string, agentId: string | null) => { const key = gatewayUrl.trim(); const normalizedDeskUid = deskUid.trim(); if (!key || !normalizedDeskUid) return; setDeskAssignmentByDeskUid((previous) => { const next = { ...previous }; if (agentId) { for (const [existingDeskUid, existingAgentId] of Object.entries( next, )) { if ( existingDeskUid !== normalizedDeskUid && existingAgentId === agentId ) { delete next[existingDeskUid]; } } next[normalizedDeskUid] = agentId; } else { delete next[normalizedDeskUid]; } return next; }); settingsCoordinator.schedulePatch( { deskAssignments: { [key]: { [normalizedDeskUid]: agentId, }, }, }, 0, ); }, [gatewayUrl, settingsCoordinator], ); const handleDeskAssignmentsReset = useCallback( (deskUids: string[]) => { const key = gatewayUrl.trim(); if (!key || deskUids.length === 0) return; setDeskAssignmentByDeskUid((previous) => { const next = { ...previous }; for (const deskUid of deskUids) delete next[deskUid]; return next; }); settingsCoordinator.schedulePatch( { deskAssignments: { [key]: Object.fromEntries( deskUids.map((deskUid) => [deskUid, null]), ), }, }, 0, ); }, [gatewayUrl, settingsCoordinator], ); useEffect(() => { stateRef.current = state; }, [state]); useEffect(() => { officeTriggerStateRef.current = officeTriggerState; }, [officeTriggerState]); useEffect(() => { const timerId = window.setInterval(() => { setClockTick((value) => value + 1); }, 2000); return () => { window.clearInterval(timerId); }; }, []); useEffect(() => { if (status === "connecting") { setDidAttemptGatewayConnect(true); } }, [status]); useEffect(() => { if (gatewayError) { setDidAttemptGatewayConnect(true); } }, [gatewayError]); const loadStudioSettings = useCallback( (options?: StudioSettingsLoadOptions) => settingsCoordinator.loadSettings(options), [settingsCoordinator], ); useEffect(() => { let cancelled = false; const key = gatewayUrl.trim(); if (!key) { setDeskAssignmentByDeskUid({}); return; } void (async () => { try { const settings = await loadStudioSettings(); if (cancelled) return; setDeskAssignmentByDeskUid( settings ? resolveDeskAssignments(settings, key) : {}, ); } catch { if (cancelled) return; setDeskAssignmentByDeskUid({}); } })(); return () => { cancelled = true; }; }, [gatewayUrl, loadStudioSettings]); const loadAgents = useCallback(async (options?: { forceSettings?: boolean; minIntervalMs?: number; onlyWhenIdleForMs?: number; settingsMaxAgeMs?: number; silent?: boolean; }) => { if (status !== "connected") return; if (loadAgentsInFlightRef.current) return loadAgentsInFlightRef.current; const now = Date.now(); const minIntervalMs = options?.minIntervalMs ?? 0; if ( minIntervalMs > 0 && now - lastLoadAgentsStartedAtRef.current < minIntervalMs ) { return; } const onlyWhenIdleForMs = options?.onlyWhenIdleForMs ?? 0; if ( onlyWhenIdleForMs > 0 && now - lastGatewayActivityAtRef.current < onlyWhenIdleForMs ) { return; } lastLoadAgentsStartedAtRef.current = now; const connectionEpochAtStart = connectionEpochRef.current; const task = (async () => { if (!options?.silent) { setLoading(true); } try { const settingsLoadOptions: StudioSettingsLoadOptions | undefined = options?.forceSettings ? { force: true } : { maxAgeMs: options?.settingsMaxAgeMs ?? 60_000 }; const commands = await runStudioBootstrapLoadOperation({ client, gatewayUrl, cachedConfigSnapshot: gatewayConfigSnapshot.current, loadStudioSettings: () => loadStudioSettings(settingsLoadOptions), isDisconnectLikeError: isGatewayDisconnectLikeError, preferredSelectedAgentId: null, hasCurrentSelection: false, logError: console.error, }); if (connectionEpochAtStart !== connectionEpochRef.current) { return; } executeStudioBootstrapLoadCommands({ commands, setGatewayConfigSnapshot: (val: GatewayModelPolicySnapshot) => { gatewayConfigSnapshot.current = val; }, hydrateAgents, dispatchUpdateAgent: (agentId, patch) => { dispatch({ type: "updateAgent", agentId, patch }); }, setError, }); if (connectionEpochAtStart !== connectionEpochRef.current) { return; } const refreshedAgents = stateRef.current.agents; const debugCollector: OfficeDebugRow[] = []; const inferredByAgentId = new Map(); await Promise.all( refreshedAgents.map(async (agent) => { if (connectionEpochAtStart !== connectionEpochRef.current) { return; } try { const inference = await inferRunningFromAgentSessions({ client, agentId: agent.agentId, }); if (connectionEpochAtStart !== connectionEpochRef.current) { return; } const inferredRunning = inference.inferredRunning; inferredByAgentId.set(agent.agentId, inferredRunning); if (inference.latestSessionUpdatedAtMs > 0) { setLastSeenByAgentId((prev) => ({ ...prev, [agent.agentId]: inference.latestSessionUpdatedAtMs, })); } const nextStatus: AgentState["status"] = inferredRunning ? "running" : "idle"; if (agent.status !== nextStatus) { dispatch({ type: "updateAgent", agentId: agent.agentId, patch: { status: nextStatus, runId: inferredRunning ? (agent.runId ?? `inferred-${agent.agentId}`) : null, }, }); } if (debugEnabled) { debugCollector.push({ agentId: agent.agentId, name: agent.name, storeStatus: agent.status, runId: agent.runId, inferredRunning, lastRole: inference.lastRole, lastText: inference.lastText, messageCount: inference.messageCount, detectedSessionKey: inference.sessionKey, inspectedSessions: inference.inspectedSessions.join(" | "), inferenceSource: inference.inferenceSource, at: new Date().toISOString(), }); } } catch (error) { if (!isGatewayDisconnectLikeError(error)) { console.warn( "Failed to infer agent run state from history.", error, ); } } }), ); if (connectionEpochAtStart !== connectionEpochRef.current) { return; } if (debugEnabled) { setDebugRows(debugCollector); console.info("[office-debug] Reconciled agent state.", debugCollector); } lastGatewayActivityAtRef.current = Date.now(); setAgentsLoaded(true); } finally { if (!options?.silent) { setLoading(false); } loadAgentsInFlightRef.current = null; } })(); loadAgentsInFlightRef.current = task; return task; }, [ client, debugEnabled, dispatch, gatewayUrl, hydrateAgents, loadStudioSettings, setError, setLoading, status, ]); const requestAgentHistoryRefresh = useCallback( async (params: { agentId: string; reason: "chat-final-no-trace" | "run-start-no-chat"; sessionKey?: string; }) => { if (status !== "connected") return; const requestedSessionKey = params.sessionKey?.trim() ?? ""; if (requestedSessionKey) { try { const history = await client.call<{ messages?: Record[]; }>("chat.history", { sessionKey: requestedSessionKey, limit: RUNTIME_SYNC_DEFAULT_HISTORY_LIMIT, }); const messages = Array.isArray(history.messages) ? history.messages : []; const derived = buildHistoryLines(messages); let lastUser = derived.lastUser?.trim() ?? ""; if (!lastUser) { const previewResult = await client.call( "sessions.preview", { keys: [requestedSessionKey], limit: 12, maxChars: 400, }, ); lastUser = resolveLatestUserTextFromPreview( previewResult, requestedSessionKey, ) ?? ""; } const targetAgentId = parseAgentIdFromSessionKey(requestedSessionKey) ?? params.agentId; const patch: Partial = {}; if (lastUser) { patch.lastUserMessage = lastUser; } if (derived.lastAssistant) { patch.latestPreview = derived.lastAssistant; } if (typeof derived.lastAssistantAt === "number") { patch.lastAssistantMessageAt = derived.lastAssistantAt; } if (typeof derived.lastUserAt === "number") { patch.lastActivityAt = derived.lastUserAt; } if (Object.keys(patch).length > 0) { dispatch({ type: "updateAgent", agentId: targetAgentId, patch, }); } // Do not replay movement directives from history refresh. // History can include old transport commands; replaying them causes auto-walks on load. setOpenClawLogEntries((previous) => { const next = [ ...previous, createOpenClawLogEntry({ eventName: "history-refresh", eventKind: "derived", summary: `session=${requestedSessionKey} reason=${params.reason} lastUser=${formatOpenClawValue(lastUser)} lastAssistant=${formatOpenClawValue(derived.lastAssistant)}`, messageText: lastUser || null, streamText: derived.lastAssistant ?? null, payload: { sessionKey: requestedSessionKey, reason: params.reason, historyMessageCount: messages.length, lastUser: lastUser || null, lastAssistant: derived.lastAssistant ?? null, }, }), ]; return next.slice(-MAX_OPENCLAW_LOG_ENTRIES); }); if (debugEnabled) { console.info( "[office-debug] Refreshed transport session history.", { agentId: targetAgentId, requestedSessionKey, reason: params.reason, lastUser: lastUser || null, }, ); } } catch (error) { setOpenClawLogEntries((previous) => { const next = [ ...previous, createOpenClawLogEntry({ eventName: "history-refresh", eventKind: "error", summary: `session=${requestedSessionKey} reason=${params.reason} refresh failed`, payload: { sessionKey: requestedSessionKey, reason: params.reason, error: error instanceof Error ? error.message : String(error), }, }), ]; return next.slice(-MAX_OPENCLAW_LOG_ENTRIES); }); if (!isGatewayDisconnectLikeError(error)) { console.error( "Failed to refresh transport session history.", error, ); } } return; } const commands = await runHistorySyncOperation({ client, agentId: params.agentId, getAgent: (agentId) => stateRef.current.agents.find((entry) => entry.agentId === agentId) ?? null, inFlightSessionKeys: historyInFlightRef.current, requestId: randomUUID(), loadedAt: Date.now(), defaultLimit: RUNTIME_SYNC_DEFAULT_HISTORY_LIMIT, maxLimit: RUNTIME_SYNC_MAX_HISTORY_LIMIT, transcriptV2Enabled: TRANSCRIPT_V2_ENABLED, }); executeHistorySyncCommands({ commands, dispatch, logMetric: (metric, meta) => logTranscriptDebugMetric(metric, meta), isDisconnectLikeError: isGatewayDisconnectLikeError, logError: (message, error) => console.error(message, error), }); if (debugEnabled) { console.info("[office-debug] Requested agent history refresh.", { agentId: params.agentId, reason: params.reason, }); } }, [client, debugEnabled, dispatch, status], ); const refreshRecentTransportSessionHistory = useCallback( (event: EventFrame) => { if (event.event !== "health") return; const payload = event.payload as | { agents?: Array<{ agentId?: unknown; sessions?: { recent?: Array<{ key?: unknown; updatedAt?: unknown }>; }; }>; } | undefined; const gatewayAgents = Array.isArray(payload?.agents) ? payload.agents : []; if (gatewayAgents.length === 0) return; for (const gatewayAgent of gatewayAgents) { const agentId = typeof gatewayAgent?.agentId === "string" ? gatewayAgent.agentId.trim() : ""; if (!agentId) continue; const localAgent = stateRef.current.agents.find( (agent) => agent.agentId === agentId, ); if (!localAgent?.sessionKey) continue; const recentSessions = Array.isArray(gatewayAgent.sessions?.recent) ? gatewayAgent.sessions.recent : []; const latestTransportSession = recentSessions.find((entry) => { const sessionKey = typeof entry?.key === "string" ? entry.key.trim() : ""; if (!sessionKey) return false; if (isSameSessionKey(sessionKey, localAgent.sessionKey)) return false; return parseAgentIdFromSessionKey(sessionKey) === agentId; }); if (!latestTransportSession) continue; const sessionKey = typeof latestTransportSession.key === "string" ? latestTransportSession.key.trim() : ""; if (!sessionKey) continue; const updatedAt = typeof latestTransportSession.updatedAt === "number" && Number.isFinite(latestTransportSession.updatedAt) ? latestTransportSession.updatedAt : 0; const refreshKey = `${sessionKey}:${updatedAt}`; if (lastTransportHistoryRefreshKeyRef.current[agentId] === refreshKey) { continue; } lastTransportHistoryRefreshKeyRef.current[agentId] = refreshKey; void requestAgentHistoryRefresh({ agentId, reason: "run-start-no-chat", sessionKey, }); } }, [requestAgentHistoryRefresh], ); useEffect(() => { if (status !== "connected" || agentsLoaded) return; void loadAgents({ forceSettings: true }); }, [agentsLoaded, loadAgents, status]); useEffect(() => { if (status !== "connected") return; if (state.loading) return; if (state.agents.length > 0) return; const timeoutId = window.setTimeout(() => { void loadAgents({ forceSettings: true }); }, 500); return () => { window.clearTimeout(timeoutId); }; }, [loadAgents, state.agents.length, state.loading, status]); useEffect(() => { if (status === "disconnected") { connectionEpochRef.current += 1; setAgentsLoaded(false); loadAgentsInFlightRef.current = null; gatewayConfigSnapshot.current = null; lastLoadAgentsStartedAtRef.current = 0; hydrateAgents([]); setFeedEvents([]); setDebugRows([]); setRunCountByAgentId({}); setLastSeenByAgentId({}); prevAssistantPreviewRef.current = {}; lastGatewayActivityAtRef.current = 0; } }, [hydrateAgents, status]); useEffect(() => { if (!agentsLoaded) return; const previousByAgentId = prevAssistantPreviewRef.current; const nextByAgentId: Record = {}; let initialized = Object.keys(previousByAgentId).length > 0; for (const agent of state.agents) { const previewText = normalizeOfficeFeedText( agent.lastResult ?? agent.latestPreview, ); const previewTs = agent.lastAssistantMessageAt ?? 0; if (!previewText || previewTs <= 0) continue; nextByAgentId[agent.agentId] = { ts: previewTs, text: previewText }; const previous = previousByAgentId[agent.agentId]; if (!previous) continue; initialized = true; if (previous.ts === previewTs && previous.text === previewText) continue; if (previewTs < previous.ts) continue; setFeedEvents((prev) => [ { id: agent.agentId, name: agent.name || "Agent", text: previewText, ts: previewTs, kind: "reply" as const, }, ...prev, ].slice(0, 6), ); } if (!initialized) { prevAssistantPreviewRef.current = nextByAgentId; return; } prevAssistantPreviewRef.current = nextByAgentId; }, [agentsLoaded, state.agents]); useEffect(() => { if (status !== "connected" || !agentsLoaded) return; const runtimeHandler = createGatewayRuntimeEventHandler({ getStatus: () => status, getAgents: () => stateRef.current.agents, dispatch: (action) => { dispatch(action as never); }, queueLivePatch: (agentId, patch) => { dispatch({ type: "updateAgent", agentId, patch }); if ("status" in patch || "runId" in patch) { const agent = stateRef.current.agents.find( (entry) => entry.agentId === agentId, ); if (agent) { const wasWorking = prevWorkingRef.current[agentId] ?? false; const isNowWorking = patch.status === "running" || Boolean(patch.runId); if (isNowWorking !== wasWorking) { prevWorkingRef.current[agentId] = isNowWorking; const text = isNowWorking ? "started working" : "went idle"; setFeedEvents((prev) => [ { id: agentId, name: agent.name || "Agent", text, ts: Date.now(), kind: "status" as const, }, ...prev, ].slice(0, 6), ); if (isNowWorking) { setRunCountByAgentId((prev) => ({ ...prev, [agentId]: (prev[agentId] ?? 0) + 1, })); } } } } }, clearPendingLivePatch: () => {}, loadSummarySnapshot: async () => { await loadAgents({ minIntervalMs: 3_000, settingsMaxAgeMs: 60_000, silent: true, }); }, requestHistoryRefresh: requestAgentHistoryRefresh, refreshHeartbeatLatestUpdate: () => {}, bumpHeartbeatTick: () => {}, setTimeout: (fn, delayMs) => window.setTimeout(fn, delayMs), clearTimeout: (id) => window.clearTimeout(id), isDisconnectLikeError: isGatewayDisconnectLikeError, logWarn: (message, meta) => console.warn(message, meta), updateSpecialLatestUpdate: () => {}, }); // Run reconciliation before subscribing to events so dedup keys are // populated in the trigger state. This prevents stale gateway event // replays from setting timed room holds on page load. setOfficeTriggerState((previous) => reconcileOfficeAnimationTriggerState({ state: previous, agents: stateRef.current.agents, }), ); const unsubscribeEvent = client.onEvent((event) => { lastGatewayActivityAtRef.current = Date.now(); setOpenClawLogEntries((previous) => { const next = [...previous, formatOpenClawEventLogEntry(event)]; return next.slice(-MAX_OPENCLAW_LOG_ENTRIES); }); refreshRecentTransportSessionHistory(event); setOfficeTriggerState((previous) => reduceOfficeAnimationTriggerEvent({ state: previous, event, agents: stateRef.current.agents, }), ); if (debugEnabled) { console.info("[office-debug] Gateway event.", { event: event.event, seq: event.seq, payload: typeof event.payload === "object" && event.payload !== null ? JSON.stringify(event.payload).slice(0, 220) : (event.payload ?? null), }); } if ( shouldSuppressPhoneBoothAssistantReply({ event, agents: stateRef.current.agents, phoneCallByAgentId: officeTriggerStateRef.current.phoneCallByAgentId, }) ) { return; } runtimeHandler.handleEvent(event); }); const unsubscribeGap = client.onGap(() => { void loadAgents({ minIntervalMs: 5_000, settingsMaxAgeMs: 30_000, silent: true, }); }); return () => { unsubscribeEvent(); unsubscribeGap(); runtimeHandler.dispose(); }; }, [ agentsLoaded, client, debugEnabled, dispatch, loadAgents, refreshRecentTransportSessionHistory, requestAgentHistoryRefresh, status, ]); useEffect(() => { if (status !== "connected" || !agentsLoaded) return; const intervalId = window.setInterval(() => { if (document.visibilityState !== "visible") return; void loadAgents({ minIntervalMs: 60_000, onlyWhenIdleForMs: 120_000, settingsMaxAgeMs: 180_000, silent: true, }); }, 60_000); return () => { window.clearInterval(intervalId); }; }, [agentsLoaded, loadAgents, status]); useEffect(() => { if (status !== "connected" || !agentsLoaded) return; const handleFocus = () => { if (document.visibilityState !== "visible") return; void loadAgents({ minIntervalMs: 15_000, settingsMaxAgeMs: 30_000, silent: true, }); }; window.addEventListener("focus", handleFocus); document.addEventListener("visibilitychange", handleFocus); return () => { window.removeEventListener("focus", handleFocus); document.removeEventListener("visibilitychange", handleFocus); }; }, [agentsLoaded, loadAgents, status]); useEffect(() => { setOfficeTriggerState((previous) => reconcileOfficeAnimationTriggerState({ state: previous, agents: state.agents, }), ); }, [state.agents]); useEffect(() => { setMarketplaceGymHoldByAgentId((previous) => { const activeAgentIds = new Set( state.agents.map((agent) => agent.agentId), ); const next = Object.fromEntries( Object.entries(previous).filter( ([agentId, held]) => held && activeAgentIds.has(agentId), ), ); if ( Object.keys(previous).length === Object.keys(next).length && Object.keys(previous).every( (agentId) => previous[agentId] === next[agentId], ) ) { return previous; } return next; }); }, [state.agents]); useEffect(() => { if (!monitorAgentId) return; if (state.agents.some((agent) => agent.agentId === monitorAgentId)) return; setMonitorAgentId(null); }, [monitorAgentId, state.agents]); useEffect(() => { if (!githubReviewAgentId) return; if (state.agents.some((agent) => agent.agentId === githubReviewAgentId)) { return; } setGithubReviewAgentId(null); }, [githubReviewAgentId, state.agents]); useEffect(() => { if (!qaTestingAgentId) return; if (state.agents.some((agent) => agent.agentId === qaTestingAgentId)) { return; } setQaTestingAgentId(null); }, [qaTestingAgentId, state.agents]); useEffect(() => { if (status !== "connected") return; let cancelled = false; void (async () => { try { const result = await client.call<{ models: GatewayModelChoice[] }>( "models.list", {}, ); if (!cancelled) { setGatewayModels( buildGatewayModelChoices( Array.isArray(result.models) ? result.models : [], null, ), ); } } catch { // Models are optional - chat still works without model selection. } })(); return () => { cancelled = true; }; }, [status, client]); useEffect(() => { if (chatOpen && !selectedChatAgentId && state.agents.length > 0) { setSelectedChatAgentId(state.agents[0].agentId); } }, [chatOpen, selectedChatAgentId, state.agents]); const chatController = useChatInteractionController({ client, status, agents: state.agents, dispatch: (action) => dispatch(action as never), setError, getAgents: () => stateRef.current.agents, clearRunTracking: () => {}, clearHistoryInFlight: () => {}, clearSpecialUpdateMarker: () => {}, clearSpecialLatestUpdateInFlight: () => {}, setInspectSidebarNull: () => {}, setMobilePaneChat: () => {}, }); const focusedChatAgent = selectedChatAgentId ? (state.agents.find((agent) => agent.agentId === selectedChatAgentId) ?? null) : null; const agentEditorAgent = agentEditorAgentId ? (state.agents.find((agent) => agent.agentId === agentEditorAgentId) ?? null) : null; const mainAgent = state.agents.find((agent) => agent.agentId === MAIN_AGENT_ID) ?? null; const runLog = useRunLog({ client, status, agents: state.agents }); const standupAgentSnapshots = useMemo( () => state.agents.map((agent) => ({ agentId: agent.agentId, name: agent.name || agent.agentId, latestPreview: agent.latestPreview, lastUserMessage: agent.lastUserMessage, })), [state.agents], ); const standupController = useOfficeStandupController({ gatewayUrl, agents: standupAgentSnapshots, }); const handleMarketplaceGymStart = useCallback((agentId: string) => { setMarketplaceGymHoldByAgentId((previous) => ({ ...previous, [agentId]: true, })); }, []); const handleMarketplaceGymEnd = useCallback((agentId: string) => { setMarketplaceGymHoldByAgentId((previous) => { if (!previous[agentId]) return previous; const next = { ...previous }; delete next[agentId]; return next; }); }, []); const marketplace = useOfficeSkillsMarketplace({ client, status, agents: state.agents, preferredAgentId: selectedChatAgentId, onSkillActivityStart: handleMarketplaceGymStart, onSkillActivityEnd: handleMarketplaceGymEnd, }); const animationNowMs = Date.now(); const officeAnimationState = useMemo( () => buildOfficeAnimationState({ state: officeTriggerState, agents: state.agents, marketplaceGymHoldByAgentId, nowMs: animationNowMs, }), [ animationNowMs, marketplaceGymHoldByAgentId, officeTriggerState, state.agents, ], ); const { deskHoldByAgentId, githubHoldByAgentId, manualGymUntilByAgentId, pendingStandupRequest, phoneBoothHoldByAgentId, phoneCallByAgentId, qaHoldByAgentId, smsBoothHoldByAgentId, skillGymHoldByAgentId, textMessageByAgentId, workingUntilByAgentId, } = officeAnimationState; const immediateGymHoldByAgentId = useMemo( () => ({ ...marketplaceGymHoldByAgentId, ...skillGymHoldByAgentId, }), [marketplaceGymHoldByAgentId, skillGymHoldByAgentId], ); useEffect(() => { const now = Date.now(); setGymCooldownUntilByAgentId((previous) => { const next: Record = {}; for (const agent of state.agents) { const agentId = agent.agentId; const immediateHeld = Boolean(immediateGymHoldByAgentId[agentId]); const wasImmediateHeld = prevImmediateGymHoldRef.current[agentId] ?? false; const previousUntil = previous[agentId] ?? 0; if (immediateHeld) { if (previousUntil > now) { next[agentId] = previousUntil; } continue; } if (wasImmediateHeld) { next[agentId] = now + GYM_WORKOUT_LATCH_MS; continue; } if (previousUntil > now) { next[agentId] = previousUntil; } } prevImmediateGymHoldRef.current = Object.fromEntries( state.agents.map((agent) => [ agent.agentId, Boolean(immediateGymHoldByAgentId[agent.agentId]), ]), ); return next; }); }, [immediateGymHoldByAgentId, state.agents]); const activeGithubReviewAgentId = useMemo( () => state.agents.find((agent) => githubHoldByAgentId[agent.agentId]) ?.agentId ?? null, [githubHoldByAgentId, state.agents], ); const activeQaTestingAgentId = useMemo( () => state.agents.find((agent) => qaHoldByAgentId[agent.agentId])?.agentId ?? null, [qaHoldByAgentId, state.agents], ); useEffect(() => { setGithubReviewAgentId(activeGithubReviewAgentId); }, [activeGithubReviewAgentId]); useEffect(() => { if (!activeGithubReviewAgentId) return; setSelectedChatAgentId(activeGithubReviewAgentId); dispatch({ type: "selectAgent", agentId: activeGithubReviewAgentId }); }, [activeGithubReviewAgentId, dispatch]); useEffect(() => { setQaTestingAgentId(activeQaTestingAgentId); }, [activeQaTestingAgentId]); useEffect(() => { if (!activeQaTestingAgentId) return; setSelectedChatAgentId(activeQaTestingAgentId); dispatch({ type: "selectAgent", agentId: activeQaTestingAgentId }); }, [activeQaTestingAgentId, dispatch]); useEffect(() => { const activeKeys = new Set( Object.values(phoneCallByAgentId).map((request) => request.key), ); promptedPhoneCallKeysRef.current = new Set( [...promptedPhoneCallKeysRef.current].filter((key) => activeKeys.has(key)), ); preparedPhoneCallKeysRef.current = new Set( [...preparedPhoneCallKeysRef.current].filter((key) => activeKeys.has(key)), ); spokenPhoneCallKeysRef.current = new Set( [...spokenPhoneCallKeysRef.current].filter((key) => activeKeys.has(key)), ); setPreparedPhoneCallsByAgentId((previous) => { const next = Object.fromEntries( Object.entries(previous).filter(([, entry]) => activeKeys.has(entry.requestKey)), ); if ( Object.keys(previous).length === Object.keys(next).length && Object.keys(previous).every((agentId) => previous[agentId] === next[agentId]) ) { return previous; } return next; }); }, [phoneCallByAgentId]); useEffect(() => { const requests = Object.entries(phoneCallByAgentId); if (requests.length === 0) return; const appendPromptForAgent = (agentId: string, request: OfficePhoneCallRequest) => { const agent = state.agents.find((entry) => entry.agentId === agentId); if (!agent) return; promptedPhoneCallKeysRef.current.add(request.key); void fetch("/api/office/call", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ callee: request.callee, message: null, }), }) .then(async (response) => { const body = (await response.json().catch(() => null)) as { scenario?: MockPhoneCallScenario; } | null; const promptText = body?.scenario?.promptText?.trim(); if (!response.ok || !promptText) { promptedPhoneCallKeysRef.current.delete(request.key); return; } setSelectedChatAgentId(agentId); setChatOpen(true); dispatch({ type: "selectAgent", agentId }); dispatch({ type: "appendOutput", agentId, line: buildPhoneCallOutputLine(promptText), }); }) .catch(() => { promptedPhoneCallKeysRef.current.delete(request.key); }); }; const prepareScenarioForAgent = (agentId: string, request: OfficePhoneCallRequest) => { preparedPhoneCallKeysRef.current.add(request.key); void fetch("/api/office/call", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ callee: request.callee, message: request.message, }), }) .then(async (response) => { const body = (await response.json().catch(() => null)) as { scenario?: MockPhoneCallScenario; } | null; const scenario = body?.scenario; if (!response.ok || !scenario) { preparedPhoneCallKeysRef.current.delete(request.key); return; } setPreparedPhoneCallsByAgentId((previous) => ({ ...previous, [agentId]: { requestKey: request.key, scenario, }, })); }) .catch(() => { preparedPhoneCallKeysRef.current.delete(request.key); }); }; for (const [agentId, request] of requests) { if ( request.phase === "needs_message" && !promptedPhoneCallKeysRef.current.has(request.key) ) { appendPromptForAgent(agentId, request); } if ( request.phase === "ready_to_call" && !preparedPhoneCallKeysRef.current.has(request.key) ) { prepareScenarioForAgent(agentId, request); } } }, [dispatch, phoneCallByAgentId, state.agents]); const activePhoneBoothAgentId = useMemo( () => state.agents.find((agent) => { if (!phoneBoothHoldByAgentId[agent.agentId]) return false; const prepared = preparedPhoneCallsByAgentId[agent.agentId]; const request = phoneCallByAgentId[agent.agentId]; return Boolean(prepared && request && prepared.requestKey === request.key); })?.agentId ?? null, [phoneBoothHoldByAgentId, phoneCallByAgentId, preparedPhoneCallsByAgentId, state.agents], ); const activePhoneCallScenario = useMemo(() => { if (!activePhoneBoothAgentId) return null; return preparedPhoneCallsByAgentId[activePhoneBoothAgentId]?.scenario ?? null; }, [activePhoneBoothAgentId, preparedPhoneCallsByAgentId]); const handlePhoneCallSpeak = useCallback( ({ agentId, requestKey }: PhoneCallSpeakPayload) => { if (spokenPhoneCallKeysRef.current.has(requestKey)) return; spokenPhoneCallKeysRef.current.add(requestKey); setSelectedChatAgentId(agentId); dispatch({ type: "selectAgent", agentId }); }, [dispatch], ); const handlePhoneCallComplete = useCallback( (agentId: string) => { setPreparedPhoneCallsByAgentId((previous) => { const next = { ...previous }; delete next[agentId]; return next; }); const request = phoneCallByAgentId[agentId]; if (request) { dispatch({ type: "appendOutput", agentId, line: buildPhoneCallOutputLine(`Call with ${request.callee} finished.`), }); } setOfficeTriggerState((previous) => clearOfficeAnimationTriggerHold({ state: previous, hold: "call", agentId, }), ); }, [dispatch, phoneCallByAgentId], ); useEffect(() => { const activeKeys = new Set( Object.values(textMessageByAgentId).map((request) => request.key), ); promptedTextMessageKeysRef.current = new Set( [...promptedTextMessageKeysRef.current].filter((key) => activeKeys.has(key)), ); preparedTextMessageKeysRef.current = new Set( [...preparedTextMessageKeysRef.current].filter((key) => activeKeys.has(key)), ); setPreparedTextMessagesByAgentId((previous) => { const next = Object.fromEntries( Object.entries(previous).filter(([, entry]) => activeKeys.has(entry.requestKey)), ); if ( Object.keys(previous).length === Object.keys(next).length && Object.keys(previous).every((agentId) => previous[agentId] === next[agentId]) ) { return previous; } return next; }); }, [textMessageByAgentId]); useEffect(() => { const requests = Object.entries(textMessageByAgentId); if (requests.length === 0) return; const appendPromptForAgent = (agentId: string, request: OfficeTextMessageRequest) => { const agent = state.agents.find((entry) => entry.agentId === agentId); if (!agent) return; promptedTextMessageKeysRef.current.add(request.key); void fetch("/api/office/text", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ recipient: request.recipient, message: null, }), }) .then(async (response) => { const body = (await response.json().catch(() => null)) as { scenario?: MockTextMessageScenario; } | null; const promptText = body?.scenario?.promptText?.trim(); if (!response.ok || !promptText) { promptedTextMessageKeysRef.current.delete(request.key); return; } setSelectedChatAgentId(agentId); setChatOpen(true); dispatch({ type: "selectAgent", agentId }); dispatch({ type: "appendOutput", agentId, line: buildTextMessageOutputLine(promptText), }); }) .catch(() => { promptedTextMessageKeysRef.current.delete(request.key); }); }; const prepareScenarioForAgent = (agentId: string, request: OfficeTextMessageRequest) => { preparedTextMessageKeysRef.current.add(request.key); void fetch("/api/office/text", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ recipient: request.recipient, message: request.message, }), }) .then(async (response) => { const body = (await response.json().catch(() => null)) as { scenario?: MockTextMessageScenario; } | null; const scenario = body?.scenario; if (!response.ok || !scenario) { preparedTextMessageKeysRef.current.delete(request.key); return; } setPreparedTextMessagesByAgentId((previous) => ({ ...previous, [agentId]: { requestKey: request.key, scenario, }, })); }) .catch(() => { preparedTextMessageKeysRef.current.delete(request.key); }); }; for (const [agentId, request] of requests) { if ( request.phase === "needs_message" && !promptedTextMessageKeysRef.current.has(request.key) ) { appendPromptForAgent(agentId, request); } if ( request.phase === "ready_to_send" && !preparedTextMessageKeysRef.current.has(request.key) ) { prepareScenarioForAgent(agentId, request); } } }, [dispatch, state.agents, textMessageByAgentId]); const activeSmsBoothAgentId = useMemo( () => state.agents.find((agent) => { if (!smsBoothHoldByAgentId[agent.agentId]) return false; const prepared = preparedTextMessagesByAgentId[agent.agentId]; const request = textMessageByAgentId[agent.agentId]; return Boolean(prepared && request && prepared.requestKey === request.key); })?.agentId ?? null, [preparedTextMessagesByAgentId, smsBoothHoldByAgentId, state.agents, textMessageByAgentId], ); const activeTextMessageScenario = useMemo(() => { if (!activeSmsBoothAgentId) return null; return preparedTextMessagesByAgentId[activeSmsBoothAgentId]?.scenario ?? null; }, [activeSmsBoothAgentId, preparedTextMessagesByAgentId]); const handleTextMessageComplete = useCallback( (agentId: string) => { setPreparedTextMessagesByAgentId((previous) => { const next = { ...previous }; delete next[agentId]; return next; }); const request = textMessageByAgentId[agentId]; if (request) { dispatch({ type: "appendOutput", agentId, line: buildTextMessageOutputLine(`Message to ${request.recipient} sent.`), }); } setOfficeTriggerState((previous) => clearOfficeAnimationTriggerHold({ state: previous, hold: "text", agentId, }), ); }, [dispatch, textMessageByAgentId], ); const gymHoldByAgentId = useMemo(() => { const next: Record = {}; for (const agent of state.agents) { const agentId = agent.agentId; if ( immediateGymHoldByAgentId[agentId] || (manualGymUntilByAgentId[agentId] ?? 0) > animationNowMs || (gymCooldownUntilByAgentId[agentId] ?? 0) > animationNowMs ) { next[agentId] = true; } } return next; }, [ animationNowMs, gymCooldownUntilByAgentId, immediateGymHoldByAgentId, manualGymUntilByAgentId, state.agents, ]); const handleOpenAgentChat = useCallback( (agentId: string) => { setSelectedChatAgentId(agentId); setChatOpen(true); dispatch({ type: "selectAgent", agentId }); }, [dispatch], ); const lastStandupTriggerKeyRef = useRef(null); const triggerStandupMeeting = useCallback( async (message: string) => { const trimmed = message.trim(); if (!trimmed) return false; if ( standupController.meeting && standupController.meeting.phase !== "complete" ) { return false; } await standupController.startMeeting("manual"); return true; }, [standupController], ); const handleGithubReviewDismiss = useCallback(() => { if (!githubReviewAgentId) return; setOfficeTriggerState((previous) => clearOfficeAnimationTriggerHold({ state: previous, hold: "github", agentId: githubReviewAgentId, }), ); }, [githubReviewAgentId]); const handleQaDismiss = useCallback(() => { if (!qaTestingAgentId) return; setOfficeTriggerState((previous) => clearOfficeAnimationTriggerHold({ state: previous, hold: "qa", agentId: qaTestingAgentId, }), ); }, [qaTestingAgentId]); const handleChatSend = useCallback( async (agentId: string, sessionKey: string, message: string) => { stopVoiceReplyPlayback(); const trimmed = message.trim(); if (!trimmed) return; const intentSnapshot = resolveOfficeIntentSnapshot(trimmed); setOpenClawLogEntries((previous) => { const next = [ ...previous, createOpenClawLogEntry({ eventName: "office-intent", eventKind: "derived", summary: `agent=${agentId} gym=${intentSnapshot.gym?.source ?? "-"} qa=${intentSnapshot.qa ?? "-"} github=${intentSnapshot.github ?? "-"} desk=${intentSnapshot.desk ?? "-"} text=${intentSnapshot.text?.phase ?? "-"}`, payload: { agentId, message: trimmed, normalized: intentSnapshot.normalized, intentSnapshot, }, }), ]; return next.slice(-MAX_OPENCLAW_LOG_ENTRIES); }); const pendingPhoneCall = phoneCallByAgentId[agentId] ?? null; const pendingTextMessage = textMessageByAgentId[agentId] ?? null; const hasImmediateOfficeTrigger = Boolean( intentSnapshot.desk || intentSnapshot.github || intentSnapshot.gym || intentSnapshot.qa || intentSnapshot.standup || intentSnapshot.text, ); const isPhoneCallFollowUp = pendingPhoneCall?.phase === "needs_message" && !intentSnapshot.call && !intentSnapshot.text && !intentSnapshot.desk && !intentSnapshot.github && !intentSnapshot.gym && !intentSnapshot.qa && !intentSnapshot.standup; const isTextMessageFollowUp = pendingTextMessage?.phase === "needs_message" && !intentSnapshot.call && !intentSnapshot.text && !intentSnapshot.desk && !intentSnapshot.github && !intentSnapshot.gym && !intentSnapshot.qa && !intentSnapshot.standup; if ( hasImmediateOfficeTrigger && !intentSnapshot.call && !isPhoneCallFollowUp && !isTextMessageFollowUp ) { const nowMs = Date.now(); const runId = randomUUID(); setOfficeTriggerState((previous) => reduceOfficeAnimationTriggerEvent({ state: previous, agents: stateRef.current.agents, nowMs, event: { type: "event", event: "chat", payload: { runId, sessionKey, state: "final", message: { role: "user", content: trimmed, }, }, }, }), ); } if (intentSnapshot.call || isPhoneCallFollowUp) { const nowMs = Date.now(); const runId = randomUUID(); dispatch({ type: "updateAgent", agentId, patch: { draft: "", lastUserMessage: trimmed, lastActivityAt: nowMs, }, }); dispatch({ type: "appendOutput", agentId, line: `> ${trimmed}`, transcript: { source: "local-send", runId, sessionKey, timestampMs: nowMs, role: "user", kind: "user", confirmed: true, }, }); setOfficeTriggerState((previous) => reduceOfficeAnimationTriggerEvent({ state: previous, agents: stateRef.current.agents, nowMs, event: { type: "event", event: "chat", payload: { runId, sessionKey, state: "final", message: { role: "user", content: trimmed, }, }, }, }), ); return; } await chatController.handleSend(agentId, sessionKey, trimmed); }, [ chatController, dispatch, phoneCallByAgentId, stopVoiceReplyPlayback, textMessageByAgentId, ], ); useEffect(() => { if (!pendingStandupRequest) return; if (lastStandupTriggerKeyRef.current === pendingStandupRequest.key) return; if ( standupController.meeting && standupController.meeting.phase !== "complete" ) { return; } lastStandupTriggerKeyRef.current = pendingStandupRequest.key; void triggerStandupMeeting(pendingStandupRequest.message).catch((error) => { console.error("Failed to trigger standup meeting.", error); }); }, [pendingStandupRequest, standupController.meeting, triggerStandupMeeting]); const transcribeVoicePayload = useCallback( async (payload: VoiceSendPayload) => { const file = new File([payload.blob], payload.fileName, { type: payload.mimeType, }); const formData = new FormData(); formData.set("audio", file); const response = await fetch("/api/office/voice/transcribe", { method: "POST", body: formData, }); const result = (await response.json().catch(() => null)) as { transcript?: string | null; error?: string; ignored?: boolean; } | null; if (!response.ok) { throw new Error( result?.error?.trim() || "Failed to transcribe voice input.", ); } if (result?.ignored) { return null; } const transcript = result?.transcript?.trim() ?? ""; if (!transcript) { throw new Error("OpenClaw returned an empty transcript."); } return transcript; }, [], ); const sendVoicePayloadToAgent = useCallback( async ( agent: Pick | null, payload: VoiceSendPayload, ) => { if (!agent) { throw new Error("Target agent not found."); } const transcript = await transcribeVoicePayload(payload); if (!transcript) return; await handleChatSend(agent.agentId, agent.sessionKey, transcript); }, [handleChatSend, transcribeVoicePayload], ); const handleVoiceSend = useCallback( async (payload: VoiceSendPayload) => { if (!focusedChatAgent) { throw new Error("Select an agent before using push-to-talk."); } await sendVoicePayloadToAgent(focusedChatAgent, payload); }, [focusedChatAgent, sendVoicePayloadToAgent], ); const { state: mainVoiceState, error: mainVoiceError, supported: mainVoiceSupported, start: startMainVoiceRecording, stop: stopMainVoiceRecording, clearError: clearMainVoiceError, } = useVoiceRecorder({ enabled: status === "connected" && Boolean(mainAgent), onVoiceSend: async (payload) => { if (!mainAgent) { throw new Error("Main agent not found."); } await sendVoicePayloadToAgent(mainAgent, payload); }, }); useFinalizedAssistantReplyListener(state.agents, ({ text }) => { if (!voiceRepliesLoaded || !voiceRepliesEnabled) return; enqueueVoiceReply({ text, provider: voiceRepliesPreference.provider, voiceId: voiceRepliesPreference.voiceId, }); }); useEffect(() => { const optionHeldRef = { current: false }; const handleKeyDown = (event: globalThis.KeyboardEvent) => { if (event.key !== "Alt" || event.repeat || optionHeldRef.current) return; optionHeldRef.current = true; event.preventDefault(); void startMainVoiceRecording(); }; const handleKeyUp = (event: globalThis.KeyboardEvent) => { if (event.key !== "Alt") return; optionHeldRef.current = false; event.preventDefault(); stopMainVoiceRecording(); }; const handleWindowBlur = () => { optionHeldRef.current = false; stopMainVoiceRecording(); }; window.addEventListener("keydown", handleKeyDown, true); window.addEventListener("keyup", handleKeyUp, true); window.addEventListener("blur", handleWindowBlur); return () => { window.removeEventListener("keydown", handleKeyDown, true); window.removeEventListener("keyup", handleKeyUp, true); window.removeEventListener("blur", handleWindowBlur); }; }, [startMainVoiceRecording, stopMainVoiceRecording]); useEffect(() => { if (!mainVoiceError) return; const timer = window.setTimeout(() => { clearMainVoiceError(); }, 4000); return () => { window.clearTimeout(timer); }; }, [clearMainVoiceError, mainVoiceError]); const officeAgents = useMemo(() => { void clockTick; const now = Date.now(); const nextCache = new Map< string, { agent: AgentState; deskHeld: boolean; gymHeld: boolean; latchedWorking: boolean; officeAgent: OfficeAgent; phoneBoothHeld: boolean; qaHeld: boolean; smsBoothHeld: boolean; } >(); const nextOfficeAgents = state.agents.map((agent) => { const latchedWorking = (workingUntilByAgentId[agent.agentId] ?? 0) > now; const deskHeld = Boolean(deskHoldByAgentId[agent.agentId]); const gymHeld = Boolean(gymHoldByAgentId[agent.agentId]); const phoneBoothHeld = Boolean(phoneBoothHoldByAgentId[agent.agentId]); const qaHeld = Boolean(qaHoldByAgentId[agent.agentId]); const smsBoothHeld = Boolean(smsBoothHoldByAgentId[agent.agentId]); const cached = officeAgentCacheRef.current.get(agent.agentId); if ( cached && cached.agent === agent && cached.latchedWorking === latchedWorking && cached.deskHeld === deskHeld && cached.gymHeld === gymHeld && cached.phoneBoothHeld === phoneBoothHeld && cached.qaHeld === qaHeld && cached.smsBoothHeld === smsBoothHeld ) { nextCache.set(agent.agentId, cached); return cached.officeAgent; } const effectiveAgent: AgentState = latchedWorking && agent.status !== "error" ? { ...agent, status: "running", runId: agent.runId ?? `latched-${agent.agentId}`, } : (deskHeld || gymHeld || qaHeld || phoneBoothHeld || smsBoothHeld) && agent.status !== "error" ? { ...agent, status: "running", runId: agent.runId ?? (qaHeld ? `qa-hold-${agent.agentId}` : smsBoothHeld ? `text-hold-${agent.agentId}` : phoneBoothHeld ? `call-hold-${agent.agentId}` : gymHeld ? `gym-hold-${agent.agentId}` : `desk-hold-${agent.agentId}`), } : agent; const officeAgent = mapAgentToOffice(effectiveAgent); nextCache.set(agent.agentId, { agent, deskHeld, gymHeld, latchedWorking, officeAgent, phoneBoothHeld, qaHeld, smsBoothHeld, }); return officeAgent; }); officeAgentCacheRef.current = nextCache; return nextOfficeAgents; }, [ clockTick, deskHoldByAgentId, gymHoldByAgentId, phoneBoothHoldByAgentId, qaHoldByAgentId, smsBoothHoldByAgentId, state.agents, workingUntilByAgentId, ]); const openClawLiveStateText = useMemo(() => { const lines = ["== LIVE OPENCLAW STATE =="]; if (state.agents.length === 0) { lines.push("No agents loaded yet."); return lines.join("\n"); } for (const agent of state.agents) { lines.push(""); lines.push(`[${agent.agentId}] ${agent.name || "Agent"}`); lines.push( `status=${agent.status} runId=${agent.runId ?? "-"} session=${agent.sessionKey}`, ); lines.push( `lastActivity=${agent.lastActivityAt ? formatOpenClawTimestamp(agent.lastActivityAt) : "-"} lastAssistant=${agent.lastAssistantMessageAt ? formatOpenClawTimestamp(agent.lastAssistantMessageAt) : "-"}`, ); lines.push( `latestPreview=${formatOpenClawValue(agent.latestPreview)} lastUser=${formatOpenClawValue(agent.lastUserMessage)}`, ); if (agent.thinkingTrace?.trim()) { lines.push("thinking>"); lines.push(agent.thinkingTrace.trim()); } if (agent.streamText?.trim()) { lines.push("assistant_stream>"); lines.push(agent.streamText.trim()); } const recentOutput = agent.outputLines .slice(-MAX_OPENCLAW_AGENT_OUTPUT_LINES) .map((line) => line.trimEnd()) .filter(Boolean); if (recentOutput.length > 0) { lines.push("recent_output>"); lines.push(...recentOutput); } } return lines.join("\n"); }, [state.agents]); const normalizedOpenClawConsoleSearch = openClawConsoleSearch .trim() .toLowerCase(); const filteredOpenClawLogEntries = useMemo(() => { if (!normalizedOpenClawConsoleSearch) return openClawLogEntries; return openClawLogEntries.filter((entry) => [ entry.timestamp, entry.eventName, entry.eventKind, entry.summary, entry.role ?? "", entry.messageText ?? "", entry.thinkingText ?? "", entry.streamText ?? "", entry.toolText ?? "", entry.payloadText, ] .join("\n") .toLowerCase() .includes(normalizedOpenClawConsoleSearch), ); }, [normalizedOpenClawConsoleSearch, openClawLogEntries]); const openClawLiveStateMatchesSearch = useMemo(() => { if (!normalizedOpenClawConsoleSearch) return true; return openClawLiveStateText .toLowerCase() .includes(normalizedOpenClawConsoleSearch); }, [normalizedOpenClawConsoleSearch, openClawLiveStateText]); const openClawConsoleExportJson = useMemo( () => safeJsonStringify({ exportedAt: new Date().toISOString(), searchQuery: openClawConsoleSearch, visibleEventCount: filteredOpenClawLogEntries.length, totalEventCount: openClawLogEntries.length, liveStateMatchesSearch: openClawLiveStateMatchesSearch, liveStateText: openClawLiveStateText, events: filteredOpenClawLogEntries, }), [ filteredOpenClawLogEntries, openClawConsoleSearch, openClawLiveStateMatchesSearch, openClawLiveStateText, openClawLogEntries.length, ], ); const handleClearOpenClawConsole = useCallback(() => { setOpenClawLogEntries([]); }, []); const handleCopyOpenClawConsoleJson = useCallback(async () => { try { await navigator.clipboard.writeText(openClawConsoleExportJson); setOpenClawConsoleCopyStatus("copied"); window.setTimeout(() => { setOpenClawConsoleCopyStatus("idle"); }, 1800); } catch (error) { console.error("Failed to copy OpenClaw console JSON.", error); setOpenClawConsoleCopyStatus("error"); window.setTimeout(() => { setOpenClawConsoleCopyStatus("idle"); }, 1800); } }, [openClawConsoleExportJson]); const handleDownloadOpenClawConsoleJson = useCallback(() => { const blob = new Blob([openClawConsoleExportJson], { type: "application/json;charset=utf-8", }); const url = window.URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = `openclaw-events-${Date.now()}.json`; document.body.appendChild(anchor); anchor.click(); anchor.remove(); window.URL.revokeObjectURL(url); }, [openClawConsoleExportJson]); const monitorByAgentId = useMemo( () => { const nextCache = new Map< string, { agent: AgentState; monitor: OfficeDeskMonitor } >(); const nextMonitorByAgentId: Record = {}; for (const agent of state.agents) { const cached = deskMonitorCacheRef.current.get(agent.agentId); if (cached && cached.agent === agent) { nextCache.set(agent.agentId, cached); nextMonitorByAgentId[agent.agentId] = cached.monitor; continue; } const monitor = buildOfficeDeskMonitor(agent); const entry = { agent, monitor }; nextCache.set(agent.agentId, entry); nextMonitorByAgentId[agent.agentId] = monitor; } deskMonitorCacheRef.current = nextCache; return nextMonitorByAgentId; }, [state.agents], ); const githubSkill = useMemo( () => marketplace.skillsReport?.skills.find((skill) => { const normalizedKey = skill.skillKey.trim().toLowerCase(); const normalizedName = skill.name.trim().toLowerCase(); return normalizedKey === "github" || normalizedName === "github"; }) ?? null, [marketplace.skillsReport], ); if ( !agentsLoaded && (!connectPromptReady || (gatewayUrl.trim().length > 0 && !shouldPromptForConnect && (!didAttemptGatewayConnect || status === "connecting"))) ) { return (
CONNECTING TO GATEWAY...
); } if ( connectPromptReady && status === "disconnected" && !agentsLoaded && (shouldPromptForConnect || didAttemptGatewayConnect) ) { return (
void connect()} />
); } const runningCount = state.agents.filter( (agent) => agent.status === "running" || deskHoldByAgentId[agent.agentId] || gymHoldByAgentId[agent.agentId] || phoneBoothHoldByAgentId[agent.agentId] || smsBoothHoldByAgentId[agent.agentId] || qaHoldByAgentId[agent.agentId], ).length; const unseenInboxCount = state.agents.filter( (agent) => agent.hasUnseenActivity, ).length; const showEmptyFleetBanner = status === "connected" && agentsLoaded && state.agents.length === 0; const emptyFleetMessage = state.error?.trim() || "Connected to the gateway, but no agents were loaded into the office."; return (
{ void previewVoiceReply({ text: `Hi, how can I help you? My name is ${voiceName}.`, provider: voiceRepliesPreference.provider, voiceId, speed: voiceRepliesSpeed, }); }} atmAnalytics={{ client, status, agents: state.agents, gatewayUrl, settingsCoordinator, }} onGatewayDisconnect={disconnect} feedEvents={feedEvents} gatewayStatus={status} runCountByAgentId={runCountByAgentId} lastSeenByAgentId={lastSeenByAgentId} standupMeeting={standupController.meeting} standupAutoOpenBoard={standupController.openBoardByDefault} onStandupArrivalsChange={(arrivedAgentIds) => { void standupController.reportArrivals(arrivedAgentIds); }} onStandupStartRequested={() => { if ( !standupController.meeting || standupController.meeting.phase === "complete" ) { void standupController.startMeeting("manual"); } }} onMonitorSelect={(agentId) => { setMonitorAgentId(agentId); if (agentId) { setSelectedChatAgentId(agentId); dispatch({ type: "selectAgent", agentId }); } }} onAgentEdit={(agentId) => { openAgentEditor(agentId, "avatar"); }} onDeskAssignmentChange={handleDeskAssignmentChange} onDeskAssignmentsReset={handleDeskAssignmentsReset} onGithubReviewDismiss={() => { handleGithubReviewDismiss(); }} onQaLabDismiss={() => { handleQaDismiss(); }} onPhoneCallSpeak={handlePhoneCallSpeak} onPhoneCallComplete={handlePhoneCallComplete} onTextMessageComplete={handleTextMessageComplete} onOpenGithubSkillSetup={() => { setSidebarOpen(true); setActiveSidebarTab("marketplace"); }} /> {showEmptyFleetBanner ? (

Office fleet status

{emptyFleetMessage}

) : null} {!debugEnabled ? ( setSidebarOpen((prev) => !prev)} onTabChange={setActiveSidebarTab} inboxPanel={ { handleOpenAgentChat(agentId); setActiveSidebarTab("inbox"); }} /> } historyPanel={ { handleOpenAgentChat(agentId); setActiveSidebarTab("history"); }} /> } playbooksPanel={ } marketplacePanel={ { handleOpenAgentChat(agentId); router.push("/office"); }} /> } analyticsPanel={ { handleOpenAgentChat(agentId); setActiveSidebarTab("analytics"); }} /> } /> ) : null} {showOpenClawConsole ? (
OpenClaw Event Console
agents {state.agents.length} | events{" "} {filteredOpenClawLogEntries.length}/{openClawLogEntries.length}
{!openClawConsoleCollapsed ? (
setOpenClawConsoleSearch(event.target.value) } placeholder="Search logs, payloads, thinking, user text." className="min-w-0 flex-1 rounded border border-cyan-500/20 bg-black/35 px-2 py-1 text-[10px] normal-case tracking-normal text-cyan-50 placeholder:text-cyan-100/30 focus:border-cyan-400/40 focus:outline-none" /> {openClawConsoleSearch ? ( ) : null}
{openClawLiveStateMatchesSearch ? (
Live OpenClaw State
                  {renderOpenClawHighlightedText(
                    openClawLiveStateText,
                    openClawConsoleSearch,
                  )}
                
) : (
Live OpenClaw state does not match the current search.
)}
Raw OpenClaw Gateway Events
{filteredOpenClawLogEntries.length === 0 ? (
{openClawLogEntries.length === 0 ? "No OpenClaw gateway events received yet." : "No OpenClaw events match the current search."}
) : ( filteredOpenClawLogEntries.map((entry) => { const isUserMessage = entry.role === "user"; return (
{renderOpenClawHighlightedText( `[${entry.timestamp}] ${entry.eventName} / ${entry.eventKind}`, openClawConsoleSearch, )}
{entry.role ? ( {entry.role} ) : null}
{renderOpenClawHighlightedText( entry.summary, openClawConsoleSearch, )}
{entry.messageText ? (
User / Message Text
{renderOpenClawHighlightedText( entry.messageText, openClawConsoleSearch, )}
) : null} {entry.thinkingText ? (
Thinking
{renderOpenClawHighlightedText( entry.thinkingText, openClawConsoleSearch, )}
) : null} {entry.streamText ? (
Stream
{renderOpenClawHighlightedText( entry.streamText, openClawConsoleSearch, )}
) : null} {entry.toolText ? (
Tool Output
{renderOpenClawHighlightedText( entry.toolText, openClawConsoleSearch, )}
) : null}
Raw Payload
                        {renderOpenClawHighlightedText(
                          entry.payloadText,
                          openClawConsoleSearch,
                        )}
                      
); }) )}
) : null}
) : null}
{chatOpen && (
Agents {state.agents.length}
{state.agents.length === 0 ? (
No agents.
) : ( state.agents.map((agent) => { const isSelected = agent.agentId === selectedChatAgentId; const isRunning = agent.status === "running"; return ( ); }) )}
{focusedChatAgent ? ( {}} onOpenSettings={() => { router.push("/office"); }} onNewSession={() => chatController.handleNewSession(focusedChatAgent.agentId) } onModelChange={(value) => dispatch({ type: "updateAgent", agentId: focusedChatAgent.agentId, patch: { model: value ?? undefined }, }) } onThinkingChange={(value) => dispatch({ type: "updateAgent", agentId: focusedChatAgent.agentId, patch: { thinkingLevel: value ?? undefined }, }) } onDraftChange={(value) => chatController.handleDraftChange( focusedChatAgent.agentId, value, ) } onSend={(message) => { void handleChatSend( focusedChatAgent.agentId, focusedChatAgent.sessionKey, message, ); }} onRemoveQueuedMessage={(index) => chatController.removeQueuedMessage( focusedChatAgent.agentId, index, ) } onStopRun={() => { void chatController.handleStopRun( focusedChatAgent.agentId, focusedChatAgent.sessionKey, ); }} onAvatarShuffle={() => openAgentEditor(focusedChatAgent.agentId, "avatar") } onVoiceSend={handleVoiceSend} /> ) : (
Select an agent to chat.
)}
)}
{mainVoiceState !== "idle" || mainVoiceError ? (
Main agent {mainVoiceError ? mainVoiceError : mainVoiceState === "recording" ? "Listening. Release Option to send." : mainVoiceState === "transcribing" ? "Transcribing your voice note." : mainVoiceState === "requesting" ? "Requesting microphone access." : !mainVoiceSupported ? "Voice shortcuts are not supported in this browser." : "Voice shortcut ready."}
) : null} {debugEnabled ? (
office debug
status: {status} | agents: {state.agents.length}
{debugRows.length === 0 ? (
No debug data yet.
) : (
{debugRows.map((row) => (
{row.name} ({row.agentId})
storeStatus={row.storeStatus} runId={row.runId ?? "null"}{" "} inferredRunning= {String(row.inferredRunning)}
lastRole={row.lastRole} messages={row.messageCount}
detectedSession={row.detectedSessionKey || "-"}
lastText={row.lastText || "-"}
sessions={row.inspectedSessions || "-"}
source={row.inferenceSource}
at={row.at}
))}
)}
) : null} {agentEditorAgent ? ( { setAgentEditorAgentId(null); }} onAvatarSave={handleAvatarProfileSave} onRename={async (agentId, name) => { if (!client) return false; try { await renameGatewayAgent({ client, agentId, name }); dispatch({ type: "updateAgent", agentId, patch: { name } }); return true; } catch { return false; } }} /> ) : null}
); }