"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<string, OfficePhoneCallRequest>;
}): 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<string, unknown>)
      : 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() ? (
      <mark
        key={`${part}-${index}`}
        className="rounded bg-amber-300/25 px-0.5 text-amber-100"
      >
        {part}
      </mark>
    ) : (
      part
    ),
  );
};

const resolveMessageRole = (message: unknown) =>
  message && typeof message === "object"
    ? ((message as Record<string, unknown>).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<string, unknown>)
          : 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<Record<string, unknown>>;
};

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<Record<string, unknown>>,
): 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: <T = unknown>(method: string, args: unknown) => Promise<T>;
  };
  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<SessionsListResult>(
    "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<SessionsPreviewResult>(
      "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<ChatHistoryResult>(
      "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<OfficeDebugRow[]>([]);
  const [feedEvents, setFeedEvents] = useState<OfficeFeedEvent[]>([]);
  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<string, { agent: AgentState; monitor: OfficeDeskMonitor }>
  >(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<Record<string, boolean>>({});
  const prevAssistantPreviewRef = useRef<
    Record<string, { ts: number; text: string }>
  >({});
  const [runCountByAgentId, setRunCountByAgentId] = useState<
    Record<string, number>
  >({});
  const [lastSeenByAgentId, setLastSeenByAgentId] = useState<
    Record<string, number>
  >({});
  const [marketplaceGymHoldByAgentId, setMarketplaceGymHoldByAgentId] =
    useState<Record<string, boolean>>({});
  const [gymCooldownUntilByAgentId, setGymCooldownUntilByAgentId] = useState<
    Record<string, number>
  >({});
  const prevImmediateGymHoldRef = useRef<Record<string, boolean>>({});
  const [monitorAgentId, setMonitorAgentId] = useState<string | null>(null);
  const [githubReviewAgentId, setGithubReviewAgentId] = useState<string | null>(
    null,
  );
  const [qaTestingAgentId, setQaTestingAgentId] = useState<string | null>(null);
  const gatewayConfigSnapshot = useRef<GatewayModelPolicySnapshot | null>(null);
  const loadAgentsInFlightRef = useRef<Promise<void> | 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<Set<string>>(new Set());
  const lastTransportHistoryRefreshKeyRef = useRef<Record<string, string>>({});
  const [chatOpen, setChatOpen] = useState(false);
  const [selectedChatAgentId, setSelectedChatAgentId] = useState<string | null>(
    null,
  );
  const [agentEditorAgentId, setAgentEditorAgentId] = useState<string | null>(null);
  const [agentEditorInitialSection, setAgentEditorInitialSection] =
    useState<AgentEditorSection>("avatar");
  const [preparedPhoneCallsByAgentId, setPreparedPhoneCallsByAgentId] = useState<
    Record<string, PreparedPhoneCallEntry>
  >({});
  const [preparedTextMessagesByAgentId, setPreparedTextMessagesByAgentId] = useState<
    Record<string, PreparedTextMessageEntry>
  >({});
  const promptedPhoneCallKeysRef = useRef<Set<string>>(new Set());
  const preparedPhoneCallKeysRef = useRef<Set<string>>(new Set());
  const spokenPhoneCallKeysRef = useRef<Set<string>>(new Set());
  const promptedTextMessageKeysRef = useRef<Set<string>>(new Set());
  const preparedTextMessageKeysRef = useRef<Set<string>>(new Set());
  const [deskAssignmentByDeskUid, setDeskAssignmentByDeskUid] = useState<
    Record<string, string>
  >({});
  const [gatewayModels, setGatewayModels] = useState<GatewayModelChoice[]>([]);
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [activeSidebarTab, setActiveSidebarTab] =
    useState<HQSidebarTab>("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<string, boolean>();
        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<string, unknown>[];
          }>("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<SummaryPreviewSnapshot>(
              "sessions.preview",
              {
                keys: [requestedSessionKey],
                limit: 12,
                maxChars: 400,
              },
            );
            lastUser =
              resolveLatestUserTextFromPreview(
                previewResult,
                requestedSessionKey,
              ) ?? "";
          }
          const targetAgentId =
            parseAgentIdFromSessionKey(requestedSessionKey) ?? params.agentId;
          const patch: Partial<AgentState> = {};
          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<string, { ts: number; text: string }> = {};
    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<StandupAgentSnapshot[]>(
    () =>
      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<string, number> = {};
      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) =>
      Object.fromEntries(
        Object.entries(previous).filter(([, entry]) => activeKeys.has(entry.requestKey)),
      ),
    );
  }, [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) =>
      Object.fromEntries(
        Object.entries(previous).filter(([, entry]) => activeKeys.has(entry.requestKey)),
      ),
    );
  }, [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<string, boolean> = {};
    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<string | null>(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<AgentState, "agentId" | "sessionKey"> | 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<string, OfficeDeskMonitor> = {};

      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<SkillStatusEntry | null>(
    () =>
      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 (
      <div className="flex min-h-screen items-center justify-center bg-black font-mono text-[#4FC3F7]">
        CONNECTING TO GATEWAY...
      </div>
    );
  }

  if (
    connectPromptReady &&
    status === "disconnected" &&
    !agentsLoaded &&
    (shouldPromptForConnect || didAttemptGatewayConnect)
  ) {
    return (
      <main className="min-h-screen bg-black px-4 py-10">
        <GatewayConnectScreen
          gatewayUrl={gatewayUrl}
          token={token}
          localGatewayDefaults={localGatewayDefaults}
          status={status}
          error={gatewayError}
          showApprovalHint={didAttemptGatewayConnect}
          onGatewayUrlChange={setGatewayUrl}
          onTokenChange={setToken}
          onUseLocalDefaults={useLocalGatewayDefaults}
          onConnect={() => void connect()}
        />
      </main>
    );
  }

  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 (
    <main className="h-full w-full overflow-hidden bg-black">
      <RetroOffice3D
        agents={officeAgents}
        animationState={officeAnimationState}
        deskAssignmentByDeskUid={deskAssignmentByDeskUid}
        githubReviewAgentId={githubReviewAgentId}
        qaTestingAgentId={qaTestingAgentId}
        phoneBoothAgentId={activePhoneBoothAgentId}
        phoneCallScenario={activePhoneCallScenario}
        smsBoothAgentId={activeSmsBoothAgentId}
        textMessageScenario={activeTextMessageScenario}
        monitorAgentId={monitorAgentId}
        monitorByAgentId={monitorByAgentId}
        githubSkill={githubSkill}
        officeTitle={officeTitle}
        officeTitleLoaded={officeTitleLoaded}
        voiceRepliesEnabled={voiceRepliesEnabled}
        voiceRepliesVoiceId={voiceRepliesVoiceId}
        voiceRepliesSpeed={voiceRepliesSpeed}
        voiceRepliesLoaded={voiceRepliesLoaded}
        onOfficeTitleChange={setOfficeTitle}
        onVoiceRepliesToggle={setVoiceRepliesEnabled}
        onVoiceRepliesVoiceChange={setVoiceRepliesVoiceId}
        onVoiceRepliesSpeedChange={setVoiceRepliesSpeed}
        onVoiceRepliesPreview={(voiceId, voiceName) => {
          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 ? (
        <div className="pointer-events-none fixed left-1/2 top-16 z-40 w-full max-w-xl -translate-x-1/2 px-4">
          <div className="pointer-events-auto rounded-lg border border-amber-400/35 bg-black/80 px-4 py-3 shadow-2xl backdrop-blur">
            <div className="flex items-center justify-between gap-3">
              <div>
                <p className="font-mono text-[10px] uppercase tracking-[0.16em] text-amber-200/80">
                  Office fleet status
                </p>
                <p className="mt-1 text-sm text-amber-50">{emptyFleetMessage}</p>
              </div>
              <button
                type="button"
                className="ui-btn-secondary shrink-0 px-3 py-2 text-xs font-semibold tracking-[0.05em] text-foreground"
                onClick={() => {
                  void loadAgents({ forceSettings: true });
                }}
              >
                Retry
              </button>
            </div>
          </div>
        </div>
      ) : null}

      {!debugEnabled ? (
        <HQSidebar
          open={sidebarOpen}
          activeTab={activeSidebarTab}
          inboxCount={unseenInboxCount}
          onToggle={() => setSidebarOpen((prev) => !prev)}
          onTabChange={setActiveSidebarTab}
          inboxPanel={
            <InboxPanel
              agents={state.agents}
              onSelectAgent={(agentId) => {
                handleOpenAgentChat(agentId);
                setActiveSidebarTab("inbox");
              }}
            />
          }
          historyPanel={
            <HistoryPanel
              runs={runLog}
              agents={state.agents}
              onSelectAgent={(agentId) => {
                handleOpenAgentChat(agentId);
                setActiveSidebarTab("history");
              }}
            />
          }
          playbooksPanel={
            <PlaybooksPanel
              client={client}
              status={status}
              agents={state.agents}
              standup={standupController}
            />
          }
          marketplacePanel={
            <SkillsMarketplacePanel
              marketplace={marketplace}
              onSelectAgent={handleOpenAgentChat}
              onOpenAgentSettings={(agentId) => {
                handleOpenAgentChat(agentId);
                router.push("/office");
              }}
            />
          }
          analyticsPanel={
            <AnalyticsPanel
              client={client}
              status={status}
              agents={state.agents}
              runLog={runLog}
              gatewayUrl={gatewayUrl}
              settingsCoordinator={settingsCoordinator}
              onSelectAgent={(agentId) => {
                handleOpenAgentChat(agentId);
                setActiveSidebarTab("analytics");
              }}
            />
          }
        />
      ) : null}

      {showOpenClawConsole ? (
        <section className="pointer-events-auto fixed bottom-3 left-3 z-30 flex w-[520px] max-w-[calc(100vw-1.5rem)] flex-col overflow-hidden rounded border border-cyan-500/25 bg-black/78 shadow-2xl backdrop-blur">
          <div className="flex items-center justify-between border-b border-cyan-500/15 px-3 py-2 font-mono text-[11px] uppercase tracking-[0.18em] text-cyan-200/80">
            <span>OpenClaw Event Console</span>
            <div className="flex items-center gap-2">
              <span className="text-[10px] text-cyan-100/45">
                agents {state.agents.length} | events{" "}
                {filteredOpenClawLogEntries.length}/{openClawLogEntries.length}
              </span>
              <button
                type="button"
                onClick={() => {
                  void handleCopyOpenClawConsoleJson();
                }}
                className="rounded border border-cyan-500/20 px-2 py-0.5 text-[9px] text-cyan-100/70 transition-colors hover:border-cyan-400/45 hover:text-cyan-50"
              >
                {openClawConsoleCopyStatus === "copied"
                  ? "Copied"
                  : openClawConsoleCopyStatus === "error"
                    ? "Copy Failed"
                    : "Copy JSON"}
              </button>
              <button
                type="button"
                onClick={handleDownloadOpenClawConsoleJson}
                className="rounded border border-cyan-500/20 px-2 py-0.5 text-[9px] text-cyan-100/70 transition-colors hover:border-cyan-400/45 hover:text-cyan-50"
              >
                Download JSON
              </button>
              <button
                type="button"
                onClick={handleClearOpenClawConsole}
                className="rounded border border-cyan-500/20 px-2 py-0.5 text-[9px] text-cyan-100/70 transition-colors hover:border-cyan-400/45 hover:text-cyan-50"
              >
                Clear
              </button>
              <button
                type="button"
                onClick={() =>
                  setOpenClawConsoleCollapsed((previous) => !previous)
                }
                className="rounded border border-cyan-500/20 px-2 py-0.5 text-[9px] text-cyan-100/70 transition-colors hover:border-cyan-400/45 hover:text-cyan-50"
              >
                {openClawConsoleCollapsed ? "Expand" : "Minimize"}
              </button>
            </div>
          </div>
          {!openClawConsoleCollapsed ? (
            <div className="flex h-[320px] flex-col gap-3 overflow-y-auto bg-[#02090b]/96 px-3 py-2 font-mono text-[10px] leading-4">
            <div className="rounded border border-cyan-500/10 bg-cyan-950/10 p-2">
              <div className="flex items-center gap-2">
                <input
                  type="text"
                  value={openClawConsoleSearch}
                  onChange={(event) =>
                    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 ? (
                  <button
                    type="button"
                    onClick={() => setOpenClawConsoleSearch("")}
                    className="rounded border border-cyan-500/20 px-2 py-1 text-[9px] uppercase tracking-[0.16em] text-cyan-100/70 transition-colors hover:border-cyan-400/45 hover:text-cyan-50"
                  >
                    Reset
                  </button>
                ) : null}
              </div>
            </div>
            {openClawLiveStateMatchesSearch ? (
              <div className="rounded border border-cyan-500/10 bg-cyan-950/10 p-2">
                <div className="mb-1 text-[9px] uppercase tracking-[0.16em] text-cyan-300/70">
                  Live OpenClaw State
                </div>
                <pre className="whitespace-pre-wrap break-words text-cyan-100/80">
                  {renderOpenClawHighlightedText(
                    openClawLiveStateText,
                    openClawConsoleSearch,
                  )}
                </pre>
              </div>
            ) : (
              <div className="rounded border border-cyan-500/10 bg-cyan-950/10 p-2 text-cyan-100/45">
                Live OpenClaw state does not match the current search.
              </div>
            )}
            <div className="text-[9px] uppercase tracking-[0.16em] text-cyan-300/70">
              Raw OpenClaw Gateway Events
            </div>
            {filteredOpenClawLogEntries.length === 0 ? (
              <div className="rounded border border-cyan-500/10 bg-cyan-950/10 p-2 text-cyan-100/45">
                {openClawLogEntries.length === 0
                  ? "No OpenClaw gateway events received yet."
                  : "No OpenClaw events match the current search."}
              </div>
            ) : (
              filteredOpenClawLogEntries.map((entry) => {
                const isUserMessage = entry.role === "user";
                return (
                  <div
                    key={entry.id}
                    className={`rounded border p-2 ${
                      isUserMessage
                        ? "border-amber-400/30 bg-amber-950/12"
                        : "border-cyan-500/12 bg-cyan-950/8"
                    }`}
                  >
                    <div className="flex items-center justify-between gap-3">
                      <div
                        className={`text-[9px] uppercase tracking-[0.16em] ${
                          isUserMessage
                            ? "text-amber-300/85"
                            : "text-cyan-300/75"
                        }`}
                      >
                        {renderOpenClawHighlightedText(
                          `[${entry.timestamp}] ${entry.eventName} / ${entry.eventKind}`,
                          openClawConsoleSearch,
                        )}
                      </div>
                      {entry.role ? (
                        <span
                          className={`rounded px-1.5 py-0.5 text-[9px] uppercase ${
                            isUserMessage
                              ? "bg-amber-400/15 text-amber-200"
                              : "bg-cyan-400/10 text-cyan-200/80"
                          }`}
                        >
                          {entry.role}
                        </span>
                      ) : null}
                    </div>
                    <div className="mt-1 whitespace-pre-wrap break-words text-cyan-100/55">
                      {renderOpenClawHighlightedText(
                        entry.summary,
                        openClawConsoleSearch,
                      )}
                    </div>
                    {entry.messageText ? (
                      <div className="mt-2 rounded border border-amber-400/20 bg-amber-950/25 px-2 py-1 text-amber-100">
                        <div className="text-[9px] uppercase tracking-[0.16em] text-amber-300/75">
                          User / Message Text
                        </div>
                        <div className="mt-1 whitespace-pre-wrap break-words">
                          {renderOpenClawHighlightedText(
                            entry.messageText,
                            openClawConsoleSearch,
                          )}
                        </div>
                      </div>
                    ) : null}
                    {entry.thinkingText ? (
                      <div className="mt-2 rounded border border-fuchsia-400/15 bg-fuchsia-950/15 px-2 py-1 text-fuchsia-100/90">
                        <div className="text-[9px] uppercase tracking-[0.16em] text-fuchsia-300/70">
                          Thinking
                        </div>
                        <div className="mt-1 whitespace-pre-wrap break-words">
                          {renderOpenClawHighlightedText(
                            entry.thinkingText,
                            openClawConsoleSearch,
                          )}
                        </div>
                      </div>
                    ) : null}
                    {entry.streamText ? (
                      <div className="mt-2 rounded border border-cyan-400/15 bg-cyan-950/18 px-2 py-1 text-cyan-50/90">
                        <div className="text-[9px] uppercase tracking-[0.16em] text-cyan-300/70">
                          Stream
                        </div>
                        <div className="mt-1 whitespace-pre-wrap break-words">
                          {renderOpenClawHighlightedText(
                            entry.streamText,
                            openClawConsoleSearch,
                          )}
                        </div>
                      </div>
                    ) : null}
                    {entry.toolText ? (
                      <div className="mt-2 rounded border border-violet-400/15 bg-violet-950/15 px-2 py-1 text-violet-100/90">
                        <div className="text-[9px] uppercase tracking-[0.16em] text-violet-300/70">
                          Tool Output
                        </div>
                        <div className="mt-1 whitespace-pre-wrap break-words">
                          {renderOpenClawHighlightedText(
                            entry.toolText,
                            openClawConsoleSearch,
                          )}
                        </div>
                      </div>
                    ) : null}
                    <details className="mt-2">
                      <summary className="cursor-pointer text-[9px] uppercase tracking-[0.16em] text-cyan-300/55">
                        Raw Payload
                      </summary>
                      <pre className="mt-1 whitespace-pre-wrap break-words text-cyan-100/45">
                        {renderOpenClawHighlightedText(
                          entry.payloadText,
                          openClawConsoleSearch,
                        )}
                      </pre>
                    </details>
                  </div>
                );
              })
            )}
            </div>
          ) : null}
        </section>
      ) : null}

      <div
        className={`fixed bottom-3 z-30 flex flex-col items-end gap-2 ${sidebarOpen ? "right-84" : "right-3"} ${
          debugEnabled ? "hidden" : ""
        }`}
      >
        {chatOpen && (
          <div
            className="flex overflow-hidden rounded border border-white/10 bg-[#0e0a04] shadow-2xl"
            style={{ width: 560, height: 520 }}
          >
            <div className="flex w-44 shrink-0 flex-col border-r border-white/10">
              <div className="flex items-center justify-between border-b border-white/10 px-3 py-2">
                <span className="font-mono text-[11px] font-semibold uppercase tracking-widest text-white/60">
                  Agents
                </span>
                <span className="font-mono text-[10px] text-white/40">
                  {state.agents.length}
                </span>
              </div>
              <div className="flex-1 overflow-y-auto">
                {state.agents.length === 0 ? (
                  <div className="px-3 py-4 font-mono text-[11px] text-white/30">
                    No agents.
                  </div>
                ) : (
                  state.agents.map((agent) => {
                    const isSelected = agent.agentId === selectedChatAgentId;
                    const isRunning = agent.status === "running";
                    return (
                      <button
                        key={agent.agentId}
                        type="button"
                        onClick={() => handleOpenAgentChat(agent.agentId)}
                        className={`flex w-full items-center gap-2 px-3 py-2.5 text-left transition-colors ${
                          isSelected
                            ? "bg-white/10 text-white"
                            : "text-white/50 hover:bg-white/5 hover:text-white/80"
                        }`}
                      >
                        <span
                          className={`h-1.5 w-1.5 shrink-0 rounded-full ${isRunning ? "bg-emerald-400" : "bg-white/20"}`}
                        />
                        <span className="min-w-0 flex-1 truncate font-mono text-[11px]">
                          {agent.name || agent.agentId}
                        </span>
                      </button>
                    );
                  })
                )}
              </div>
            </div>

            <div className="flex min-w-0 flex-1 flex-col">
              {focusedChatAgent ? (
                <AgentChatPanel
                  agent={focusedChatAgent}
                  isSelected={false}
                  canSend={status === "connected"}
                  models={gatewayModels}
                  stopBusy={
                    chatController.stopBusyAgentId === focusedChatAgent.agentId
                  }
                  onLoadMoreHistory={() => {}}
                  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}
                />
              ) : (
                <div className="flex flex-1 items-center justify-center font-mono text-[12px] text-white/30">
                  Select an agent to chat.
                </div>
              )}
            </div>
          </div>
        )}

        <button
          type="button"
          onClick={() => setChatOpen((prev) => !prev)}
          className="flex items-center gap-1.5 rounded border border-amber-700/50 bg-[#0e0a04]/90 px-3 py-1.5 font-mono text-[11px] font-medium tracking-wider text-amber-500/80 shadow-lg backdrop-blur transition-colors hover:border-amber-600/70 hover:text-amber-400"
        >
          {chatOpen ? (
            <>
              <ChevronDown className="h-3.5 w-3.5" />
              <span>HIDE CHAT</span>
            </>
          ) : (
            <>
              <MessageSquare className="h-3.5 w-3.5" />
              <span>CHAT</span>
              {runningCount > 0 ? (
                <span className="rounded bg-amber-500/20 px-1 text-[10px] text-amber-400">
                  {runningCount}
                </span>
              ) : null}
            </>
          )}
        </button>
      </div>

      {mainVoiceState !== "idle" || mainVoiceError ? (
        <div className="pointer-events-none fixed inset-x-0 bottom-6 z-40 flex justify-center">
          <div
            className={`flex min-w-[220px] items-center gap-3 rounded-full border px-4 py-3 font-mono text-[12px] shadow-2xl backdrop-blur ${
              mainVoiceError
                ? "border-red-500/45 bg-red-950/75 text-red-100"
                : "border-cyan-400/35 bg-black/70 text-white"
            }`}
          >
            <div
              className={`flex h-10 w-10 items-center justify-center rounded-full ${
                mainVoiceState === "recording"
                  ? "bg-red-500/25 text-red-200"
                  : mainVoiceState === "transcribing"
                    ? "bg-cyan-400/20 text-cyan-100"
                    : "bg-white/10 text-white"
              }`}
            >
              <Mic className="h-5 w-5" />
            </div>
            <div className="flex flex-col">
              <span className="text-[10px] uppercase tracking-[0.18em] text-white/55">
                Main agent
              </span>
              <span className="text-[12px] font-medium text-white">
                {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."}
              </span>
            </div>
          </div>
        </div>
      ) : null}

      {debugEnabled ? (
        <section className="fixed bottom-3 right-3 z-50 max-h-[45vh] w-[560px] overflow-auto rounded border border-slate-700 bg-black/90 p-3 font-mono text-[11px] text-slate-100">
          <div className="mb-2 font-semibold text-cyan-300">office debug</div>
          <div className="mb-2 text-slate-400">
            status: {status} | agents: {state.agents.length}
          </div>
          {debugRows.length === 0 ? (
            <div className="text-slate-500">No debug data yet.</div>
          ) : (
            <div className="space-y-2">
              {debugRows.map((row) => (
                <div
                  key={row.agentId}
                  className="rounded border border-slate-800 p-2"
                >
                  <div className="text-cyan-200">
                    {row.name} ({row.agentId})
                  </div>
                  <div>
                    storeStatus={row.storeStatus} runId={row.runId ?? "null"}{" "}
                    inferredRunning=
                    {String(row.inferredRunning)}
                  </div>
                  <div>
                    lastRole={row.lastRole} messages={row.messageCount}
                  </div>
                  <div className="truncate text-slate-400">
                    detectedSession={row.detectedSessionKey || "-"}
                  </div>
                  <div className="truncate text-slate-400">
                    lastText={row.lastText || "-"}
                  </div>
                  <div className="truncate text-slate-500">
                    sessions={row.inspectedSessions || "-"}
                  </div>
                  <div className="text-slate-500">
                    source={row.inferenceSource}
                  </div>
                  <div className="text-slate-500">at={row.at}</div>
                </div>
              ))}
            </div>
          )}
        </section>
      ) : null}
      {agentEditorAgent ? (
        <AgentEditorModal
          open
          client={client}
          agents={state.agents}
          agent={agentEditorAgent}
          initialSection={agentEditorInitialSection}
          onClose={() => {
            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}
    </main>
  );
}
