{% extends "base.html" %}
{% block title %}DarkMatter{% endblock %}
{% block content %}

<div class="flex h-[calc(100vh-48px)] relative">

  <!-- ================================================================== -->
  <!-- SIDEBAR — 240px                                                     -->
  <!-- ================================================================== -->
  <div id="sidebar" class="bg-base border-r border-border/50 flex flex-col shrink-0 overflow-hidden hidden md:flex z-20 transition-[width] duration-200 ease-out" style="width:240px">

    <!-- Identity card -->
    <div class="px-4 py-3 border-b border-border/30">
      <div class="flex items-center gap-2.5">
        <div class="w-9 h-9 rounded-full bg-accent-blue/15 flex items-center justify-center shrink-0">
          <span class="text-[15px] font-semibold" style="color: var(--accent-blue)" id="sidebar-avatar">H</span>
        </div>
        <div class="min-w-0">
          <div class="text-[14px] font-semibold text-primary truncate" id="sidebar-name">{{ state.display_name or 'Human' }}</div>
          <div class="text-[11px] text-muted truncate" id="sidebar-agent-id">{{ short_id(state.agent_id) }}</div>
        </div>
      </div>
    </div>

    <!-- Connect form -->
    <div class="px-3 py-2.5 border-b border-border/30">
      <form id="connect-form" class="flex gap-1.5">
        <input type="text" name="url" placeholder="http://host:port"
          class="flex-1 bg-surface border border-border/50 rounded-lg px-2.5 py-1.5 text-[13px] text-primary placeholder-muted focus:border-accent-blue/50 transition-colors min-w-0">
        <button type="submit"
          class="text-white text-[13px] font-semibold px-3 py-1.5 rounded-lg transition-colors shrink-0"
          style="background: var(--accent-blue)">
          +
        </button>
      </form>
    </div>

    <!-- Discovered agents -->
    <div id="discovered-section" class="border-b border-border/30">
      <div class="px-3 py-2 flex items-center justify-between">
        <span class="text-[11px] text-muted font-semibold uppercase tracking-wider">Discovered</span>
        <button onclick="fetch('/api/scan',{method:'POST'}).then(()=>pollServer())"
          class="text-muted hover:text-primary text-[11px] transition-colors">Scan</button>
      </div>
      <div id="discovered-list" class="px-3 pb-2 space-y-1">
        <p class="text-[12px] text-muted">Scanning...</p>
      </div>
    </div>

    <!-- Pending requests -->
    <div id="pending-section" class="hidden border-b border-border/30">
      <div class="px-3 py-2 flex items-center gap-2">
        <div class="w-1.5 h-1.5 rounded-full bg-accent-purple"></div>
        <span class="text-[11px] text-muted font-semibold uppercase tracking-wider">Pending</span>
      </div>
      <div id="pending-list" class="px-3 pb-2 space-y-1"></div>
    </div>

    <!-- Connections list -->
    <div class="border-b border-border/30">
      <div class="px-3 py-2 flex items-center justify-between">
        <span class="text-[11px] text-muted font-semibold uppercase tracking-wider">Connections</span>
        <div class="flex items-center gap-2">
          <button id="checkin-btn" onclick="checkinAll()" title="Ask all agents for a status update"
            class="text-[10px] px-2 py-0.5 rounded-md font-semibold transition-colors"
            style="background: var(--accent-green); color: white; opacity: 0.85;"
            onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.85'">
            Check In
          </button>
          <button id="update-all-btn" onclick="updateAllAgents()" title="Git pull &amp; restart all LAN agents"
            class="text-[10px] px-2 py-0.5 rounded-md font-semibold transition-colors"
            style="background: var(--accent-blue); color: white; opacity: 0.85;"
            onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.85'">
            Update All
          </button>
          <span class="text-[11px] text-muted" id="sidebar-conn-count">0</span>
        </div>
      </div>
      <div id="connections-list" class="px-3 pb-2 space-y-0.5 max-h-48 overflow-y-auto">
        <p class="text-[12px] text-muted">No connections</p>
      </div>
    </div>

    <!-- QR Code -->
    <div id="qr-panel" class="border-b border-border/30">
      <button onclick="toggleQR()" class="w-full px-3 py-2 flex items-center justify-between hover:bg-surface/50 transition-colors">
        <span class="text-[11px] text-muted font-semibold uppercase tracking-wider">LAN Access</span>
        <svg id="qr-chevron" class="text-muted transition-transform" width="10" height="10" viewBox="0 0 10 10"><path d="M3 4l2 2 2-2" stroke="currentColor" stroke-width="1.2" fill="none"/></svg>
      </button>
      <div id="qr-content" class="px-3 pb-3 hidden">
        <div id="qr-code" class="flex justify-center mb-2"></div>
        <div id="qr-url" class="text-[11px] text-muted text-center break-all select-all"></div>
        <div id="qr-unavailable" class="text-[11px] text-muted text-center hidden">LAN not available</div>
      </div>
    </div>

    <!-- Security settings -->
    <div id="security-section" class="border-b border-border/30">
      <button onclick="toggleSecurityPanel()" class="w-full px-3 py-2 flex items-center justify-between hover:bg-surface/50 transition-colors">
        <span class="flex items-center gap-1.5">
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-muted"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
          <span class="text-[11px] text-muted font-semibold uppercase tracking-wider">Security</span>
        </span>
        <svg id="security-chevron" class="text-muted transition-transform" width="10" height="10" viewBox="0 0 10 10"><path d="M3 4l2 2 2-2" stroke="currentColor" stroke-width="1.2" fill="none"/></svg>
      </button>
      <div id="security-panel" class="px-3 pb-3 hidden space-y-3">
        <!-- PIN -->
        <div>
          <div class="flex items-center justify-between mb-1.5">
            <span class="text-[11px] text-muted font-semibold">PIN Lock</span>
            <span id="pin-status-badge" class="text-[10px] font-semibold px-1.5 py-0.5 rounded-md bg-surface text-muted">OFF</span>
          </div>
          <div class="flex gap-1.5">
            <input type="password" id="pin-set-input" placeholder="Enter PIN" maxlength="32"
              class="flex-1 bg-surface border border-border/50 rounded-lg px-2.5 py-1.5 text-[13px] text-primary placeholder-muted text-center tracking-widest font-semibold focus:border-accent-blue/50 transition-colors min-w-0">
            <button id="pin-set-btn" onclick="setPin()"
              class="text-white text-[11px] font-semibold px-3 py-1.5 rounded-lg transition-colors shrink-0"
              style="background: var(--accent-blue)">Set</button>
            <button id="pin-clear-btn" onclick="clearPin()"
              class="hidden text-[11px] font-semibold px-3 py-1.5 rounded-lg transition-colors shrink-0 border border-border/30 text-muted hover:text-primary">Clear</button>
          </div>
        </div>
        <!-- Toggles -->
        <label class="flex items-center justify-between cursor-pointer">
          <span class="text-[11px] text-muted">Auto-accept local agents</span>
          <input type="checkbox" id="sec-auto-accept" onchange="updateSecurityToggle('auto_accept_local', this.checked)" class="accent-[var(--accent-blue)] w-3.5 h-3.5">
        </label>
        <label class="flex items-center justify-between cursor-pointer">
          <span class="text-[11px] text-muted">Sandbox spawned agents</span>
          <input type="checkbox" id="sec-sandbox" onchange="updateSecurityToggle('sandbox_enabled', this.checked)" class="accent-[var(--accent-blue)] w-3.5 h-3.5">
        </label>
        <label id="sec-sandbox-net-row" class="flex items-center justify-between cursor-pointer hidden">
          <span class="text-[11px] text-muted pl-3">Allow network in sandbox</span>
          <input type="checkbox" id="sec-sandbox-net" onchange="updateSecurityToggle('sandbox_network', this.checked)" class="accent-[var(--accent-blue)] w-3.5 h-3.5">
        </label>
        <!-- Push to LAN -->
        <button onclick="pushSecurityToLAN()" class="w-full text-[11px] font-semibold py-1.5 rounded-lg border border-border/30 text-muted hover:text-primary transition-colors">
          Push to LAN agents
        </button>
      </div>
    </div>

    <!-- NAT status footer -->
    <div id="nat-status" class="px-3 py-2 mt-auto hidden">
      <div class="flex items-center gap-1.5">
        <div class="w-1.5 h-1.5 rounded-full bg-accent-orange"></div>
        <span class="text-[11px] text-muted">NAT detected — using relay</span>
      </div>
    </div>
  </div>

  <!-- ================================================================== -->
  <!-- MAIN CHAT AREA                                                      -->
  <!-- ================================================================== -->
  <div id="chat-area" class="flex-1 flex flex-col bg-surface overflow-hidden relative">

    <!-- Sidebar toggle (desktop) -->
    <button id="sidebar-toggle" onclick="toggleSidebar()" class="hidden md:flex absolute top-2 left-2 z-30 w-7 h-7 items-center justify-center rounded-lg text-muted/50 hover:text-muted hover:bg-card/40 transition-all" title="Toggle sidebar">
      <svg id="sidebar-toggle-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>
    </button>

    <!-- Network context banner (collapsible) -->
    <div id="context-banner" class="border-b border-border/30 bg-base/60 backdrop-blur-sm hidden">
      <button onclick="toggleContextPanel()" class="w-full px-5 py-2.5 flex items-center justify-between text-[12px] text-muted hover:text-label transition-colors">
        <span class="flex items-center gap-2">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
          <span id="context-summary" class="font-medium">Network context</span>
        </span>
        <svg id="context-chevron" class="transition-transform duration-200" width="10" height="10" viewBox="0 0 10 10"><path d="M3 4l2 2 2-2" stroke="currentColor" stroke-width="1.2" fill="none"/></svg>
      </button>
      <div id="context-panel" class="hidden px-5 pb-4">
        <div id="context-connections" class="space-y-2"></div>
      </div>
    </div>

    <!-- Message feed -->
    <div id="feed-scroll" class="flex-1 overflow-y-auto">
      <div id="feed-container">
        <!-- Empty state -->
        <div id="empty-state" class="flex flex-col items-center justify-center h-full text-center select-none pt-32 px-4">
          <div class="relative mb-6">
            <div class="w-20 h-20 rounded-2xl bg-card flex items-center justify-center" style="box-shadow: 0 0 40px rgba(10,132,255,0.08)">
              <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.3" class="text-muted">
                <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
              </svg>
            </div>
            <div class="absolute -bottom-1 -right-1 w-5 h-5 rounded-full bg-accent-green flex items-center justify-center">
              <div class="w-2 h-2 rounded-full bg-white"></div>
            </div>
          </div>
          <p class="text-primary text-lg font-semibold tracking-tight">DarkMatter</p>
          <p class="text-muted text-sm mt-1.5 max-w-xs leading-relaxed">Connect to an agent in the sidebar to start messaging the mesh</p>
        </div>
      </div>
    </div>

    <!-- Drawing canvas -->
    <div id="draw-panel" class="hidden border-t border-border/30 px-4 py-3">
      <div class="flex items-center justify-between mb-2">
        <div class="flex items-center gap-2">
          <span class="text-[12px] font-semibold text-muted">Draw</span>
          <input type="color" id="draw-color" value="#ffffff" class="w-5 h-5 rounded cursor-pointer border-0 p-0 bg-transparent">
          <input type="range" id="draw-size" min="1" max="20" value="3" class="w-16 h-1 accent-[var(--accent-blue)]">
          <button onclick="clearDrawCanvas()" class="text-[10px] text-muted hover:text-primary px-1.5 py-0.5 rounded border border-border/30">Clear</button>
          <button onclick="undoDrawCanvas()" class="text-[10px] text-muted hover:text-primary px-1.5 py-0.5 rounded border border-border/30">Undo</button>
        </div>
        <button onclick="sendDrawing()" class="text-[11px] font-semibold px-3 py-1 rounded-lg text-white" style="background: var(--accent-blue)">Send Drawing</button>
      </div>
      <canvas id="draw-canvas" width="600" height="200" class="w-full rounded-lg cursor-crosshair" style="background: #1a1a2e; touch-action: none;"></canvas>
    </div>

    <!-- Compose bar -->
    <div id="compose-bar" class="px-4 sm:px-6 py-3">
      <input type="file" id="file-input" multiple class="hidden">
      <input type="file" id="camera-input" accept="image/*" capture="environment" class="hidden">
      <form id="compose-form">
        <div class="compose-ring bg-card/30 relative" id="compose-drop-zone">
          <!-- Attachment chips -->
          <div id="attachment-chips" class="hidden flex flex-wrap gap-1.5 px-4 pt-3 pb-1"></div>
          <!-- Textarea -->
          <textarea name="content" id="compose-input" rows="1"
            placeholder="Send a message..."
            class="w-full bg-transparent text-[15px] text-primary placeholder-muted/50 resize-none leading-relaxed px-4 pt-3 pb-12 border-0 focus:ring-0"
            style="max-height:200px;outline:none;min-height:52px"
            oninput="autoGrow(this)"></textarea>
          <!-- Drop overlay -->
          <div id="drop-overlay" class="hidden absolute inset-0 bg-accent-blue/10 border-2 border-dashed border-accent-blue/40 rounded-xl flex items-center justify-center z-10 pointer-events-none">
            <span class="text-[13px] font-semibold text-accent-blue">Drop files here</span>
          </div>
          <!-- Bottom toolbar inside textarea -->
          <div class="absolute bottom-0 left-0 right-0 flex items-center justify-between px-3 pb-2.5">
            <div class="flex items-center gap-1.5">
              <!-- Route selector -->
              <div class="relative" id="target-dropdown-wrap">
                <button type="button" id="target-dropdown-btn" onclick="toggleDropdown()"
                  class="flex items-center gap-1.5 text-[11px] text-muted hover:text-primary rounded-lg px-2 py-1 transition-all hover:bg-surface/50">
                  <div id="route-dot" class="w-1.5 h-1.5 rounded-full bg-accent-green shrink-0"></div>
                  <span id="target-label" class="font-medium">auto</span>
                  <svg width="7" height="7" viewBox="0 0 8 8" class="text-muted/60"><path d="M2 3l2 2 2-2" stroke="currentColor" stroke-width="1" fill="none"/></svg>
                </button>
                <div id="target-dropdown-menu" class="dropdown-menu absolute bottom-full left-0 mb-1.5 w-56 bg-card/95 backdrop-blur-xl border border-border/30 rounded-xl z-30 py-1 max-h-48 overflow-y-auto" style="box-shadow: 0 8px 32px rgba(0,0,0,0.2)"></div>
              </div>
              <!-- Attach button -->
              <button type="button" onclick="document.getElementById('file-input').click()" title="Attach files"
                class="text-muted/40 hover:text-muted transition-colors rounded-lg px-2 py-1 hover:bg-surface/50">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
              </button>
              <!-- Camera button -->
              <button type="button" onclick="document.getElementById('camera-input').click()" title="Take photo"
                class="text-muted/40 hover:text-muted transition-colors rounded-lg px-2 py-1 hover:bg-surface/50">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
              </button>
              <!-- Draw button -->
              <button type="button" onclick="toggleDrawCanvas()" title="Draw a sketch" id="draw-toggle-btn"
                class="text-muted/40 hover:text-muted transition-colors rounded-lg px-2 py-1 hover:bg-surface/50">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>
              </button>
              <!-- Clear button -->
              <button type="button" onclick="clearFeed()" title="Clear messages"
                class="text-muted/40 hover:text-muted transition-colors rounded-lg px-2 py-1 hover:bg-surface/50">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              </button>
            </div>
            <!-- Send button -->
            <button type="submit" id="send-btn"
              class="rounded-lg text-white flex items-center justify-center transition-all disabled:opacity-20 px-3 py-1.5 text-[12px] font-semibold gap-1.5"
              style="background: var(--accent-blue)"
              title="Send">
              <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
              Send
            </button>
          </div>
        </div>
        <input type="hidden" name="target" id="target-value" value="auto">
      </form>
    </div>
  </div>

  <!-- ================================================================== -->
  <!-- MESH VIEW                                                            -->
  <!-- ================================================================== -->
  <div id="mesh-view" class="flex-1 flex flex-col bg-surface overflow-hidden relative hidden">
    <div class="px-4 py-3 border-b border-border/30 flex items-center justify-between">
      <div class="flex items-center gap-2">
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="text-accent-blue">
          <circle cx="12" cy="5" r="2"/><circle cx="5" cy="19" r="2"/><circle cx="19" cy="19" r="2"/><line x1="12" y1="7" x2="5" y2="17"/><line x1="12" y1="7" x2="19" y2="17"/><line x1="7" y1="19" x2="17" y2="19"/>
        </svg>
        <span class="text-[14px] font-semibold text-primary">Mesh Topology</span>
        <span id="mesh-node-count" class="text-[11px] text-muted"></span>
      </div>
      <button onclick="showChatView()" class="text-muted hover:text-primary text-[13px] font-medium transition-colors">Back to Chat</button>
    </div>
    <div id="mesh-graph" class="flex-1 relative"></div>
    <!-- Mesh tooltip -->
    <div id="mesh-tooltip" class="fixed hidden bg-card/95 backdrop-blur-xl border border-border/50 rounded-xl p-3 z-50 max-w-xs" style="box-shadow: 0 8px 32px rgba(0,0,0,0.3);">
      <div class="text-[13px] font-semibold text-primary" id="tooltip-name"></div>
      <div class="text-[11px] text-muted mt-0.5" id="tooltip-id"></div>
      <div class="text-[12px] text-label mt-1.5" id="tooltip-bio"></div>
      <div class="text-[11px] text-muted mt-1" id="tooltip-level"></div>
      <button id="tooltip-connect-btn" class="hidden mt-2 w-full text-[12px] font-semibold text-white rounded-lg py-1.5 transition-colors" style="background: var(--accent-blue)" onclick="connectFromTooltip()">Connect</button>
    </div>
  </div>

  <!-- ================================================================== -->
  <!-- MOBILE DRAWER                                                       -->
  <!-- ================================================================== -->
  <div id="mobile-drawer-overlay" class="drawer-overlay fixed inset-0 bg-black/40 backdrop-blur-sm z-50 md:hidden" onclick="toggleMobileDrawer()">
    <div class="drawer-panel absolute left-0 top-0 bottom-0 w-72 bg-base flex flex-col overflow-hidden" onclick="event.stopPropagation()">
      <div class="px-4 py-3 border-b border-border/30 flex items-center justify-between">
        <span class="text-[14px] font-semibold text-primary">DarkMatter</span>
        <button onclick="toggleMobileDrawer()" class="text-muted hover:text-primary text-lg">&times;</button>
      </div>
      <div class="flex-1 overflow-y-auto px-3 py-2 space-y-1" id="mobile-connections-list"></div>
    </div>
  </div>

</div>

<!-- ================================================================== -->
<!-- WALLET MODAL                                                        -->
<!-- ================================================================== -->
<div id="wallet-modal" class="fixed inset-0 bg-black/40 backdrop-blur-sm z-[60] hidden flex items-center justify-center" onclick="if(event.target===this)closeWalletModal()">
  <div class="bg-base border border-border/50 rounded-2xl shadow-2xl w-full max-w-md mx-4 overflow-hidden">
    <div class="px-5 pt-5 pb-3 border-b border-border/30 flex items-center justify-between">
      <div class="flex items-center gap-2.5">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" style="color: var(--accent-blue)">
          <rect x="2" y="6" width="20" height="14" rx="2"/><path d="M2 10h20"/><circle cx="18" cy="15" r="1"/>
        </svg>
        <span class="text-[15px] font-semibold text-primary">Wallet</span>
      </div>
      <button onclick="closeWalletModal()" class="text-muted hover:text-primary transition-colors text-xl leading-none">&times;</button>
    </div>

    <!-- Tabs -->
    <div class="flex border-b border-border/30">
      <button id="wallet-main-tab-balances" onclick="walletMainTab('balances')"
        class="flex-1 text-[12px] font-semibold py-2.5 transition-colors text-primary border-b-2" style="border-color: var(--accent-blue)">
        Balances
      </button>
      <button id="wallet-main-tab-send" onclick="walletMainTab('send')"
        class="flex-1 text-[12px] font-semibold py-2.5 transition-colors text-muted hover:text-primary border-b-2 border-transparent">
        Send
      </button>
      <button id="wallet-main-tab-identity" onclick="walletMainTab('identity')"
        class="flex-1 text-[12px] font-semibold py-2.5 transition-colors text-muted hover:text-primary border-b-2 border-transparent">
        Identity
      </button>
    </div>

    <!-- Balances panel -->
    <div id="wallet-panel-balances" class="px-5 py-4 space-y-3 max-h-[60vh] overflow-y-auto">
      <div class="flex items-center gap-2">
        <span class="text-[11px] text-muted uppercase tracking-wider">Solana</span>
        <span id="wallet-sol-addr" class="text-[11px] text-label font-mono flex-1 truncate"></span>
        <button onclick="copyWalletAddr()" class="text-muted hover:text-primary transition-colors" title="Copy">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
        </button>
      </div>
      <div id="wallet-no-solana" class="hidden text-[13px] text-muted py-4 text-center">No Solana wallet available</div>
      <div id="wallet-balances-list" class="space-y-1.5">
        <div class="flex items-center justify-between px-3 py-2.5 bg-surface rounded-xl">
          <span class="text-[13px] text-primary font-medium">SOL</span>
          <span id="wallet-sol-balance" class="text-[13px] text-label font-mono">--</span>
        </div>
      </div>
      <div id="wallet-token-balances" class="space-y-1"></div>
      <button onclick="refreshWalletBalances()" id="wallet-refresh-btn"
        class="w-full text-[12px] text-muted hover:text-primary py-1.5 transition-colors flex items-center justify-center gap-1">
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
        Refresh
      </button>
    </div>

    <!-- Send panel -->
    <div id="wallet-panel-send" class="px-5 py-4 space-y-3 hidden">
      <div>
        <label class="text-[11px] text-muted uppercase tracking-wider mb-1 block">Recipient</label>
        <select id="wallet-send-recipient" class="w-full bg-surface border border-border/50 rounded-xl px-3 py-2.5 text-[13px] text-primary focus:border-accent-blue/50 transition-colors">
          <option value="">Select connected agent...</option>
        </select>
      </div>
      <div class="flex gap-2">
        <div class="flex-1">
          <label class="text-[11px] text-muted uppercase tracking-wider mb-1 block">Amount</label>
          <input type="number" id="wallet-send-amount" step="any" min="0" placeholder="0.0"
            class="w-full bg-surface border border-border/50 rounded-xl px-3 py-2.5 text-[13px] text-primary placeholder-muted focus:border-accent-blue/50 transition-colors">
        </div>
        <div class="w-28">
          <label class="text-[11px] text-muted uppercase tracking-wider mb-1 block">Token</label>
          <select id="wallet-send-token" class="w-full bg-surface border border-border/50 rounded-xl px-3 py-2.5 text-[13px] text-primary focus:border-accent-blue/50 transition-colors">
            <option value="SOL">SOL</option>
          </select>
        </div>
      </div>
      <button onclick="walletSend()" id="wallet-send-btn"
        class="w-full text-white text-[13px] font-semibold px-4 py-2.5 rounded-xl transition-colors"
        style="background: var(--accent-blue)">
        Send
      </button>
      <div id="wallet-send-result" class="hidden text-[12px] font-mono bg-surface rounded-xl px-3 py-2 break-all"></div>
    </div>

    <!-- Identity panel -->
    <div id="wallet-panel-identity" class="px-5 py-4 space-y-4 hidden">
      <p class="text-[13px] text-label leading-relaxed">
        Sign a deterministic challenge with your Solana wallet to derive a mesh identity.
      </p>

      <div class="flex border border-border/50 rounded-xl overflow-hidden">
        <button id="wallet-tab-ext" onclick="walletTab('ext')"
          class="flex-1 text-[12px] font-semibold py-2 transition-colors bg-surface text-primary">
          Browser Extension
        </button>
        <button id="wallet-tab-qr" onclick="walletTab('qr')"
          class="flex-1 text-[12px] font-semibold py-2 transition-colors text-muted hover:text-primary">
          Mobile QR
        </button>
      </div>

      <!-- Extension flow -->
      <div id="wallet-ext-panel">
        <div class="bg-accent-orange/10 border border-accent-orange/20 rounded-xl px-3 py-2 mb-3">
          <p class="text-[12px] text-accent-orange">This will change your identity and disconnect existing peers.</p>
        </div>
        <label class="flex items-center gap-2 cursor-pointer mb-3">
          <input type="checkbox" id="wallet-remember" class="w-3.5 h-3.5 rounded accent-blue-500">
          <span class="text-[13px] text-label">Remember on this device</span>
        </label>
        <div class="flex gap-2">
          <button id="wallet-connect-btn" onclick="walletAuth()"
            class="flex-1 text-white text-[13px] font-semibold px-4 py-2.5 rounded-xl transition-colors"
            style="background: var(--accent-blue)">
            Connect Wallet
          </button>
          <button id="wallet-disconnect-btn" onclick="walletDisconnect()"
            class="hidden bg-accent-red/10 hover:bg-accent-red/20 text-accent-red text-[13px] font-semibold px-4 py-2.5 rounded-xl transition-colors border border-accent-red/20">
            Revert
          </button>
        </div>
        <p class="text-[11px] text-muted mt-2">Phantom, Solflare, Backpack, or any Solana wallet extension.</p>
      </div>

      <!-- Mobile QR flow -->
      <div id="wallet-qr-panel" class="hidden">
        <div class="bg-accent-orange/10 border border-accent-orange/20 rounded-xl px-3 py-2 mb-3">
          <p class="text-[12px] text-accent-orange">Scan with your phone camera, then sign in your wallet app.</p>
        </div>
        <div id="wallet-qr-idle">
          <label class="flex items-center gap-2 cursor-pointer mb-3">
            <input type="checkbox" id="wallet-qr-remember" class="w-3.5 h-3.5 rounded accent-blue-500">
            <span class="text-[13px] text-label">Remember on this device</span>
          </label>
          <button onclick="startQRSession()"
            class="w-full text-white text-[13px] font-semibold px-4 py-2.5 rounded-xl transition-colors"
            style="background: var(--accent-blue)">
            Generate QR Code
          </button>
        </div>
        <div id="wallet-qr-active" class="hidden">
          <div id="wallet-qr-code" class="flex justify-center mb-2"></div>
          <p id="wallet-qr-url" class="text-[11px] text-muted text-center break-all select-all mb-2"></p>
          <div class="flex items-center justify-center gap-2">
            <div class="w-1.5 h-1.5 rounded-full bg-accent-blue animate-pulse"></div>
            <span class="text-[12px] text-label">Waiting for signature...</span>
          </div>
        </div>
        <div id="wallet-qr-done" class="hidden text-center py-2">
          <p class="text-[13px] text-accent-green font-medium">Signed! Applying identity...</p>
        </div>
      </div>

      <div id="wallet-status" class="hidden text-[13px] text-accent-green font-mono bg-accent-green/10 border border-accent-green/20 rounded-xl px-3 py-2"></div>
    </div>
  </div>
</div>

<script>
// =========================================================================
// State
// =========================================================================
const pendingSection = document.getElementById('pending-section');
const pendingListEl = document.getElementById('pending-list');
const discoveredList = document.getElementById('discovered-list');
const connectionsList = document.getElementById('connections-list');
const mobileConnectionsList = document.getElementById('mobile-connections-list');
const composeForm = document.getElementById('compose-form');
const composeInput = document.getElementById('compose-input');
const sendBtn = document.getElementById('send-btn');
const connectForm = document.getElementById('connect-form');
const targetValue = document.getElementById('target-value');
const targetLabel = document.getElementById('target-label');
const targetMenu = document.getElementById('target-dropdown-menu');
const feedScroll = document.getElementById('feed-scroll');
const feedContainer = document.getElementById('feed-container');
const emptyState = document.getElementById('empty-state');

const renderedConns = new Map();
const renderedConnsMobile = new Map();
const renderedDiscovered = new Map();
const renderedPending = new Map();

let prevPollData = null;
let selfAgentId = null;
let qrInitialized = false;
let qrExpanded = false;
let _walletData = null;
let _walletQRPollTimer = null;

// Feed timeline: all items merged chronologically
const feedItems = new Map();  // id -> { type, timestamp, ... }
const renderedFeedItems = new Map();  // id -> DOM element
let clearedAtTs = parseInt(sessionStorage.getItem('dm-cleared-at') || '0', 10);  // items older than this are hidden
let prevConnIds = new Set();

// File attachments
let pendingAttachments = [];

// =========================================================================
// Utilities
// =========================================================================
function shortId(id) {
  if (!id) return '?';
  if (id.length > 16) return id.slice(0, 8) + '\u2026' + id.slice(-4);
  return id;
}

function agentLabel(obj) {
  if (!obj) return '?';
  const id = obj.agent_id || obj.from_agent_id;
  if (id && id === selfAgentId) return 'You';
  return (obj.display_name || obj.from_display_name || '').trim()
    || (obj.bio || obj.from_bio || '').trim().slice(0, 30)
    || shortId(id);
}

function timeAgo(iso) {
  if (!iso) return '';
  const s = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
  if (s < 5) return 'now';
  if (s < 60) return s + 's ago';
  if (s < 3600) return Math.floor(s/60) + 'm ago';
  if (s < 86400) return Math.floor(s/3600) + 'h ago';
  return Math.floor(s/86400) + 'd ago';
}

function formatTime(iso) {
  if (!iso) return '';
  const d = new Date(iso);
  return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}

function esc(t) {
  const d = document.createElement('div');
  d.textContent = t;
  return d.innerHTML;
}

function agentColor(agentId) {
  if (!agentId) return '#8e8e93';
  let hash = 0;
  for (let i = 0; i < agentId.length; i++) hash = ((hash << 5) - hash + agentId.charCodeAt(i)) | 0;
  const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9'];
  return colors[Math.abs(hash) % colors.length];
}

// Returns a very subtle rgba background tint from an agent color
function agentTint(agentId) {
  const hex = agentColor(agentId);
  const r = parseInt(hex.slice(1,3), 16);
  const g = parseInt(hex.slice(3,5), 16);
  const b = parseInt(hex.slice(5,7), 16);
  return `rgba(${r},${g},${b},0.04)`;
}

function renderMd(text) {
  if (!text) return '';
  try {
    const html = marked.parse(text, { breaks: true, gfm: true });
    return DOMPurify.sanitize(html);
  } catch (e) {
    return esc(text);
  }
}

function ajaxPost(url, body) {
  return fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
    body: JSON.stringify(body),
  }).then(r => {
    if (!r.ok) return { success: false, error: `Server error (${r.status})` };
    return r.json();
  });
}

function autoGrow(el) {
  el.style.height = 'auto';
  el.style.height = Math.min(el.scrollHeight, 200) + 'px';
}

function formatFileSize(bytes) {
  if (bytes < 1024) return bytes + ' B';
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
  return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}

async function uploadFiles(fileList) {
  if (!fileList.length) return;
  const fd = new FormData();
  for (const f of fileList) fd.append('files', f);
  try {
    const resp = await fetch('/api/upload', { method: 'POST', body: fd });
    const data = await resp.json();
    if (data.success && data.files) {
      pendingAttachments.push(...data.files);
      renderAttachmentChips();
    } else {
      showToast(data.error || 'Upload failed', 'error');
    }
  } catch (err) {
    showToast('Upload failed', 'error');
  }
}

function renderAttachmentChips() {
  const container = document.getElementById('attachment-chips');
  if (!pendingAttachments.length) {
    container.classList.add('hidden');
    container.innerHTML = '';
    return;
  }
  container.classList.remove('hidden');
  container.innerHTML = pendingAttachments.map((a, i) => {
    const isImg = (a.content_type || '').startsWith('image/');
    return `<div class="flex items-center gap-1.5 bg-surface/80 border border-border/30 rounded-lg px-2.5 py-1 text-[12px] text-label">
      ${isImg ? `<img src="${esc(a.url)}" class="w-5 h-5 rounded object-cover">` :
        `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`}
      <span class="truncate max-w-[120px]">${esc(a.filename)}</span>
      <span class="text-muted">${formatFileSize(a.size)}</span>
      <button type="button" onclick="removeAttachment(${i})" class="text-muted hover:text-primary ml-0.5">&times;</button>
    </div>`;
  }).join('');
}

function removeAttachment(idx) {
  pendingAttachments.splice(idx, 1);
  renderAttachmentChips();
}

function renderAttachmentsInMessage(attachments) {
  if (!attachments || !attachments.length) return '';
  return '<div class="flex flex-wrap gap-2 mt-2">' + attachments.map(a => {
    const isImg = (a.content_type || '').startsWith('image/');
    if (isImg) {
      return `<a href="${esc(a.url)}" target="_blank" class="block"><img src="${esc(a.url)}" alt="${esc(a.filename)}" class="rounded-lg max-w-[240px] max-h-[180px] object-cover border border-border/20"></a>`;
    }
    return `<a href="${esc(a.url)}" target="_blank" class="flex items-center gap-1.5 bg-surface/60 border border-border/30 rounded-lg px-2.5 py-1.5 text-[12px] text-label hover:text-primary transition-colors">
      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
      <span>${esc(a.filename)}</span>
      <span class="text-muted">${formatFileSize(a.size)}</span>
    </a>`;
  }).join('') + '</div>';
}

let sidebarCollapsed = false;
function toggleSidebar() {
  const sb = document.getElementById('sidebar');
  sidebarCollapsed = !sidebarCollapsed;
  if (sidebarCollapsed) {
    sb.style.width = '0px';
    sb.style.borderRightWidth = '0px';
    sb.style.overflow = 'hidden';
  } else {
    sb.style.width = '240px';
    sb.style.borderRightWidth = '';
    setTimeout(() => { sb.style.overflow = ''; }, 200);
  }
  localStorage.setItem('dm-sidebar', sidebarCollapsed ? '0' : '1');
}

// Restore sidebar state
(function() {
  if (localStorage.getItem('dm-sidebar') === '0') {
    const sb = document.getElementById('sidebar');
    if (sb) { sb.style.width = '0px'; sb.style.borderRightWidth = '0px'; sb.style.overflow = 'hidden'; sidebarCollapsed = true; }
  }
})();

function clearFeed() {
  for (const [id, domEl] of renderedFeedItems) {
    domEl.remove();
  }
  renderedFeedItems.clear();
  clearedAtTs = Date.now();
  sessionStorage.setItem('dm-cleared-at', String(clearedAtTs));
  if (emptyState) emptyState.style.display = '';
}

function toggleShardExpand(btn) {
  const target = document.getElementById(btn.dataset.target);
  if (!target) return;
  const expanded = btn.dataset.expanded === '1';
  if (expanded) {
    target.textContent = btn.dataset.short;
    btn.textContent = 'Show more';
    btn.dataset.expanded = '0';
  } else {
    target.textContent = btn.dataset.full;
    btn.textContent = 'Show less';
    btn.dataset.expanded = '1';
  }
}

// =========================================================================
// QR Code
// =========================================================================
function toggleQR() {
  const content = document.getElementById('qr-content');
  const chevron = document.getElementById('qr-chevron');
  qrExpanded = !qrExpanded;
  content.classList.toggle('hidden', !qrExpanded);
  chevron.style.transform = qrExpanded ? 'rotate(180deg)' : '';
}

function initQR(lanUrl) {
  if (qrInitialized) return;
  const el = document.getElementById('qr-code');
  const urlEl = document.getElementById('qr-url');
  const unavailEl = document.getElementById('qr-unavailable');
  if (!lanUrl) { unavailEl.classList.remove('hidden'); return; }
  qrInitialized = true;
  urlEl.textContent = lanUrl;
  el.innerHTML = '';
  const style = getComputedStyle(document.documentElement);
  new QRCode(el, {
    text: lanUrl, width: 120, height: 120,
    colorDark: style.getPropertyValue('--qr-fg').trim() || '#ffffff',
    colorLight: style.getPropertyValue('--qr-bg').trim() || '#1c1c1e',
    correctLevel: QRCode.CorrectLevel.M,
  });
  if (!qrExpanded) toggleQR();
}

function onThemeChange() {
  if (qrInitialized) {
    qrInitialized = false;
    initQR(document.getElementById('qr-url').textContent);
  }
}

// =========================================================================
// Security panel
// =========================================================================
let securityPanelOpen = false;

function toggleSecurityPanel() {
  securityPanelOpen = !securityPanelOpen;
  document.getElementById('security-panel').classList.toggle('hidden', !securityPanelOpen);
  document.getElementById('security-chevron').style.transform = securityPanelOpen ? 'rotate(180deg)' : '';
}

async function loadSecurityStatus() {
  try {
    const resp = await fetch('/api/security');
    if (!resp.ok) return;
    const data = await resp.json();
    // PIN badge
    const badge = document.getElementById('pin-status-badge');
    const clearBtn = document.getElementById('pin-clear-btn');
    if (data.pin_enabled) {
      badge.textContent = 'ON';
      badge.className = 'text-[10px] font-semibold px-1.5 py-0.5 rounded-md bg-accent-green/15 text-accent-green';
      clearBtn.classList.remove('hidden');
    } else {
      badge.textContent = 'OFF';
      badge.className = 'text-[10px] font-semibold px-1.5 py-0.5 rounded-md bg-surface text-muted';
      clearBtn.classList.add('hidden');
    }
    // Toggles
    document.getElementById('sec-auto-accept').checked = data.auto_accept_local;
    document.getElementById('sec-sandbox').checked = data.sandbox_enabled;
    document.getElementById('sec-sandbox-net').checked = data.sandbox_network;
    document.getElementById('sec-sandbox-net-row').classList.toggle('hidden', !data.sandbox_enabled);
  } catch (e) {
    document.getElementById('security-section').classList.add('hidden');
  }
}

async function setPin() {
  const input = document.getElementById('pin-set-input');
  const pin = input.value.trim();
  if (!pin) { showToast('Enter a PIN', 'error'); return; }
  const resp = await fetch('/api/security', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ pin }),
  });
  const data = await resp.json();
  if (data.success) {
    showToast('PIN set', 'success');
    input.value = '';
    loadSecurityStatus();
  } else {
    showToast(data.error || 'Failed', 'error');
  }
}

async function clearPin() {
  const resp = await fetch('/api/security', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ pin: '' }),
  });
  const data = await resp.json();
  if (data.success) {
    showToast('PIN cleared', 'success');
    loadSecurityStatus();
  } else {
    showToast(data.error || 'Failed', 'error');
  }
}

async function updateSecurityToggle(key, value) {
  const resp = await fetch('/api/security', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ [key]: value }),
  });
  const data = await resp.json();
  if (data.success) {
    showToast('Updated', 'success');
    if (key === 'sandbox_enabled') {
      document.getElementById('sec-sandbox-net-row').classList.toggle('hidden', !value);
    }
  } else {
    showToast(data.error || 'Failed', 'error');
  }
}

async function pushSecurityToLAN() {
  const resp = await fetch('/api/security/push', { method: 'POST' });
  const data = await resp.json();
  if (data.success) {
    const count = (data.pushed_to || []).length;
    showToast(count ? `Pushed to ${count} agent(s)` : 'No LAN agents found', count ? 'success' : 'info');
  } else {
    showToast('Failed', 'error');
  }
}

loadSecurityStatus();

// =========================================================================
// Target dropdown
// =========================================================================
let dropdownOpen = false;

function toggleDropdown() {
  dropdownOpen = !dropdownOpen;
  document.getElementById('target-dropdown-wrap').classList.toggle('dropdown-open', dropdownOpen);
}

function selectTarget(el) {
  const val = el.dataset.value;
  const label = el.textContent.trim();
  targetValue.value = val;
  targetLabel.textContent = label;
  const dot = document.getElementById('route-dot');
  if (val === 'broadcast') {
    dot.className = 'w-2 h-2 rounded-full bg-accent-yellow shrink-0';
  } else if (val === 'auto') {
    dot.className = 'w-2 h-2 rounded-full bg-accent-green shrink-0';
  } else if (val === 'fastest') {
    dot.className = 'w-2 h-2 rounded-full bg-[#06b6d4] shrink-0';
  } else {
    dot.style.background = 'var(--accent-blue)';
    dot.className = 'w-2 h-2 rounded-full shrink-0';
  }
  dropdownOpen = false;
  document.getElementById('target-dropdown-wrap').classList.remove('dropdown-open');
}

document.addEventListener('click', (e) => {
  if (!e.target.closest('#target-dropdown-wrap') && dropdownOpen) {
    dropdownOpen = false;
    document.getElementById('target-dropdown-wrap').classList.remove('dropdown-open');
  }
});

function updateDropdownOptions(conns) {
  const cur = targetValue.value;
  let html = `<div class="px-3 py-2 text-[13px] text-label hover:bg-surface cursor-pointer flex items-center gap-2 transition-colors rounded-lg mx-1"
    data-value="auto" onclick="selectTarget(this)">
    <div class="w-2 h-2 rounded-full bg-accent-green shrink-0"></div>auto
  </div>
  <div class="px-3 py-2 text-[13px] text-label hover:bg-surface cursor-pointer flex items-center gap-2 transition-colors rounded-lg mx-1"
    data-value="fastest" onclick="selectTarget(this)">
    <div class="w-2 h-2 rounded-full bg-[#06b6d4] shrink-0"></div>fastest
  </div>
  <div class="px-3 py-2 text-[13px] text-label hover:bg-surface cursor-pointer flex items-center gap-2 transition-colors rounded-lg mx-1"
    data-value="broadcast" onclick="selectTarget(this)">
    <div class="w-2 h-2 rounded-full bg-accent-yellow shrink-0"></div>broadcast all
  </div>`;
  if (conns.length > 0) {
    html += `<div class="border-t border-border/30 my-1 mx-2"></div>`;
    conns.forEach(c => {
      const dead = c.health_status === 'unreachable';
      const dotCls = dead ? 'bg-muted' : 'bg-accent-blue';
      const textCls = dead ? 'text-muted' : 'text-label';
      const hoverCls = dead ? '' : 'hover:bg-surface cursor-pointer';
      const clickAttr = dead ? '' : 'onclick="selectTarget(this)"';
      html += `<div class="px-3 py-2 text-[13px] ${textCls} ${hoverCls} flex items-center gap-2 transition-colors rounded-lg mx-1"
        data-value="${c.agent_id}" ${clickAttr}>
        <div class="w-2 h-2 rounded-full ${dotCls} shrink-0"></div>${esc(agentLabel(c))}
      </div>`;
    });
  }
  targetMenu.innerHTML = html;
  const validValues = ['auto', 'fastest', 'broadcast', ...conns.map(c => c.agent_id)];
  if (!validValues.includes(cur)) {
    targetValue.value = 'auto';
    targetLabel.textContent = 'auto';
    document.getElementById('route-dot').className = 'w-2 h-2 rounded-full bg-accent-green shrink-0';
  }
}

// =========================================================================
// Chat Feed — Build unified timeline
// =========================================================================
function buildFeedItems(data) {
  const items = [];
  const conns = data.connections || [];
  const findConn = (id) => conns.find(cn => cn.agent_id === id);

  // Outbound messages
  for (const msg of (data.outbox || [])) {
    items.push({
      id: 'out_' + msg.message_id,
      type: 'outbound',
      content: msg.content,
      timestamp: msg.created_at,
      status: msg.status,
      routed_to: msg.routed_to || [],
      routed_to_names: msg.routed_to_names || [],
      updates: msg.updates || [],
      responses: msg.responses || [],
      message_id: msg.message_id,
      attachments: (msg.metadata || {}).attachments || [],
    });

    // Responses to sent messages appear as inbound replies
    for (const r of (msg.responses || [])) {
      const c = findConn(r.agent_id);
      items.push({
        id: 'resp_' + msg.message_id + '_' + r.agent_id,
        type: 'response',
        content: r.response,
        timestamp: r.timestamp || msg.created_at,
        from_name: c ? agentLabel(c) : (r.display_name || shortId(r.agent_id)),
        from_agent_id: r.agent_id,
        parent_id: msg.message_id,
      });
    }
  }

  // Inbound messages
  for (const msg of (data.inbox || [])) {
    const c = findConn(msg.from_agent_id);
    items.push({
      id: 'in_' + msg.message_id,
      type: 'inbound',
      content: msg.content,
      timestamp: msg.received_at,
      from_name: c ? agentLabel(c) : (msg.from_display_name || shortId(msg.from_agent_id)),
      from_agent_id: msg.from_agent_id,
      message_id: msg.message_id,
      attachments: (msg.metadata || {}).attachments || [],
    });
  }

  // Shards
  for (const shard of (data.shards || [])) {
    items.push({
      id: 'shard_' + shard.shard_id,
      type: 'shard',
      content: shard.content || shard.summary || '',
      timestamp: shard.created_at,
      tags: shard.tags || [],
      author: shard.author_name || shortId(shard.author_id),
      author_id: shard.author_id,
    });
  }

  // Sort by timestamp
  items.sort((a, b) => {
    const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0;
    const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0;
    return ta - tb;
  });

  return items;
}

function renderFeedItem(item) {
  const el = document.createElement('div');
  el.className = 'fade-in';
  el.dataset.feedId = item.id;

  if (item.type === 'outbound') {
    const tint = agentTint(selfAgentId);
    const isFailed = item.status === 'failed' || item.status === 'timed_out';
    const failUpdate = isFailed ? (item.updates || []).find(u => u.type === 'delivery_failed' || u.type === 'timed_out') : null;
    const failReason = failUpdate ? (failUpdate.error || failUpdate.type) : (isFailed ? item.status : '');
    el.innerHTML = `
      <div class="msg-block msg-block-out" style="background:${tint}" data-status="${item.status}">
        <div class="flex items-baseline gap-3 mb-1.5">
          <span class="msg-sender text-primary">You</span>
          ${item.routed_to_names.length ? `<span class="msg-meta">&rarr; ${esc(item.routed_to_names.join(', '))}</span>` : ''}
          <span class="msg-meta ml-auto flex items-center gap-1.5" title="${item.timestamp || ''}">${timeAgo(item.timestamp)} ${buildStatusDot(item.status, item.updates)}</span>
        </div>
        <div class="msg-content">${renderMd(item.content)}</div>
        ${renderAttachmentsInMessage(item.attachments)}
        ${isFailed ? `
          <div class="mt-2 px-3 py-2 rounded-lg text-[12px] flex items-center gap-2 flex-wrap" style="background: ${item.status === 'timed_out' ? 'rgba(255,149,0,0.15)' : 'rgba(255,59,48,0.15)'}; color: ${item.status === 'timed_out' ? '#FF9500' : '#FF3B30'}">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="shrink-0"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
            <span class="flex-1 min-w-0 truncate">${item.status === 'timed_out' ? 'Timed out' : 'Delivery failed'}${failReason && failReason !== item.status ? ': ' + esc(failReason) : ''}</span>
            <button class="retry-btn text-[11px] font-semibold px-3 py-1 rounded-md transition-all hover:opacity-80 shrink-0" style="background: ${item.status === 'timed_out' ? '#FF9500' : '#FF3B30'}; color: white" data-message-id="${item.message_id}">Retry</button>
          </div>` : ''}
      </div>`;
    const retryBtn = el.querySelector('.retry-btn');
    if (retryBtn) {
      retryBtn.addEventListener('click', async () => {
        retryBtn.disabled = true;
        retryBtn.textContent = 'Retrying...';
        try {
          const result = await ajaxPost(`/retry/${encodeURIComponent(item.message_id)}`, {});
          if (result.success) {
            showToast('Message re-sent', 'success');
            pollServer();
          } else {
            showToast(result.error || 'Retry failed', 'error');
            retryBtn.disabled = false;
            retryBtn.textContent = 'Retry';
          }
        } catch (err) {
          showToast('Retry failed', 'error');
          retryBtn.disabled = false;
          retryBtn.textContent = 'Retry';
        }
      });
    }
  } else if (item.type === 'inbound') {
    const aColor = agentColor(item.from_agent_id);
    const tint = agentTint(item.from_agent_id);
    el.innerHTML = `
      <div class="msg-block msg-block-in group" style="background:${tint}">
        <div class="flex items-baseline gap-3 mb-1.5">
          <span class="msg-sender" style="color:${aColor}">${esc(item.from_name)}</span>
          <span class="msg-meta" title="${item.timestamp || ''}">${timeAgo(item.timestamp)}</span>
          <button class="msg-reply-btn msg-meta opacity-0 group-hover:opacity-100 transition-opacity flex items-center gap-1 hover:text-primary cursor-pointer ml-auto" data-agent-id="${item.from_agent_id}" data-agent-name="${esc(item.from_name)}" data-message-id="${item.message_id}">
            <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg>
            Reply
          </button>
        </div>
        <div class="msg-content">${renderMd(item.content)}</div>
        ${renderAttachmentsInMessage(item.attachments)}
        <div class="reply-area" data-reply-for="${item.message_id}">
          <div class="flex gap-2">
            <input type="text" placeholder="Reply to ${esc(item.from_name)}..." data-message-id="${item.message_id}"
              class="reply-input flex-1 compose-ring bg-card/30 px-4 py-2.5 text-[14px] text-primary placeholder-muted/50 min-w-0">
            <button type="button" data-message-id="${item.message_id}"
              class="reply-btn text-white text-[13px] font-semibold px-5 py-2.5 rounded-lg transition-all hover:opacity-90"
              style="background: var(--accent-blue)">
              Send
            </button>
          </div>
        </div>
      </div>`;
    const replyBtn = el.querySelector('.msg-reply-btn');
    const replyArea = el.querySelector('.reply-area');
    replyBtn.addEventListener('click', () => {
      replyArea.classList.toggle('open');
      if (replyArea.classList.contains('open')) {
        replyArea.querySelector('.reply-input').focus();
      }
    });
    wireReply(el, item);
  } else if (item.type === 'response') {
    const aColor = agentColor(item.from_agent_id);
    const tint = agentTint(item.from_agent_id);
    el.innerHTML = `
      <div class="msg-block msg-block-in" style="background:${tint}">
        <div class="flex items-baseline gap-3 mb-1.5">
          <span class="msg-sender" style="color:${aColor}">${esc(item.from_name)}</span>
          <span class="msg-meta" title="${item.timestamp || ''}">${timeAgo(item.timestamp)}</span>
        </div>
        <div class="msg-content">${renderMd(item.content)}</div>
      </div>`;
  } else if (item.type === 'shard') {
    const shardId = 'shard-content-' + item.id.replace(/[^a-zA-Z0-9]/g, '_');
    const isLong = item.content.length > 300;
    const aColor = agentColor(item.author_id);
    el.innerHTML = `
      <div class="msg-block" style="padding-top:0.75rem;padding-bottom:0.75rem">
        <div class="shard-card">
          <div class="px-5 py-4">
            <div class="flex items-center justify-between mb-3">
              <div class="flex items-baseline gap-2.5">
                <span class="msg-sender" style="color:${aColor}">${esc(item.author)}</span>
                <span class="msg-meta">${timeAgo(item.timestamp)}</span>
              </div>
              <span class="text-[9px] font-bold uppercase tracking-widest px-2 py-0.5 rounded-md" style="background: linear-gradient(135deg, rgba(175,82,222,0.1), rgba(10,132,255,0.1)); color: #AF52DE;">SHARD</span>
            </div>
            <div id="${shardId}" class="msg-content" style="color:rgb(var(--c-label))">${esc(isLong ? item.content.slice(0, 300) : item.content)}${isLong ? '...' : ''}</div>
            ${isLong ? `<span class="shard-expand-btn text-[12px] mt-2 inline-block font-medium" style="color: var(--accent-blue)" data-expanded="0" data-full="${esc(item.content).replace(/"/g, '&quot;')}" data-short="${esc(item.content.slice(0, 300)).replace(/"/g, '&quot;')}..." data-target="${shardId}" onclick="toggleShardExpand(this)">Show more</span>` : ''}
            ${item.tags.length ? `<div class="flex flex-wrap gap-1.5 mt-3 pt-3" style="border-top:1px solid rgb(var(--c-border)/0.06)">${item.tags.map(t => `<span class="text-[11px] px-2.5 py-0.5 rounded-full font-medium" style="background: rgb(var(--c-surface) / 0.5); color: rgb(var(--c-muted))">${esc(t)}</span>`).join('')}</div>` : ''}
          </div>
        </div>
      </div>`;
  } else if (item.type === 'connection_event') {
    const aColor = item.agent_id ? agentColor(item.agent_id) : null;
    const lvl = item.connectivity_level ? `L${item.connectivity_level}${item.connectivity_method ? ':' + item.connectivity_method : ''}` : '';
    el.innerHTML = `
      <div class="sys-event">
        ${aColor ? `<span style="color:${aColor};font-weight:600">${esc(item.agent_name || '')}</span> connected` : esc(item.content)}${lvl ? ` <span style="opacity:0.4;font-family:monospace;font-size:0.7rem">[${lvl}]</span>` : ''}
      </div>`;
  } else if (item.type === 'pending_request') {
    const aColor = agentColor(item.from_agent_id);
    const tint = agentTint(item.from_agent_id);
    el.innerHTML = `
      <div class="msg-block" style="background:${tint}">
        <div class="flex items-center justify-between gap-4">
          <div class="min-w-0">
            <div class="flex items-baseline gap-2">
              <span class="msg-sender" style="color:${aColor}">${esc(item.from_name)}</span>
              <span class="msg-meta">wants to connect</span>
            </div>
            ${item.bio ? `<div class="msg-meta mt-1 leading-snug">${esc(item.bio.slice(0, 120))}</div>` : ''}
          </div>
          <div class="flex gap-2 shrink-0">
            <button class="feed-accept-btn text-white text-[12px] font-semibold px-4 py-1.5 rounded-lg transition-all hover:opacity-90" style="background: var(--accent-blue)">Accept</button>
            <button class="feed-reject-btn text-[12px] font-medium px-3 py-1.5 rounded-lg transition-all border" style="color:rgb(var(--c-muted));border-color:rgb(var(--c-border)/0.2)">Deny</button>
          </div>
        </div>
      </div>`;
    el.querySelector('.feed-accept-btn').addEventListener('click', async () => {
      try {
        const result = await ajaxPost(`/accept/${encodeURIComponent(item.request_id)}`, {});
        if (result.success) showToast(`Connected to ${item.from_name}`, 'success');
      } catch (err) { showToast('Accept failed', 'error'); }
      pollServer();
    });
    el.querySelector('.feed-reject-btn').addEventListener('click', async () => {
      try {
        await ajaxPost(`/reject/${encodeURIComponent(item.request_id)}`, {});
        showToast('Request denied', 'info');
      } catch (err) { showToast('Reject failed', 'error'); }
      pollServer();
    });
  }

  return el;
}

function buildStatusDot(status, updates) {
  if (!updates || !updates.length) {
    if (status === 'pending') return '<div class="w-1.5 h-1.5 rounded-full bg-muted"></div>';
    if (status === 'delivered') return '<div class="w-1.5 h-1.5 rounded-full bg-accent-green"></div>';
    if (status === 'failed') return '<div class="w-1.5 h-1.5 rounded-full bg-accent-red" title="failed"></div>';
    if (status === 'timed_out') return '<div class="w-1.5 h-1.5 rounded-full bg-accent-yellow" title="timed out"></div>';
    return '';
  }
  const lastType = updates[updates.length - 1].type;
  const colors = {
    received: 'bg-accent-blue', responding: 'bg-accent-yellow animate-pulse',
    forwarded: 'bg-accent-purple', expired: 'bg-accent-red',
    delivery_failed: 'bg-accent-red', timed_out: 'bg-accent-yellow',
  };
  return `<div class="w-1.5 h-1.5 rounded-full ${colors[lastType] || 'bg-muted'}" title="${lastType}"></div>`;
}

function wireReply(el, item) {
  const input = el.querySelector('.reply-input');
  const btn = el.querySelector('.reply-btn');
  if (!input || !btn) return;

  async function doReply() {
    const text = input.value.trim();
    if (!text) return;
    btn.disabled = true;
    input.disabled = true;
    try {
      const result = await ajaxPost(`/respond/${encodeURIComponent(item.message_id)}`, { response: text });
      if (result.success) {
        showToast('Reply sent', 'success');
        input.value = '';
      } else {
        showToast(result.error || 'Reply failed', 'error');
      }
    } catch (err) {
      showToast('Reply failed', 'error');
    }
    btn.disabled = false;
    input.disabled = false;
  }

  btn.addEventListener('click', doReply);
  input.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); doReply(); }
  });
}

function renderFeed(data) {
  const items = buildFeedItems(data);

  // Detect connection events by diffing
  const currConnIds = new Set(data.connections.map(c => c.agent_id));
  if (prevConnIds.size > 0) {
    for (const c of data.connections) {
      if (!prevConnIds.has(c.agent_id)) {
        items.push({
          id: 'evt_conn_' + c.agent_id + '_' + Date.now(),
          type: 'connection_event',
          content: `${agentLabel(c)} connected`,
          timestamp: new Date().toISOString(),
          agent_id: c.agent_id,
          agent_name: agentLabel(c),
          connectivity_level: c.connectivity_level || null,
          connectivity_method: c.connectivity_method || '',
        });
      }
    }
    for (const id of prevConnIds) {
      if (!currConnIds.has(id)) {
        items.push({
          id: 'evt_disc_' + id + '_' + Date.now(),
          type: 'connection_event',
          content: `Agent disconnected`,
          timestamp: new Date().toISOString(),
          agent_id: id,
          agent_name: null,
        });
      }
    }
  }
  prevConnIds = currConnIds;

  // Add pending requests as feed items
  for (const req of (data.pending_requests || [])) {
    items.push({
      id: 'pending_' + req.request_id,
      type: 'pending_request',
      timestamp: req.requested_at || new Date().toISOString(),
      from_name: req.from_display_name || shortId(req.from_agent_id),
      from_agent_id: req.from_agent_id,
      bio: req.from_bio,
      request_id: req.request_id,
    });
  }

  // Re-sort after adding events
  items.sort((a, b) => {
    const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0;
    const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0;
    return ta - tb;
  });

  const newIds = new Set(items.map(i => i.id));

  // Remove stale (except connection events which are ephemeral)
  for (const [id, el] of renderedFeedItems) {
    if (!newIds.has(id) && !id.startsWith('evt_')) {
      el.remove();
      renderedFeedItems.delete(id);
    }
  }

  // Filter out cleared items for empty state check
  const visibleItems = clearedAtTs > 0
    ? items.filter(i => !i.timestamp || new Date(i.timestamp).getTime() >= clearedAtTs)
    : items;
  if (emptyState) emptyState.style.display = visibleItems.length > 0 ? 'none' : '';

  // Check if near bottom for auto-scroll
  const nearBottom = feedScroll.scrollHeight - feedScroll.scrollTop - feedScroll.clientHeight < 80;

  // Add new items (skip items cleared by user)
  for (const item of items) {
    if (clearedAtTs > 0 && item.timestamp) {
      const itemTs = new Date(item.timestamp).getTime();
      if (itemTs < clearedAtTs) continue;
    }
    if (renderedFeedItems.has(item.id)) {
      // Update existing outbound status
      if (item.type === 'outbound') {
        const existing = renderedFeedItems.get(item.id);
        const prevStatus = existing.querySelector('.msg-block')?.dataset.status;
        if (prevStatus !== item.status && (item.status === 'failed' || item.status === 'timed_out')) {
          // Full re-render to show failure banner
          const newEl = renderFeedItem(item);
          newEl.dataset.feedId = item.id;
          existing.replaceWith(newEl);
          renderedFeedItems.set(item.id, newEl);
        } else {
          const dot = existing.querySelector('.msg-meta [class*="rounded-full"]');
          if (dot) dot.outerHTML = buildStatusDot(item.status, item.updates);
        }
      }
      continue;
    }
    if (item.type === 'connection_event' && renderedFeedItems.has(item.id)) continue;

    const el = renderFeedItem(item);
    feedContainer.appendChild(el);
    renderedFeedItems.set(item.id, el);
  }

  // Auto-scroll
  if (nearBottom) {
    feedScroll.scrollTop = feedScroll.scrollHeight;
  }
}

// =========================================================================
// Send message
// =========================================================================
composeForm.addEventListener('submit', async (e) => {
  e.preventDefault();
  const content = composeInput.value.trim();
  if (!content && !pendingAttachments.length) return;
  const target = targetValue.value;

  sendBtn.disabled = true;
  composeInput.value = '';
  composeInput.style.height = 'auto';
  const attachments = [...pendingAttachments];
  pendingAttachments = [];
  renderAttachmentChips();

  try {
    const body = { content, target };
    if (attachments.length) body.attachments = attachments;
    const result = await ajaxPost('/send', body);
    if (!result.success) {
      showToast(result.error || 'Send failed', 'error');
    } else {
      if (target === 'broadcast' && result.broadcast) {
        showToast(`Broadcast to ${result.sent_count} agent${result.sent_count !== 1 ? 's' : ''}`, 'success');
      }
      pollServer();
      setTimeout(() => { feedScroll.scrollTop = feedScroll.scrollHeight; }, 300);
    }
  } catch (err) {
    showToast('Failed to send message', 'error');
  } finally {
    sendBtn.disabled = false;
    composeInput.focus();
  }
});

composeInput.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();
    composeForm.dispatchEvent(new Event('submit'));
  }
});

// =========================================================================
// File attachments: input, drag-drop, paste
// =========================================================================
document.getElementById('file-input').addEventListener('change', (e) => {
  if (e.target.files.length) uploadFiles(e.target.files);
  e.target.value = '';
});

document.getElementById('camera-input').addEventListener('change', (e) => {
  if (e.target.files.length) uploadFiles(e.target.files);
  e.target.value = '';
});

const dropZone = document.getElementById('compose-drop-zone');
const dropOverlay = document.getElementById('drop-overlay');
let dragCounter = 0;
dropZone.addEventListener('dragenter', (e) => { e.preventDefault(); dragCounter++; dropOverlay.classList.remove('hidden'); });
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dragCounter--; if (dragCounter <= 0) { dragCounter = 0; dropOverlay.classList.add('hidden'); } });
dropZone.addEventListener('dragover', (e) => e.preventDefault());
dropZone.addEventListener('drop', (e) => {
  e.preventDefault();
  dragCounter = 0;
  dropOverlay.classList.add('hidden');
  if (e.dataTransfer.files.length) uploadFiles(e.dataTransfer.files);
});

composeInput.addEventListener('paste', (e) => {
  const items = e.clipboardData?.items;
  if (!items) return;
  const files = [];
  for (const item of items) {
    if (item.kind === 'file') files.push(item.getAsFile());
  }
  if (files.length) {
    e.preventDefault();
    uploadFiles(files);
  }
});

// =========================================================================
// Connect form
// =========================================================================
connectForm.addEventListener('submit', async (e) => {
  e.preventDefault();
  const urlInput = connectForm.querySelector('input[name="url"]');
  const url = urlInput.value.trim();
  if (!url) return;

  const btn = connectForm.querySelector('button');
  btn.disabled = true;
  try {
    const result = await ajaxPost('/connect', { url });
    if (result.success) {
      showToast(result.status === 'connected' ? 'Connected' : 'Request sent', 'success');
      urlInput.value = '';
    } else {
      showToast(result.error || 'Connection failed', 'error');
    }
  } catch (err) {
    showToast('Connection failed', 'error');
  } finally {
    btn.disabled = false;
  }
  pollServer();
});

// =========================================================================
// Render pending requests (sidebar)
// =========================================================================
function renderPending(requests) {
  if (requests.length === 0) {
    pendingSection.classList.add('hidden');
    for (const [id, entry] of renderedPending) { entry.el.remove(); }
    renderedPending.clear();
    return;
  }
  pendingSection.classList.remove('hidden');

  const currentIds = new Set(requests.map(r => r.request_id));
  for (const [id, entry] of renderedPending) {
    if (!currentIds.has(id)) { entry.el.remove(); renderedPending.delete(id); }
  }

  requests.forEach(r => {
    if (renderedPending.has(r.request_id)) return;

    const el = document.createElement('div');
    el.className = 'bg-surface border border-border/30 rounded-xl p-2.5 fade-in';
    el.innerHTML = `
      <div class="flex items-center justify-between gap-2">
        <div class="min-w-0">
          <div class="text-[13px] text-primary font-medium truncate">${esc(agentLabel(r))}</div>
          ${r.from_bio ? `<div class="text-[11px] text-muted truncate mt-0.5">${esc(r.from_bio.slice(0, 40))}</div>` : ''}
        </div>
        <div class="flex gap-1 shrink-0">
          <button class="accept-btn text-white text-[11px] font-semibold px-2.5 py-1 rounded-lg transition-colors" style="background: var(--accent-blue)">Accept</button>
          <button class="reject-btn text-accent-red bg-accent-red/10 text-[11px] px-2 py-1 rounded-lg transition-colors hover:bg-accent-red/20">Deny</button>
        </div>
      </div>`;

    el.querySelector('.accept-btn').addEventListener('click', async () => {
      try {
        const result = await ajaxPost(`/accept/${encodeURIComponent(r.request_id)}`, {});
        if (result.success) showToast(`Connected to ${agentLabel(r)}`, 'success');
      } catch (err) { showToast('Accept failed', 'error'); }
      pollServer();
    });
    el.querySelector('.reject-btn').addEventListener('click', async () => {
      try {
        await ajaxPost(`/reject/${encodeURIComponent(r.request_id)}`, {});
        showToast('Request denied', 'info');
      } catch (err) { showToast('Reject failed', 'error'); }
      pollServer();
    });

    pendingListEl.appendChild(el);
    renderedPending.set(r.request_id, { el });
  });
}

// =========================================================================
// Render discovered agents (sidebar)
// =========================================================================
function renderDiscovered(agents) {
  const currentIds = new Set(agents.map(a => a.agent_id));
  for (const [id, entry] of renderedDiscovered) {
    if (!currentIds.has(id)) { entry.el.remove(); renderedDiscovered.delete(id); }
  }

  if (agents.length === 0 && renderedDiscovered.size === 0) {
    if (!discoveredList.querySelector('.text-muted')) {
      discoveredList.innerHTML = '<p class="text-[12px] text-muted">No agents found</p>';
    }
    return;
  }

  const placeholder = discoveredList.querySelector('p.text-muted');
  if (placeholder && agents.length > 0) placeholder.remove();

  agents.forEach(a => {
    const hash = `${a.display_name}|${a.bio}|${a.port}|${a.status}`;
    const existing = renderedDiscovered.get(a.agent_id);
    if (existing && existing.hash === hash) return;
    if (existing) { existing.el.remove(); renderedDiscovered.delete(a.agent_id); }

    const el = document.createElement('div');
    el.className = 'bg-surface border border-border/30 rounded-xl p-2.5 flex items-center justify-between gap-2';
    el.innerHTML = `
      <div class="min-w-0">
        <div class="flex items-center gap-1">
          <span class="text-[13px] text-primary font-medium truncate">${esc(agentLabel(a))}</span>
          <span class="text-[11px] text-muted">:${a.port}</span>
        </div>
        ${a.bio ? `<div class="text-[11px] text-muted truncate mt-0.5">${esc(a.bio.slice(0, 35))}</div>` : ''}
      </div>
      <button type="button"
        class="connect-btn shrink-0 text-[11px] font-semibold px-2.5 py-1 rounded-lg transition-colors"
        style="background: var(--accent-blue); color: white">
        Connect
      </button>`;

    el.querySelector('.connect-btn').addEventListener('click', async (e) => {
      const btn = e.currentTarget;
      btn.disabled = true; btn.textContent = '...';
      try {
        const result = await ajaxPost('/connect', { url: a.url });
        if (result.success) {
          showToast(result.status === 'connected' ? `Connected to ${agentLabel(a)}` : 'Request sent', 'success');
        } else { showToast(result.error || 'Failed', 'error'); }
      } catch (err) { showToast('Connection failed', 'error'); }
      btn.disabled = false; btn.textContent = 'Connect';
      pollServer();
    });

    discoveredList.appendChild(el);
    renderedDiscovered.set(a.agent_id, { el, hash });
  });
}

// =========================================================================
// Render connections (sidebar)
// =========================================================================
function createConnNode(c) {
  const el = document.createElement('div');
  el.className = 'flex items-center justify-between text-[12px] py-1 group rounded-lg px-1.5 hover:bg-surface/50 transition-colors';
  const dead = c.health_status === 'unreachable';
  const dotColor = dead ? 'bg-accent-red' : 'bg-accent-green';
  const nameClass = dead ? 'text-muted truncate' : 'text-label truncate';
  el.innerHTML = `
    <div class="flex items-center gap-2 min-w-0">
      <div class="w-1.5 h-1.5 rounded-full ${dotColor} shrink-0"></div>
      <span class="${nameClass}">${esc(agentLabel(c))}</span>
    </div>
    <button type="button" class="dc-btn text-muted hover:text-accent-red transition-colors ml-1 shrink-0 opacity-0 group-hover:opacity-100 text-sm">&times;</button>`;

  el.querySelector('.dc-btn').addEventListener('click', async () => {
    try {
      await ajaxPost(`/disconnect/${encodeURIComponent(c.agent_id)}`, {});
      showToast(`Disconnected from ${agentLabel(c)}`, 'info');
    } catch (err) { showToast('Disconnect failed', 'error'); }
    pollServer();
  });

  // Click to target this agent
  el.addEventListener('click', (e) => {
    if (e.target.closest('.dc-btn')) return;
    targetValue.value = c.agent_id;
    targetLabel.textContent = agentLabel(c);
    const dot = document.getElementById('route-dot');
    dot.style.background = 'var(--accent-blue)';
    dot.className = 'w-2 h-2 rounded-full shrink-0';
    composeInput.focus();
  });

  return el;
}

function connHash(c) {
  return `${c.display_name}|${c.bio}|${c.direction}|${c.last_activity}|${c.health_status || 'ok'}|${c.connectivity_level || 0}`;
}

function renderConnections(conns) {
  const currentIds = new Set(conns.map(c => c.agent_id));
  for (const [id, entry] of renderedConns) {
    if (!currentIds.has(id)) { entry.el.remove(); renderedConns.delete(id); }
  }
  conns.forEach(c => {
    const h = connHash(c);
    const existing = renderedConns.get(c.agent_id);
    if (existing && existing.hash === h) return;
    if (existing) { existing.el.remove(); renderedConns.delete(c.agent_id); }
    const ph = connectionsList.querySelector('p.text-muted');
    if (ph) ph.remove();
    const el = createConnNode(c);
    connectionsList.appendChild(el);
    renderedConns.set(c.agent_id, { el, hash: h });
  });
  if (conns.length === 0 && connectionsList.children.length === 0) {
    connectionsList.innerHTML = '<p class="text-[12px] text-muted">No connections</p>';
  }

  // Mobile
  for (const [id, entry] of renderedConnsMobile) {
    if (!currentIds.has(id)) { entry.el.remove(); renderedConnsMobile.delete(id); }
  }
  conns.forEach(c => {
    const h = connHash(c);
    const existing = renderedConnsMobile.get(c.agent_id);
    if (existing && existing.hash === h) return;
    if (existing) { existing.el.remove(); renderedConnsMobile.delete(c.agent_id); }
    const el = createConnNode(c);
    mobileConnectionsList.appendChild(el);
    renderedConnsMobile.set(c.agent_id, { el, hash: h });
  });

  document.getElementById('sidebar-conn-count').textContent = conns.length;
  updateDropdownOptions(conns);
}

// =========================================================================
// Check In — broadcast status request to all agents
// =========================================================================
async function checkinAll() {
  const btn = document.getElementById('checkin-btn');
  const origText = btn.textContent;
  btn.textContent = 'Checking...';
  btn.disabled = true;
  try {
    const data = await ajaxPost('/api/checkin', {});
    if (data.success) {
      showToast(`Check-in sent to ${data.sent_count || 0} agent(s)`, 'success');
    } else {
      showToast(data.error || 'Check-in failed', 'error');
    }
  } catch (e) {
    showToast('Check-in failed: ' + e.message, 'error');
  } finally {
    btn.textContent = origText;
    btn.disabled = false;
  }
}

// =========================================================================
// Drawing Canvas
// =========================================================================
let drawCanvasOpen = false;
let drawCtx = null;
let drawing = false;
let drawHistory = [];

function toggleDrawCanvas() {
  drawCanvasOpen = !drawCanvasOpen;
  const panel = document.getElementById('draw-panel');
  const btn = document.getElementById('draw-toggle-btn');
  panel.classList.toggle('hidden', !drawCanvasOpen);
  btn.classList.toggle('text-accent-blue', drawCanvasOpen);
  if (drawCanvasOpen && !drawCtx) initDrawCanvas();
}

function initDrawCanvas() {
  const canvas = document.getElementById('draw-canvas');
  drawCtx = canvas.getContext('2d');
  // Set canvas resolution to match display
  const rect = canvas.getBoundingClientRect();
  canvas.width = rect.width * 2;
  canvas.height = rect.height * 2;
  drawCtx.scale(2, 2);
  drawCtx.lineCap = 'round';
  drawCtx.lineJoin = 'round';

  function getPos(e) {
    const r = canvas.getBoundingClientRect();
    const t = e.touches ? e.touches[0] : e;
    return { x: t.clientX - r.left, y: t.clientY - r.top };
  }

  canvas.addEventListener('pointerdown', (e) => {
    drawing = true;
    drawCtx.strokeStyle = document.getElementById('draw-color').value;
    drawCtx.lineWidth = document.getElementById('draw-size').value;
    const p = getPos(e);
    drawCtx.beginPath();
    drawCtx.moveTo(p.x, p.y);
    // Save state for undo
    drawHistory.push(canvas.toDataURL());
    if (drawHistory.length > 50) drawHistory.shift();
  });

  canvas.addEventListener('pointermove', (e) => {
    if (!drawing) return;
    const p = getPos(e);
    drawCtx.lineTo(p.x, p.y);
    drawCtx.stroke();
  });

  canvas.addEventListener('pointerup', () => drawing = false);
  canvas.addEventListener('pointerleave', () => drawing = false);
}

function clearDrawCanvas() {
  if (!drawCtx) return;
  const canvas = document.getElementById('draw-canvas');
  drawHistory.push(canvas.toDataURL());
  drawCtx.clearRect(0, 0, canvas.width, canvas.height);
}

function undoDrawCanvas() {
  if (!drawCtx || drawHistory.length === 0) return;
  const canvas = document.getElementById('draw-canvas');
  const img = new Image();
  img.onload = () => {
    drawCtx.clearRect(0, 0, canvas.width, canvas.height);
    drawCtx.drawImage(img, 0, 0, canvas.width / 2, canvas.height / 2);
  };
  img.src = drawHistory.pop();
}

async function sendDrawing() {
  const canvas = document.getElementById('draw-canvas');
  canvas.toBlob(async (blob) => {
    if (!blob) return;
    const formData = new FormData();
    formData.append('files', blob, 'drawing.png');
    try {
      const uploadResp = await fetch('/api/upload', { method: 'POST', body: formData });
      const uploadData = await uploadResp.json();
      if (!uploadData.success || !uploadData.files.length) {
        showToast('Failed to upload drawing', 'error');
        return;
      }
      // Send as a message with attachment
      const file = uploadData.files[0];
      const target = targetValue.value;
      const sendResp = await ajaxPost('/send', {
        content: '[drawing]',
        target: target,
        attachments: JSON.stringify([file]),
      });
      if (sendResp.success) {
        showToast('Drawing sent', 'success');
        clearDrawCanvas();
        toggleDrawCanvas();
      } else {
        showToast(sendResp.error || 'Send failed', 'error');
      }
    } catch (e) {
      showToast('Failed to send drawing: ' + e.message, 'error');
    }
  }, 'image/png');
}

async function updateAllAgents() {
  const btn = document.getElementById('update-all-btn');
  const origText = btn.textContent;
  btn.textContent = 'Updating...';
  btn.disabled = true;
  try {
    const data = await ajaxPost('/api/update-agents', {});
    if (data.results && data.results.length > 0) {
      const succeeded = data.results.filter(r => r.success).length;
      showToast(`Updated ${succeeded}/${data.results.length} agents`, succeeded > 0 ? 'success' : 'error');
      data.results.forEach(r => {
        const status = r.success ? 'ok' : 'FAILED';
        console.log(`[Update] ${r.display_name}: ${status} — ${r.git_output}`);
      });
    } else {
      showToast(data.message || 'No local agents found', 'info');
    }
  } catch (e) {
    showToast('Update failed: ' + e.message, 'error');
  } finally {
    btn.textContent = origText;
    btn.disabled = false;
  }
}

// =========================================================================
// Network context panel
// =========================================================================
let contextExpanded = false;

function toggleContextPanel() {
  contextExpanded = !contextExpanded;
  document.getElementById('context-panel').classList.toggle('hidden', !contextExpanded);
  document.getElementById('context-chevron').style.transform = contextExpanded ? 'rotate(180deg)' : '';
}

function renderContextPanel(data) {
  const banner = document.getElementById('context-banner');
  const summary = document.getElementById('context-summary');
  const container = document.getElementById('context-connections');
  const conns = data.connections || [];
  const shardCount = (data.shards || []).length;
  const inboxCount = (data.inbox || []).length;
  const outboxCount = (data.outbox || []).length;

  if (conns.length === 0 && shardCount === 0) {
    banner.classList.add('hidden');
    return;
  }
  banner.classList.remove('hidden');

  // Summary line
  const parts = [];
  if (conns.length) parts.push(`${conns.length} connection${conns.length > 1 ? 's' : ''}`);
  if (inboxCount) parts.push(`${inboxCount} queued`);
  if (outboxCount) parts.push(`${outboxCount} sent`);
  if (shardCount) parts.push(`${shardCount} shard${shardCount > 1 ? 's' : ''}`);
  summary.textContent = parts.join('  ·  ') || 'Network context';

  // Connection details
  let html = '';
  for (const c of conns) {
    const color = agentColor(c.agent_id);
    const dead = c.health_status === 'unreachable';
    const healthDot = dead ? 'bg-accent-red' : 'bg-accent-green';
    const level = c.connectivity_level ? `L${c.connectivity_level}` : '';
    const method = c.connectivity_method || '';
    const levelBadge = level ? `<span class="text-[10px] font-mono px-1.5 py-0.5 rounded" style="background: rgb(var(--c-surface)); color: rgb(var(--c-muted))">${level}${method ? ':' + method : ''}</span>` : '';
    const spawned = c.spawned_agents ? `<span class="text-[10px] text-muted">${c.spawned_agents} agent${c.spawned_agents > 1 ? 's' : ''} spawned</span>` : '';
    const wallet = c.wallets && c.wallets.solana ? `<span class="text-[10px] text-muted font-mono">${c.wallets.solana.slice(0, 6)}...${c.wallets.solana.slice(-4)}</span>` : '';

    const tint = agentTint(c.agent_id);
    html += `
      <div class="rounded-xl px-3.5 py-2.5" style="background:${tint}">
        <div class="flex items-center gap-2 flex-wrap">
          <span class="text-[13px] font-semibold" style="color:${color}">${esc(agentLabel(c))}</span>
          <div class="w-1.5 h-1.5 rounded-full ${healthDot}"></div>
          ${levelBadge}
          ${spawned}
          ${wallet}
        </div>
        ${c.bio ? `<div class="text-[12px] text-muted mt-0.5 leading-snug">${esc(c.bio.slice(0, 120))}</div>` : ''}
        <div class="flex items-center gap-2 mt-0.5 text-[10px] text-subtle">
          <span>${c.direction || ''}</span>
          ${c.last_activity ? `<span>active ${timeAgo(c.last_activity)}</span>` : ''}
        </div>
      </div>`;
  }
  container.innerHTML = html;
}

// =========================================================================
// Main poll handler
// =========================================================================
function onPollData(data) {
  if (data.self) selfAgentId = data.self.agent_id;
  _walletData = data;

  renderPending(data.pending_requests);
  renderDiscovered(data.discovered || []);
  renderConnections(data.connections);
  renderContextPanel(data);
  renderFeed(data);

  // Update route-dot to reflect target agent health
  const curTarget = targetValue.value;
  if (curTarget && curTarget !== 'auto' && curTarget !== 'broadcast') {
    const targetConn = (data.connections || []).find(c => c.agent_id === curTarget);
    const dot = document.getElementById('route-dot');
    if (dot) {
      if (!targetConn || targetConn.health_status === 'unreachable') {
        dot.style.background = '#FF3B30';
        dot.title = 'Target unreachable';
      } else {
        dot.style.background = 'var(--accent-blue)';
        dot.title = '';
      }
    }
  }

  // QR code init
  if (data.self && data.self.lan_url && !qrInitialized) {
    initQR(data.self.lan_url);
  }

  // NAT status
  if (data.self && data.self.nat_detected) {
    document.getElementById('nat-status').classList.remove('hidden');
  }

  // Update sidebar identity
  if (data.self) {
    const name = data.self.display_name || 'Human';
    document.getElementById('sidebar-name').textContent = name;
    document.getElementById('sidebar-avatar').textContent = name.charAt(0).toUpperCase();
  }
}

// =========================================================================
// Wallet functions
// =========================================================================
function openWalletModal() {
  document.getElementById('wallet-modal').classList.remove('hidden');
  refreshWalletBalances();
  _populateSendForm();
}

function closeWalletModal() {
  document.getElementById('wallet-modal').classList.add('hidden');
  if (_walletQRPollTimer) { clearInterval(_walletQRPollTimer); _walletQRPollTimer = null; }
}

function walletMainTab(tab) {
  const tabs = ['balances', 'send', 'identity'];
  tabs.forEach(t => {
    const btn = document.getElementById(`wallet-main-tab-${t}`);
    const panel = document.getElementById(`wallet-panel-${t}`);
    if (t === tab) {
      btn.className = 'flex-1 text-[12px] font-semibold py-2.5 transition-colors text-primary border-b-2';
      btn.style.borderColor = 'var(--accent-blue)';
      panel.classList.remove('hidden');
    } else {
      btn.className = 'flex-1 text-[12px] font-semibold py-2.5 transition-colors text-muted hover:text-primary border-b-2 border-transparent';
      btn.style.borderColor = '';
      panel.classList.add('hidden');
    }
  });
  if (tab === 'balances') refreshWalletBalances();
  if (tab === 'send') _populateSendForm();
}

function copyWalletAddr() {
  const addr = document.getElementById('wallet-sol-addr').textContent;
  if (addr) navigator.clipboard.writeText(addr).then(() => showToast('Address copied', 'success'));
}

async function refreshWalletBalances() {
  const addrEl = document.getElementById('wallet-sol-addr');
  const solBalEl = document.getElementById('wallet-sol-balance');
  const tokenContainer = document.getElementById('wallet-token-balances');
  const noSolana = document.getElementById('wallet-no-solana');
  const balancesList = document.getElementById('wallet-balances-list');

  solBalEl.textContent = '...';
  tokenContainer.innerHTML = '<div class="text-[12px] text-muted text-center py-1">Loading...</div>';

  try {
    const resp = await fetch('/api/wallet-balances');
    const data = await resp.json();
    if (!data.success) {
      addrEl.textContent = '';
      noSolana.classList.remove('hidden');
      balancesList.classList.add('hidden');
      tokenContainer.innerHTML = '';
      return;
    }
    noSolana.classList.add('hidden');
    balancesList.classList.remove('hidden');
    addrEl.textContent = data.wallet_address || '';
    if (data.sol && data.sol.success) {
      solBalEl.textContent = parseFloat(data.sol.balance).toFixed(6);
    } else {
      solBalEl.textContent = 'Error';
    }
    tokenContainer.innerHTML = '';
    if (data.tokens) {
      for (const [name, info] of Object.entries(data.tokens)) {
        const bal = info.success ? (parseFloat(info.balance) > 0 ? parseFloat(info.balance).toLocaleString() : '0') : 'Error';
        const row = document.createElement('div');
        row.className = 'flex items-center justify-between px-3 py-2 bg-surface rounded-xl';
        row.innerHTML = `<span class="text-[12px] text-label">${esc(name)}</span><span class="text-[12px] text-muted font-mono">${esc(bal)}</span>`;
        tokenContainer.appendChild(row);
      }
    }
  } catch (e) {
    solBalEl.textContent = 'Error';
    tokenContainer.innerHTML = `<div class="text-[12px] text-accent-red text-center py-1">${esc(e.message)}</div>`;
  }
}

function _populateSendForm() {
  const select = document.getElementById('wallet-send-recipient');
  const tokenSelect = document.getElementById('wallet-send-token');
  const resultEl = document.getElementById('wallet-send-result');
  resultEl.classList.add('hidden');

  select.innerHTML = '<option value="">Select connected agent...</option>';
  if (_walletData && _walletData.connections) {
    _walletData.connections.forEach(c => {
      if (c.wallets && c.wallets.solana) {
        const opt = document.createElement('option');
        opt.value = c.agent_id;
        opt.textContent = `${c.display_name} (${c.wallets.solana.slice(0, 8)}...)`;
        select.appendChild(opt);
      }
    });
  }

  tokenSelect.innerHTML = '<option value="SOL">SOL</option>';
  fetch('/api/wallet-balances').then(r => r.json()).then(data => {
    if (data.known_tokens) {
      data.known_tokens.forEach(t => {
        const opt = document.createElement('option');
        opt.value = t;
        opt.textContent = t;
        tokenSelect.appendChild(opt);
      });
    }
  }).catch(() => {});
}

async function walletSend() {
  const agentId = document.getElementById('wallet-send-recipient').value;
  const amount = document.getElementById('wallet-send-amount').value;
  const token = document.getElementById('wallet-send-token').value;
  const btn = document.getElementById('wallet-send-btn');
  const resultEl = document.getElementById('wallet-send-result');

  if (!agentId) { showToast('Select a recipient', 'error'); return; }
  if (!amount || parseFloat(amount) <= 0) { showToast('Enter a valid amount', 'error'); return; }

  btn.disabled = true;
  btn.textContent = 'Sending...';
  resultEl.classList.add('hidden');

  try {
    const result = await ajaxPost('/api/wallet-send', { agent_id: agentId, amount: parseFloat(amount), token });
    if (result.success) {
      showToast(`Sent ${amount} ${token}`, 'success');
      resultEl.className = 'text-[12px] font-mono bg-accent-green/10 border border-accent-green/20 rounded-xl px-3 py-2 break-all text-accent-green';
      resultEl.textContent = `TX: ${result.tx_signature}`;
      resultEl.classList.remove('hidden');
      document.getElementById('wallet-send-amount').value = '';
    } else {
      showToast(result.error || 'Send failed', 'error');
      resultEl.className = 'text-[12px] font-mono bg-accent-red/10 border border-accent-red/20 rounded-xl px-3 py-2 break-all text-accent-red';
      resultEl.textContent = result.error || 'Unknown error';
      resultEl.classList.remove('hidden');
    }
  } catch (e) {
    showToast('Send failed', 'error');
  } finally {
    btn.disabled = false;
    btn.textContent = 'Send';
  }
}

function walletTab(tab) {
  const extTab = document.getElementById('wallet-tab-ext');
  const qrTab = document.getElementById('wallet-tab-qr');
  const extPanel = document.getElementById('wallet-ext-panel');
  const qrPanel = document.getElementById('wallet-qr-panel');
  if (tab === 'ext') {
    extTab.classList.add('bg-surface', 'text-primary');
    extTab.classList.remove('text-muted');
    qrTab.classList.remove('bg-surface', 'text-primary');
    qrTab.classList.add('text-muted');
    extPanel.classList.remove('hidden');
    qrPanel.classList.add('hidden');
  } else {
    qrTab.classList.add('bg-surface', 'text-primary');
    qrTab.classList.remove('text-muted');
    extTab.classList.remove('bg-surface', 'text-primary');
    extTab.classList.add('text-muted');
    qrPanel.classList.remove('hidden');
    extPanel.classList.add('hidden');
  }
}

function _getSolanaProvider() {
  if (window.phantom?.solana?.isPhantom) return window.phantom.solana;
  if (window.solana?.isPhantom) return window.solana;
  if (window.solflare?.isSolflare) return window.solflare;
  if (window.backpack?.solana) return window.backpack.solana;
  if (window.solana) return window.solana;
  return null;
}

function _bytesToHex(bytes) {
  return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}

async function walletAuth() {
  const provider = _getSolanaProvider();
  if (!provider) {
    showToast('No Solana wallet detected — install Phantom', 'error');
    return;
  }

  const btn = document.getElementById('wallet-connect-btn');
  btn.disabled = true;
  btn.textContent = 'Connecting...';

  try {
    const resp = await provider.connect();
    const publicKey = resp.publicKey || provider.publicKey;
    btn.textContent = 'Signing...';

    const challenge = "DarkMatter Identity Derivation v1";
    const encoded = new TextEncoder().encode(challenge);
    const { signature } = await provider.signMessage(encoded, "utf8");

    const remember = document.getElementById('wallet-remember').checked;
    const sigHex = _bytesToHex(signature);
    const pubHex = _bytesToHex(publicKey.toBytes());
    btn.textContent = 'Deriving identity...';

    const authResp = await fetch('/api/wallet-auth', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ public_key: pubHex, signature: sigHex, challenge, remember }),
    });
    const data = await authResp.json();

    if (data.ok) {
      const statusEl = document.getElementById('wallet-status');
      statusEl.textContent = 'Identity: ' + data.agent_id.slice(0, 12) + '...' + data.agent_id.slice(-6);
      statusEl.classList.remove('hidden');
      showToast('Wallet identity active', 'success');
      setTimeout(() => location.reload(), 1200);
    } else {
      showToast(data.error || 'Wallet auth failed', 'error');
      btn.disabled = false;
      btn.textContent = 'Connect Wallet';
    }
  } catch (e) {
    if (e.code === 4001 || e.message?.includes('rejected')) {
      showToast('Signature rejected', 'info');
    } else {
      showToast('Wallet error: ' + (e.message || e), 'error');
    }
    btn.disabled = false;
    btn.textContent = 'Connect Wallet';
  }
}

async function walletDisconnect() {
  try {
    const resp = await fetch('/api/wallet-disconnect', { method: 'POST' });
    const data = await resp.json();
    if (data.ok) {
      showToast('Reverted to original identity', 'success');
      setTimeout(() => location.reload(), 1000);
    } else {
      showToast('Failed to revert', 'error');
    }
  } catch (e) {
    showToast('Error: ' + e.message, 'error');
  }
}

async function startQRSession() {
  const remember = document.getElementById('wallet-qr-remember').checked;
  try {
    const resp = await fetch('/api/wallet-session', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ remember }),
    });
    const data = await resp.json();
    if (!data.ok) { showToast(data.error || 'Failed', 'error'); return; }

    document.getElementById('wallet-qr-idle').classList.add('hidden');
    const activeEl = document.getElementById('wallet-qr-active');
    activeEl.classList.remove('hidden');

    const qrContainer = document.getElementById('wallet-qr-code');
    qrContainer.innerHTML = '';
    const style = getComputedStyle(document.documentElement);
    new QRCode(qrContainer, {
      text: data.url, width: 160, height: 160,
      colorDark: style.getPropertyValue('--qr-fg').trim() || '#ffffff',
      colorLight: style.getPropertyValue('--qr-bg').trim() || '#1c1c1e',
      correctLevel: QRCode.CorrectLevel.M,
    });
    document.getElementById('wallet-qr-url').textContent = data.url;

    if (_walletQRPollTimer) clearInterval(_walletQRPollTimer);
    _walletQRPollTimer = setInterval(async () => {
      try {
        const pollResp = await fetch('/api/wallet-session/' + data.session_id);
        const pollData = await pollResp.json();
        if (pollData.status === 'applied') {
          clearInterval(_walletQRPollTimer);
          _walletQRPollTimer = null;
          activeEl.classList.add('hidden');
          document.getElementById('wallet-qr-done').classList.remove('hidden');
          const statusEl = document.getElementById('wallet-status');
          statusEl.textContent = 'Identity: ' + pollData.agent_id.slice(0, 12) + '...' + pollData.agent_id.slice(-6);
          statusEl.classList.remove('hidden');
          showToast('Wallet identity active', 'success');
          setTimeout(() => location.reload(), 1200);
        } else if (pollData.status === 'failed') {
          clearInterval(_walletQRPollTimer);
          _walletQRPollTimer = null;
          showToast(pollData.error || 'Signing failed', 'error');
          resetQRPanel();
        }
      } catch (e) { /* keep polling */ }
    }, 2000);
  } catch (e) {
    showToast('Error: ' + e.message, 'error');
  }
}

function resetQRPanel() {
  document.getElementById('wallet-qr-idle').classList.remove('hidden');
  document.getElementById('wallet-qr-active').classList.add('hidden');
  document.getElementById('wallet-qr-done').classList.add('hidden');
}

// Check wallet identity on load
fetch('/api/info').then(r => r.json()).then(data => {
  const walletBtn = document.getElementById('wallet-btn');
  if (data.wallet_identity) {
    if (walletBtn) walletBtn.title = 'Wallet identity active';
    document.getElementById('wallet-disconnect-btn').classList.remove('hidden');
    const statusEl = document.getElementById('wallet-status');
    statusEl.textContent = 'Active: ' + data.agent_id.slice(0, 12) + '...' + data.agent_id.slice(-6);
    statusEl.classList.remove('hidden');
  }
  if (data.wallets && data.wallets.solana) {
    const addr = data.wallets.solana;
    if (walletBtn) walletBtn.title = `Wallet: ${addr.slice(0, 6)}...${addr.slice(-4)}`;
  }
}).catch(() => {});

// ========================================================================
// MESH VIEW — D3.js force-directed graph
// ========================================================================

let _meshVisible = false;
let _meshSimulation = null;
let _meshPollTimer = null;
let _meshTooltipTarget = null;

function toggleMeshView() {
  if (_meshVisible) showChatView();
  else showMeshView();
}

function showMeshView() {
  _meshVisible = true;
  document.getElementById('chat-area').classList.add('hidden');
  document.getElementById('mesh-view').classList.remove('hidden');
  document.getElementById('mesh-btn').classList.add('text-accent-blue');
  document.getElementById('mesh-btn').classList.remove('text-muted');
  fetchMeshTopology();
  _meshPollTimer = setInterval(fetchMeshTopology, 15000);
}

function showChatView() {
  _meshVisible = false;
  document.getElementById('chat-area').classList.remove('hidden');
  document.getElementById('mesh-view').classList.add('hidden');
  document.getElementById('mesh-btn').classList.remove('text-accent-blue');
  document.getElementById('mesh-btn').classList.add('text-muted');
  if (_meshPollTimer) { clearInterval(_meshPollTimer); _meshPollTimer = null; }
  hideMeshTooltip();
}

async function fetchMeshTopology() {
  try {
    const resp = await fetch('/api/mesh-topology');
    if (resp.status === 401) { window.location.href = '/login'; return; }
    const topo = await resp.json();

    // Merge all nodes/edges from this node's view
    const nodesMap = {};
    const edgesSet = new Set();
    const edges = [];

    for (const n of topo.nodes) nodesMap[n.id] = n;
    for (const e of topo.edges) {
      const key = [e.source, e.target].sort().join('|');
      if (!edgesSet.has(key)) { edgesSet.add(key); edges.push({...e, is_local: true}); }
    }

    // Fan out to each peer's network_info (2s timeout, best-effort)
    const peerFetches = Object.entries(topo.peer_urls).map(async ([aid, url]) => {
      try {
        const ctrl = new AbortController();
        const timer = setTimeout(() => ctrl.abort(), 2000);
        const r = await fetch(url + '/__darkmatter__/network_info', {signal: ctrl.signal});
        clearTimeout(timer);
        if (!r.ok) return null;
        return await r.json();
      } catch { return null; }
    });

    const peerResults = await Promise.allSettled(peerFetches);
    for (const pr of peerResults) {
      if (pr.status !== 'fulfilled' || !pr.value) continue;
      const info = pr.value;
      // Add the node itself if not present
      if (!nodesMap[info.agent_id]) {
        nodesMap[info.agent_id] = {
          id: info.agent_id,
          display_name: info.display_name || info.agent_id.slice(0,8) + '...',
          bio: info.bio || '',
          is_self: false,
        };
      }
      // Add edges from this peer's connections
      for (const conn of (info.connections || [])) {
        if (!nodesMap[conn.agent_id]) {
          nodesMap[conn.agent_id] = {
            id: conn.agent_id,
            display_name: conn.display_name || conn.agent_id.slice(0,8) + '...',
            bio: conn.bio || '',
            is_self: false,
          };
        }
        const key = [info.agent_id, conn.agent_id].sort().join('|');
        if (!edgesSet.has(key)) {
          edgesSet.add(key);
          edges.push({source: info.agent_id, target: conn.agent_id, is_local: false});
        }
      }
    }

    const nodes = Object.values(nodesMap);
    document.getElementById('mesh-node-count').textContent = nodes.length + ' node' + (nodes.length !== 1 ? 's' : '');
    renderMeshGraph(nodes, edges, topo.self_id, topo.peer_urls);
  } catch (e) {
    console.error('Mesh topology fetch failed:', e);
  }
}

function renderMeshGraph(nodes, edges, selfId, peerUrls) {
  const container = document.getElementById('mesh-graph');
  const width = container.clientWidth;
  const height = container.clientHeight;
  if (width === 0 || height === 0) return;

  const isLight = document.documentElement.getAttribute('data-theme') === 'light';
  const accentBlue = isLight ? '#007AFF' : '#0A84FF';
  const textColor = isLight ? '#000' : '#fff';
  const mutedColor = isLight ? '#8e8e93' : '#636366';
  const nodeColor = isLight ? '#c7c7cc' : '#48484a';
  const bgColor = isLight ? '#f5f5f7' : '#000000';

  // Clear previous
  container.innerHTML = '';

  const svg = d3.select(container).append('svg')
    .attr('width', width).attr('height', height)
    .style('background', bgColor);

  const g = svg.append('g');

  // Zoom
  const zoom = d3.zoom().scaleExtent([0.3, 4]).on('zoom', (e) => g.attr('transform', e.transform));
  svg.call(zoom);

  // Build link/node data (deep copy ids for D3)
  const linkData = edges.map(e => ({source: e.source, target: e.target, is_local: e.is_local}));
  const nodeData = nodes.map(n => ({...n}));

  // Simulation
  const sim = d3.forceSimulation(nodeData)
    .force('link', d3.forceLink(linkData).id(d => d.id).distance(100))
    .force('charge', d3.forceManyBody().strength(-300))
    .force('center', d3.forceCenter(width / 2, height / 2))
    .force('collision', d3.forceCollide().radius(30));

  _meshSimulation = sim;

  // Links
  const link = g.append('g').selectAll('line')
    .data(linkData).enter().append('line')
    .attr('stroke', mutedColor)
    .attr('stroke-opacity', 0.4)
    .attr('stroke-width', 1.5)
    .attr('stroke-dasharray', d => d.is_local ? 'none' : '4,4');

  // Nodes
  const node = g.append('g').selectAll('g')
    .data(nodeData).enter().append('g')
    .call(d3.drag()
      .on('start', (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
      .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
      .on('end', (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; })
    );

  // Self highlight ring
  node.filter(d => d.id === selfId)
    .append('circle').attr('r', 18)
    .attr('fill', 'none').attr('stroke', accentBlue).attr('stroke-width', 2).attr('stroke-opacity', 0.5);

  // Node circles
  node.append('circle')
    .attr('r', 12)
    .attr('fill', d => d.id === selfId ? accentBlue : nodeColor)
    .attr('stroke', d => d.id === selfId ? accentBlue : 'transparent')
    .attr('stroke-width', 1.5)
    .style('cursor', 'pointer');

  // Labels
  node.append('text')
    .text(d => d.display_name || d.id.slice(0, 8))
    .attr('dy', 24).attr('text-anchor', 'middle')
    .attr('fill', d => d.id === selfId ? accentBlue : textColor)
    .attr('font-size', '11px').attr('font-weight', d => d.id === selfId ? '600' : '400')
    .attr('opacity', 0.8);

  // Click handler for tooltip
  node.on('click', (event, d) => {
    event.stopPropagation();
    showMeshTooltip(event, d, selfId, peerUrls);
  });
  svg.on('click', () => hideMeshTooltip());

  sim.on('tick', () => {
    link
      .attr('x1', d => d.source.x).attr('y1', d => d.source.y)
      .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
    node.attr('transform', d => `translate(${d.x},${d.y})`);
  });
}

function showMeshTooltip(event, d, selfId, peerUrls) {
  const tip = document.getElementById('mesh-tooltip');
  document.getElementById('tooltip-name').textContent = d.display_name || 'Unknown';
  document.getElementById('tooltip-id').textContent = d.id.slice(0, 16) + '...' + d.id.slice(-8);
  document.getElementById('tooltip-bio').textContent = d.bio || '';
  const lvl = d.connectivity_level ? `L${d.connectivity_level}: ${d.connectivity_method}` : '';
  document.getElementById('tooltip-level').textContent = lvl;

  const connectBtn = document.getElementById('tooltip-connect-btn');
  // Show connect button if this node is not self and not already in our connections
  const isConnected = !!peerUrls[d.id];
  if (d.id !== selfId && !isConnected && peerUrls[d.id] === undefined) {
    // Discovered through a peer — we have no direct URL, can't connect yet
    connectBtn.classList.add('hidden');
  } else if (d.id !== selfId && !isConnected) {
    connectBtn.classList.remove('hidden');
  } else {
    connectBtn.classList.add('hidden');
  }
  _meshTooltipTarget = d;

  tip.classList.remove('hidden');
  tip.style.left = Math.min(event.pageX + 12, window.innerWidth - 260) + 'px';
  tip.style.top = Math.min(event.pageY - 10, window.innerHeight - 200) + 'px';
}

function hideMeshTooltip() {
  document.getElementById('mesh-tooltip').classList.add('hidden');
  _meshTooltipTarget = null;
}

async function connectFromTooltip() {
  if (!_meshTooltipTarget) return;
  const aid = _meshTooltipTarget.id;
  hideMeshTooltip();
  try {
    // Find the peer URL from mesh topology data
    const topoResp = await fetch('/api/mesh-topology');
    const topo = await topoResp.json();
    const url = topo.peer_urls[aid];
    if (!url) { showToast('No URL available for this agent', 'error'); return; }
    const resp = await fetch('/connect', {
      method: 'POST',
      headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'},
      body: JSON.stringify({url: url}),
    });
    const data = await resp.json();
    if (data.success) showToast('Connection request sent', 'success');
    else showToast(data.error || 'Connection failed', 'error');
  } catch (e) {
    showToast('Connection failed: ' + e.message, 'error');
  }
}

function onThemeChange() {
  if (_meshVisible) fetchMeshTopology();
}
</script>

{% endblock %}
