{% extends "base.html" %}
{% from 'partials/toggle.html' import toggle, toggle_widget %}

{% block content %}
<!-- Toast Container -->
<div id="toast-container" class="fixed top-5 right-5 z-[100] flex flex-col gap-2"></div>
<style>
footer { display: none; }
main { align-items: flex-start !important; }
/* Dark mode: chat area */
html.dark #agent-state-card { background: #1f2937; border-color: #374151; }
html.dark #state-busy-dot { background: #451a03 !important; color: #fcd34d !important; }
html.dark #mobile-state-busy-dot { background: #451a03 !important; color: #fcd34d !important; }
html.dark #agent-state-card .bg-green-100 { background: #052e16 !important; color: #86efac !important; }
html.dark #agent-state-card .bg-amber-100 { background: #451a03 !important; color: #fcd34d !important; }
html.dark #agent-state-card .bg-blue-100 { background: #172554 !important; color: #93c5fd !important; }
html.dark #agent-state-card .bg-purple-100 { background: #2e1065 !important; color: #c4b5fd !important; }
html.dark #agent-state-card .bg-gray-100 { background: #374151 !important; color: #e5e7eb !important; }
html.dark #agent-state-card .text-gray-500 { color: #9ca3af !important; }
html.dark #agent-state-card .text-gray-700 { color: #e5e7eb !important; }
html.dark #agent-state-card .text-gray-400 { color: #6b7280 !important; }
html.dark #agent-state-card .text-gray-600 { color: #d1d5db !important; }
html.dark #agent-state-card .border-gray-100 { border-color: #374151 !important; }
html.dark #summary-card { background: #1f2937; border-color: #374151; }
/* Dark mode: Channels tab (channel type badges) */
html.dark #channels-list .bg-blue-100 { background: #172554 !important; color: #93c5fd !important; }
html.dark #channels-list .bg-green-100 { background: #052e16 !important; color: #86efac !important; }
html.dark #channels-list .bg-purple-100 { background: #2e1065 !important; color: #c4b5fd !important; }
/* Dark mode: Channels tab (channel list cards) */
html.dark #channels-list .bg-gray-50 { background: #1f2937 !important; }
html.dark #channels-list .border-gray-100 { border-color: #374151 !important; }
html.dark #channels-list .text-gray-800 { color: #f3f4f6 !important; }
html.dark #channels-list .text-gray-400 { color: #9ca3af !important; }
html.dark #channels-list .bg-red-50 { background: #451a1a !important; }
html.dark #channels-list .text-red-600 { color: #fca5a5 !important; }
html.dark #channels-list .hover\:bg-red-100:hover { background: #7f1d1d !important; }
html.dark #channels-list .bg-green-50 { background: #14532d !important; }
html.dark #channels-list .text-green-600 { color: #4ade80 !important; }
html.dark #channels-list .hover\:bg-green-100:hover { background: #166534 !important; }
html.dark #channels-list .bg-yellow-50 { background: #451a03 !important; }
html.dark #channels-list .text-yellow-600 { color: #fcd34d !important; }
html.dark #channels-list .hover\:bg-yellow-100:hover { background: #78350f !important; }
html.dark #channels-list .bg-gray-100 { background: #374151 !important; }
html.dark #channels-list .text-gray-600 { color: #d1d5db !important; }
html.dark #channels-list .hover\:bg-gray-200:hover { background: #4b5563 !important; }
html.dark #channels-list .text-red-500 { color: #f87171 !important; }
html.dark #channels-list .hover\:text-red-700:hover { color: #fca5a5 !important; }
/* Dark mode: Channels tab (header) */
html.dark #tab-channels .text-gray-800 { color: #f3f4f6 !important; }
/* Dark mode: Compiled Prompt Modal (LLM Request tab) */
html.dark #compiled-prompt-modal > div { background: #1f2937 !important; }
html.dark #compiled-prompt-modal .border-gray-200 { border-color: #374151 !important; }
html.dark #compiled-prompt-modal .text-gray-700 { color: #f3f4f6 !important; }
html.dark #compiled-prompt-modal .text-gray-800 { color: #f3f4f6 !important; }
html.dark #compiled-prompt-modal .text-gray-400 { color: #9ca3af !important; }
html.dark #compiled-prompt-modal .text-gray-500 { color: #9ca3af !important; }
html.dark #compiled-prompt-modal .text-gray-600 { color: #d1d5db !important; }
html.dark #compiled-prompt-modal .bg-gray-50 { background: #374151 !important; }
html.dark #compiled-prompt-modal .bg-purple-50 { background: #2e1065 !important; }
html.dark #compiled-prompt-modal .bg-blue-50 { background: #172554 !important; }
html.dark #compiled-prompt-modal .bg-green-50 { background: #052e16 !important; }
html.dark #compiled-prompt-modal .bg-amber-50 { background: #451a03 !important; }
html.dark #compiled-prompt-modal .border-purple-200 { border-color: #5b21b6 !important; }
html.dark #compiled-prompt-modal .border-amber-200 { border-color: #92400e !important; }
html.dark #compiled-prompt-modal .border-l-purple-400 { border-color: #a78bfa !important; }
html.dark #compiled-prompt-modal .border-l-blue-400 { border-color: #60a5fa !important; }
html.dark #compiled-prompt-modal .border-l-green-400 { border-color: #4ade80 !important; }
html.dark #compiled-prompt-modal .border-l-amber-400 { border-color: #fbbf24 !important; }
html.dark #compiled-prompt-modal .text-purple-600 { color: #c4b5fd !important; }
html.dark #compiled-prompt-modal .text-blue-600 { color: #93c5fd !important; }
html.dark #compiled-prompt-modal .text-green-600 { color: #86efac !important; }
html.dark #compiled-prompt-modal .text-amber-600 { color: #fcd34d !important; }
html.dark #compiled-prompt-modal pre { background: #1e293b !important; color: #e2e8f0 !important; }
html.dark #compiled-prompt-modal .bg-amber-100 { background: #451a03 !important; }
html.dark #compiled-prompt-modal .text-amber-700 { color: #fcd34d !important; }
html.dark #compiled-prompt-modal .tab-btn { color: #9ca3af !important; }
html.dark #compiled-prompt-modal .tab-btn:hover { color: #f3f4f6 !important; }
/* Chevron rotation for collapsible sections */
#compiled-prompt-modal .chevron { display: inline-block; transition: transform 0.2s ease; }
#compiled-prompt-modal .chevron.rotated { transform: rotate(90deg); }
/* Dark mode: Skills tab (selected skill badges) */
html.dark #tab-skills .bg-white { background: #1f2937 !important; }
html.dark #tab-skills .border-gray-200 { border-color: #374151 !important; }
html.dark #tab-skills .text-gray-800 { color: #f3f4f6 !important; }
html.dark #tab-skills .text-gray-400 { color: #9ca3af !important; }
html.dark #tab-skills .text-gray-500 { color: #9ca3af !important; }
html.dark #tab-skills .bg-gray-100 { background: #374151 !important; }
html.dark #tab-skills .bg-indigo-50 { background: #1e1b4b !important; }
html.dark #tab-skills .border-indigo-200 { border-color: #4338ca !important; }
html.dark #tab-skills .text-indigo-500 { color: #a5b4fc !important; }
html.dark #tab-skills .bg-green-50 { background: #052e16 !important; }
html.dark #tab-skills .text-green-600 { color: #4ade80 !important; }
html.dark #tab-skills .bg-amber-50 { background: #451a03 !important; }
html.dark #tab-skills .border-amber-200 { border-color: #92400e !important; }
html.dark #tab-skills .text-amber-800 { color: #fcd34d !important; }
html.dark #tab-skills .text-amber-600 { color: #fbbf24 !important; }
html.dark #tab-skills .text-amber-500 { color: #fbbf24 !important; }
/* Dark mode: Artifacts tab */
html.dark #tab-artifacts .bg-white { background: #1f2937 !important; }
html.dark #tab-artifacts .border-gray-200 { border-color: #374151 !important; }
html.dark #tab-artifacts .text-gray-800 { color: #f3f4f6 !important; }
html.dark #tab-artifacts .text-gray-400 { color: #9ca3af !important; }
html.dark #tab-artifacts .text-gray-500 { color: #9ca3af !important; }
html.dark #tab-artifacts .hover\:bg-gray-100:hover { background: #374151 !important; }
html.dark #tab-artifacts .bg-gray-50 { background: #374151 !important; }
html.dark #tab-artifacts .text-gray-600 { color: #d1d5db !important; }
html.dark #tab-artifacts .text-red-500 { color: #f87171 !important; }
html.dark #tab-artifacts .hover\:text-red-700:hover { color: #fca5a5 !important; }
html.dark #tab-artifacts .bg-red-50 { background: #451a1a !important; }
/* Dark mode: Artifact viewer modal */
html.dark #artifact-viewer-modal > div { background: #1f2937; }
html.dark #artifact-viewer-modal .border-gray-200 { border-color: #374151; }
html.dark #artifact-viewer-modal .border-gray-100 { border-color: #374151; }
html.dark #artifact-viewer-modal .text-gray-800 { color: #f3f4f6; }
html.dark #artifact-viewer-modal .text-gray-700 { color: #e5e7eb; }
html.dark #artifact-viewer-modal .text-gray-600 { color: #d1d5db; }
html.dark #artifact-viewer-modal .text-gray-500 { color: #9ca3af; }
html.dark #artifact-viewer-modal .text-gray-400 { color: #6b7280; }
html.dark #artifact-viewer-modal .bg-gray-50 { background: #374151; }
html.dark #artifact-viewer-modal .bg-gray-100 { background: #4b5563; }
html.dark #artifact-viewer-modal .bg-white { background: #1f2937; }
html.dark #artifact-viewer-modal .hover\\:bg-gray-100:hover { background: #4b5563; }
/* Dark mode: Channel detail modal */
html.dark #channel-detail-modal .bg-gray-50 { background: #374151 !important; }
html.dark #channel-detail-modal .border-gray-100 { border-color: #4b5563 !important; }
html.dark #channel-detail-modal .text-gray-700 { color: #e5e7eb !important; }
html.dark #channel-detail-modal .text-gray-800 { color: #f3f4f6 !important; }
html.dark #channel-detail-modal .text-gray-500 { color: #9ca3af !important; }
html.dark #channel-detail-modal .text-gray-400 { color: #6b7280 !important; }
html.dark #channel-detail-modal .bg-amber-50 { background: #451a03 !important; }
html.dark #channel-detail-modal .border-amber-200 { border-color: #92400e !important; }
html.dark #channel-detail-modal .text-amber-700 { color: #fcd34d !important; }
html.dark #channel-detail-modal .bg-amber-100 { background: #451a03 !important; }
html.dark #channel-detail-modal .bg-indigo-50 { background: #1e1b4b !important; }
html.dark #channel-detail-modal .text-indigo-600 { color: #a5b4fc !important; }
html.dark #channel-detail-modal .hover\:bg-indigo-100:hover { background: #312e81 !important; }
</style>
<div class="px-2 py-1 md:py-4 flex-1 min-h-0 flex flex-col h-[calc(100dvh-var(--header-h,56px))]">
    {% if workspace_invalid %}
    <div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg px-4 py-3 mb-4 flex-shrink-0">
        <div class="flex items-start gap-3">
            <svg class="w-5 h-5 text-amber-500 dark:text-amber-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M10.29 3.86l-8.6 14.86A1 1 0 002.56 20h18.88a1 1 0 00.87-1.5l-8.6-14.86a1 1 0 00-1.74 0z"/>
            </svg>
            <div>
                <p class="text-sm font-medium text-amber-800 dark:text-amber-300">Workspace directory is not available</p>
                <p class="text-sm text-amber-700 dark:text-amber-400 mt-1">
                    The configured workspace path <code class="bg-amber-100 dark:bg-amber-800/50 px-1.5 py-0.5 rounded text-xs font-mono">{{ workspace_path }}</code>
                    does not exist or is not a valid directory on the host.
                    File-based tool operations (read_file, write_file, etc.) will fail until this is corrected.
		    Please fix in Agent's Settings.
                </p>
            </div>
        </div>
    </div>
    {% endif %}
    <!-- Header -->
    <div class="flex items-center justify-between mb-4 flex-shrink-0">
        <div class="flex items-center gap-3">
            <a href="/agents" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
            </a>
            <h2 class="text-xl font-bold text-gray-800 dark:text-gray-100">{{ agent.name }}</h2>
            {% if not agent.is_super %}
            <span class="text-xs text-gray-400 bg-gray-100 dark:bg-gray-700 dark:text-gray-400 px-2 py-0.5 rounded font-mono">{{ agent.id }}</span>
            {% endif %}
        </div>
        {% if not agent.is_super %}
        <div class="relative" id="gear-dropdown-wrap">
            <button onclick="toggleGearDropdown()" class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-md transition-colors" title="Agent actions" id="gear-btn">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
            </button>
            <div id="gear-dropdown" class="hidden absolute right-0 top-full mt-1 w-44 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg z-50 py-1">
                <button onclick="cloneAgent()" class="w-full text-left flex items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors">
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
                    Clone
                </button>
                <button onclick="toggleEnabled()" id="toggle-enabled-btn" class="w-full text-left flex items-center gap-2 px-4 py-2 text-sm {{ 'text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600' if agent.enabled != 0 else 'text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-300' }} transition-colors">
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4"><path d="M5.636 5.636a9 9 0 1 0 12.728 0M12 3v9"/></svg>
                    <span>{{ 'Disable' if agent.enabled != 0 else 'Enable' }}</span>
                </button>
                <button onclick="deleteAgent()" class="w-full text-left flex items-center gap-2 px-4 py-2 text-sm text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors">
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4"><path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 0 0 6 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 1 0 .23 1.482l.149-.022.841 10.518A2.75 2.75 0 0 0 7.596 19h4.807a2.75 2.75 0 0 0 2.742-2.53l.841-10.52.149.023a.75.75 0 0 0 .23-1.482A41.03 41.03 0 0 0 14 4.193V3.75A2.75 2.75 0 0 0 11.25 1h-2.5ZM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4ZM8.58 7.72a.75.75 0 0 0-1.5.06l.3 7.5a.75.75 0 1 0 1.5-.06l-.3-7.5Zm4.34.06a.75.75 0 1 0-1.5-.06l-.3 7.5a.75.75 0 1 0 1.5.06l.3-7.5Z" clip-rule="evenodd"/></svg>
                    Delete
                </button>
            </div>
        </div>
        {% endif %}
    </div>
    <!-- Tabs -->
    <div class="flex gap-1 mb-6 border-b border-gray-200 dark:border-gray-700 overflow-x-auto flex-shrink-0" style="-webkit-overflow-scrolling: touch;">
        <button class="tab-btn flex-shrink-0 whitespace-nowrap px-4 py-2.5 text-sm font-medium border-b-2 border-indigo-500 text-indigo-600" onclick="showTab('chat')" data-tab="chat">Chat</button>
        <button class="tab-btn flex-shrink-0 whitespace-nowrap px-4 py-2.5 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700" onclick="showTab('general')" data-tab="general">Settings</button>
        <button class="tab-btn flex-shrink-0 whitespace-nowrap px-4 py-2.5 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700" onclick="showTab('knowledge')" data-tab="knowledge">Knowledge</button>
        <button class="tab-btn flex-shrink-0 whitespace-nowrap px-4 py-2.5 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700" onclick="showTab('tools')" data-tab="tools">Tools</button>
        {% if not agent.is_super %}
        <button class="tab-btn flex-shrink-0 whitespace-nowrap px-4 py-2.5 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700" onclick="showTab('skills')" data-tab="skills">Skills</button>
        {% endif %}
        <button class="tab-btn flex-shrink-0 whitespace-nowrap px-4 py-2.5 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700" onclick="showTab('channels')" data-tab="channels">Channels</button>
        <button class="tab-btn flex-shrink-0 whitespace-nowrap px-4 py-2.5 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700" onclick="showTab('portals')" data-tab="portals">Portals</button>
        {% if agent.artifacts_enabled %}
        <button class="tab-btn flex-shrink-0 whitespace-nowrap px-4 py-2.5 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700" onclick="showTab('artifacts')" data-tab="artifacts">Artifacts</button>
        {% endif %}
    </div>

    <!-- Settings Tab -->
    <div id="tab-general" class="tab-content hidden flex-1 min-h-0 overflow-y-auto">
        <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
        <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
            <div class="mb-4">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Name</label>
                <input type="text" id="agent-name" value="{{ agent.name }}" class="w-full p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
            </div>
            <div class="mb-4">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Description</label>
                <textarea id="agent-desc" rows="2" class="w-full p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">{{ agent.description or '' }}</textarea>
            </div>
            <div class="mb-4">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Avatar</label>
                <div class="flex items-center gap-3">
                    <img id="avatar-preview" src="/api/agents/{{ agent.id }}/avatar" alt="Avatar preview" class="w-12 h-12 rounded-full object-cover bg-indigo-50 dark:bg-indigo-900/20" onerror="this.onerror=null;this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 40 40%22 fill=%22none%22><rect width=%2240%22 height=%2240%22 rx=%2220%22 fill=%22%23e0e7ff%22/><path d=%22M20 8a5 5 0 100 10 5 5 0 000-10zm-8 18.5a8 8 0 0116 0%22 fill=%22%234f46e5%22/></svg>'">
                    <div class="flex flex-col gap-1">
                        <input type="file" id="avatar-input" accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml" class="text-sm text-gray-500 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-indigo-50 file:text-indigo-600 hover:file:bg-indigo-100">
                        <button onclick="removeAvatar()" class="text-xs text-red-500 hover:text-red-700 text-left">Remove avatar</button>
                    </div>
                </div>
                <p class="text-xs text-gray-400 mt-1">Upload a PNG, JPEG, GIF, WebP, or SVG image. Recommended: square aspect ratio.</p>
            </div>
            <div class="mb-4">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Model</label>
                <select id="agent-model-select" class="w-full p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
                    <option value="">-- Use global default --</option>
                </select>
                <p class="text-xs text-gray-400 mt-1">Select a model configured in Settings &gt; Models. "Use global default" uses whichever model is marked as default.</p>
            </div>
            <div class="mb-4">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Fallback Model</label>
                <select id="agent-fallback-model-select" class="w-full p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
                    <option value="">-- None (no fallback) --</option>
                </select>
                <p class="text-xs text-gray-400 mt-1">If the primary model fails after all retries, the agent will automatically attempt this fallback model. Leave empty to skip fallback.</p>
            </div>
            <!-- Workplace Selector -->
            <div class="mb-4">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Workplace</label>
                <div class="flex items-center gap-2">
                    <select id="agent-workplace-id" class="flex-1 p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
                        <option value="">-- None (use workspace below) --</option>
                    </select>
                    <span id="workplace-status-indicator" class="flex-shrink-0 inline-block w-2.5 h-2.5 rounded-full bg-gray-300" title="Workplace status"></span>
                </div>
                <p class="text-xs text-gray-400 mt-1">Assign an execution environment. <a href="/workplaces" class="text-indigo-500 hover:underline">Manage Workplaces</a></p>
            </div>

            <div id="agent-workspace-row" class="mb-4">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Workspace Directory</label>
                <input type="text" id="agent-workspace" value="{{ agent.workspace or '' }}" placeholder="Leave empty to use default workspace" class="w-full p-2.5 border border-gray-300 rounded-md text-sm font-mono focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10">
                <p class="text-xs text-gray-400 mt-1">Absolute path on the host used as <code>/workspace</code> in the sandbox. Leave empty to use the default global workspace. Ignored when a Workplace is assigned.</p>
            </div>
            <div id="sandbox-section" class="mb-4">
                <label class="flex items-center gap-3 cursor-pointer select-none" id="sandbox-label">
                    {{ toggle_widget(id='agent-sandbox', checked=(agent.sandbox_enabled is none or agent.sandbox_enabled)) }}
                    <div>
                        <span class="text-gray-600 dark:text-gray-300 text-sm font-medium">Docker Sandbox</span>
                        <p class="text-xs text-gray-400 mt-0.5">Run code tools (Python, Bash) inside a Docker container. Disable to run directly on the host.</p>
                    </div>
                </label>
                <div id="sandbox-warning" class="hidden mt-1.5 text-xs text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded px-2.5 py-1.5">⚠ Docker Sandbox only available for local workplace.</div>
            </div>
            <div class="mb-4">
                <label class="flex items-center gap-3 cursor-pointer select-none">
                    {{ toggle_widget(id='agent-safety-checker', checked=(agent.safety_checker_enabled is none or agent.safety_checker_enabled)) }}
                    <div>
                        <span class="text-gray-600 dark:text-gray-300 text-sm font-medium">Safety Checker</span>
                        <p class="text-xs text-gray-400 mt-0.5">Heuristic safety check on Bash/Python scripts. Disable for full autopilot — agent runs all code without approval prompts.</p>
                    </div>
                </label>
            </div>
            <!-- HMADS: Agent-specific safety rules -->
            <div class="mb-4" id="agent-safety-rules-section">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Safety Rules (Specific)</label>
                <p class="text-xs text-gray-400 mb-2">Assign specific-scope HMADS rules to this agent. Global rules always apply. <a href="/system#hmads" class="text-indigo-500 hover:underline">Manage rules</a></p>
                <div id="agent-safety-rules-list" class="space-y-1 max-h-40 overflow-y-auto border border-gray-200 dark:border-gray-600 rounded-md p-2 bg-gray-50 dark:bg-gray-700/50">
                    <div class="text-xs text-gray-400">Loading...</div>
                </div>
            </div>
            <div class="mb-4">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">System Prompt</label>
                <textarea id="agent-system-prompt" rows="10" placeholder="Define the agent's persona and behavior..." class="w-full p-2.5 border border-gray-300 rounded-md text-sm font-mono focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10">{{ agent.system_prompt or '' }}</textarea>
            </div>
            <div class="mb-4">
                <label class="flex items-center gap-3 cursor-pointer select-none">
                    {{ toggle_widget(id='agent-vision', checked=agent.vision_enabled) }}
                    <div>
                        <span class="text-gray-600 dark:text-gray-300 text-sm font-medium">Vision (Image Recognition)</span>
                        <p class="text-xs text-gray-400 mt-0.5">Allow agent to receive and analyze images from channel (Telegram/WA/Discord)</p>
                    </div>
                </label>
            </div>
            <div class="mb-4">
                <label class="flex items-center gap-3 cursor-pointer select-none">
                    {{ toggle_widget(id='agent-attachments', checked=agent.attachments_enabled) }}
                    <div>
                        <span class="text-gray-600 dark:text-gray-300 text-sm font-medium">Attachments (Files)</span>
                        <p class="text-xs text-gray-400 mt-0.5">Allow agent to receive non-image files (PDF, audio, video, etc.) from channels. Files are stored for 7 days and read on demand via the <code>read_attachment</code> tool. Requires a model with <code>attachments_supported</code>.</p>
                    </div>
                </label>
                <div class="mt-2 flex items-center gap-2">
                    <label for="agent-attachment-max-mb" class="text-xs text-gray-500 dark:text-gray-400">Max file size (MB):</label>
                    <input type="number" id="agent-attachment-max-mb" min="1" max="20" value="{{ agent.attachment_max_size_mb if agent.attachment_max_size_mb is not none else 20 }}" class="w-20 p-1.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10">
                    <span class="text-xs text-gray-400">Telegram hard-caps at 20 MB.</span>
                </div>
            </div>
            <div class="mb-4">
                <label class="flex items-center gap-3 cursor-pointer select-none">
                    {{ toggle_widget(id='agent-inject-id', checked=agent.inject_agent_id) }}
                    <div>
                        <span class="text-gray-600 dark:text-gray-300 text-sm font-medium">Inject Agent ID</span>
                        <p class="text-xs text-gray-400 mt-0.5">Prepend this agent's ID to its system prompt so the agent knows its own identity</p>
                    </div>
                </label>
            </div>
            <div class="mb-4">
                <label class="flex items-center gap-3 cursor-pointer select-none">
                    {{ toggle_widget(id='agent-inject-datetime', checked=agent.inject_datetime) }}
                    <div>
                        <span class="text-gray-600 dark:text-gray-300 text-sm font-medium">Inject Date/Time</span>
                        <p class="text-xs text-gray-400 mt-0.5">Append current date/time (WIB/UTC+7) to system prompt. Use <code>{{day}}</code>, <code>{{date}}</code>, <code>{{time}}</code> in SYSTEM.md for inline placement.</p>
                    </div>
                </label>
            </div>
            <button id="saveGeneralBtn" onclick="saveGeneral()" class="bg-indigo-500 hover:bg-indigo-600 text-white px-5 py-2 rounded-md text-sm font-medium transition-colors disabled:opacity-60 disabled:cursor-not-allowed">Save Changes</button>
            <span id="save-status" class="ml-3 text-sm text-green-600 hidden">Saved!</span>
        </div>
        <!-- Right column -->
        <div class="space-y-6">
            <!-- Advanced Settings -->
            <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 overflow-hidden">
                <button type="button" onclick="toggleAdvanced()" class="w-full flex justify-between items-center px-6 py-4 text-sm font-semibold dark:text-gray-200 text-gray-700 hover:text-gray-900 dark:hover:text-gray-50 transition-colors">
                    <span>Advanced Settings</span>
                    <span id="adv-arrow" class="text-xs transition-transform">&#9654;</span>
                </button>
                <div id="advanced-section" class="hidden px-6 pb-6 space-y-4 border-t border-gray-100">
                    <div class="pt-4">
                        <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Summarize Threshold</label>
                        <input type="number" id="agent-summarize-threshold" min="0" value="{{ agent.summarize_threshold if agent.summarize_threshold is not none else 3 }}" class="w-32 p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10">
                        <p class="text-xs text-gray-400 mt-1">Minimum messages before conversation history is summarized. Set to 0 to disable.</p>
                    </div>
                    <div>
                        <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Raw Tail Size</label>
                        <input type="number" id="agent-summarize-tail" min="1" value="{{ agent.summarize_tail if agent.summarize_tail is not none else 5 }}" class="w-32 p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10">
                        <p class="text-xs text-gray-400 mt-1">Number of recent messages kept in raw form (not summarized).</p>
                    </div>
                    <div>
                        <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Message Buffer (seconds)</label>
                        <input type="number" id="agent-buffer-seconds" min="0" step="0.5" value="{{ agent.message_buffer_seconds if agent.message_buffer_seconds is not none else 2 }}" class="w-32 p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10">
                        <p class="text-xs text-gray-400 mt-1">Wait time to accumulate rapid messages before calling LLM. Set to 0 to disable.</p>
                    </div>
                    <div>
                        <label class="flex items-center gap-3 cursor-pointer select-none">
                            {{ toggle_widget(id='agent-send-intermediate', checked=agent.send_intermediate_responses) }}
                            <div>
                                <span class="text-gray-600 dark:text-gray-300 text-sm font-medium">Send Intermediate Responses</span>
                                <p class="text-xs text-gray-400 mt-0.5">Forward intermediate tool-call text to channels (e.g. Telegram) during processing. Default: off.</p>
                            </div>
                        </label>
                    </div>
                    <div>
                        <label class="flex items-center gap-3 cursor-pointer select-none">
                            {{ toggle_widget(id='agent-state-enabled', checked=agent.enable_agent_state) }}
                            <div>
                                <span class="text-gray-600 dark:text-gray-300 text-sm font-medium">Agent State</span>
                                <p class="text-xs text-gray-400 mt-0.5">Enable plan/execute mode tracking. Write tools are blocked until the agent presents a plan and user approves.</p>
                            </div>
                        </label>
                    </div>
                    <div>
                        <label class="flex items-center gap-3 cursor-pointer select-none">
                            {{ toggle_widget(id='agent-parallel-exec', checked=(agent.disable_parallel_tool_execution if agent.disable_parallel_tool_execution is not none else False)) }}
                            <div>
                                <span class="text-gray-600 dark:text-gray-300 text-sm font-medium">Disable Parallel Tool Execution</span>
                                <p class="text-xs text-gray-400 mt-0.5">Prevent read-only tools from running concurrently. Enable this only if you experience issues with tool ordering or race conditions.</p>
                            </div>
                        </label>
                    </div>
                    <div>
                        <label class="flex items-center gap-3 cursor-pointer select-none">
                            {{ toggle_widget(id='agent-prefetch', checked=(agent.disable_turn_prefetch if agent.disable_turn_prefetch is not none else False)) }}
                            <div>
                                <span class="text-gray-600 dark:text-gray-300 text-sm font-medium">Disable Turn Prefetch</span>
                                <p class="text-xs text-gray-400 mt-0.5">Stop background pre-loading of conversation context between turns. Enable this to reduce DB I/O or if you suspect stale-context issues.</p>
                            </div>
                        </label>
                    </div>
                    <div>
                        <label class="flex items-center gap-3 cursor-pointer select-none">
                            {{ toggle_widget(id='agent-messaging-enabled', checked=agent.agent_messaging_enabled) }}
                            <div>
                                <span class="text-gray-600 dark:text-gray-300 text-sm font-medium">Agent Messaging</span>
                                <p class="text-xs text-gray-400 mt-0.5">Allow this agent to send messages to other agents. Enables <code>send_agent_message</code>, <code>escalate_to_user</code>, and <code>resolve_agent_approval</code> tools automatically.</p>
                            </div>
                        </label>
                    </div>

                    <div>
                        <label class="flex items-center gap-3 cursor-pointer select-none">
                            {{ toggle_widget(id='agent-artifacts', checked=agent.artifacts_enabled) }}
                            <div>
                                <span class="text-gray-600 dark:text-gray-300 text-sm font-medium">Artifacts (Saved Files)</span>
                                <p class="text-xs text-gray-400 mt-0.5">Allow agent to save files (reports, analysis, images, etc.) to its artifacts directory. When enabled, the agent's SYSTEM.md is updated with instructions to use the <code>save_artifact</code> tool. Files appear in the Artifacts tab.</p>
                            </div>
                        </label>
                    </div>

                    <div>
                        <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Summarize Prompt</label>
                        <textarea id="agent-summarize-prompt" rows="10" class="w-full p-2.5 border border-gray-300 rounded-md text-sm font-mono focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10">{{ agent.summarize_prompt or DEFAULT_SUMMARIZE_PROMPT }}</textarea>
                        <p class="text-xs text-gray-400 mt-1">Custom prompt for summarization. Use <code>{existing_summary_section}</code> and <code>{messages_text}</code> as placeholders.</p>
                    </div>
                    <div class="pt-2">
                        <button id="saveAdvancedBtn" onclick="saveAdvanced()" class="bg-indigo-500 hover:bg-indigo-600 text-white px-5 py-2 rounded-md text-sm font-medium transition-colors disabled:opacity-60 disabled:cursor-not-allowed">Save Advanced</button>
                    </div>
                </div>
            </div>
            <!-- Variables Card -->
            <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
                <h3 class="text-sm font-semibold text-gray-800 dark:text-gray-100 mb-1">Variables</h3>
                <p class="text-xs text-gray-400 mb-4">Key-value config for tools &amp; skills (e.g. <code>SSH_HOST</code>, <code>SSH_USERNAME</code>). Mark as secret to mask values.</p>
                <div id="agent-variables-list" class="space-y-2 mb-3"></div>
                <button type="button" onclick="addVariableRow()" class="text-sm text-indigo-500 hover:text-indigo-700 transition-colors">+ Add Variable</button>
            </div>
        </div>
        </div><!-- /grid -->
    </div>

    <!-- Knowledge Tab -->
    <div id="tab-knowledge" class="tab-content hidden flex-1 min-h-0 overflow-y-auto">
        <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6 max-w-3xl">
            <div class="flex justify-between items-center mb-4">
                <h3 class="text-base font-semibold text-gray-800 dark:text-gray-100">Knowledge Base Files</h3>
                <div class="flex gap-2">
                    <button onclick="showNewFileModal()" class="bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-md text-sm transition-colors">+ New File</button>
                    <label class="bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-2 rounded-md text-sm cursor-pointer transition-colors">
                        Upload
                        <input type="file" accept=".md,.txt" class="hidden" onchange="uploadFile(this)">
                    </label>
                </div>
            </div>
            <p class="text-xs text-gray-400 mb-4">Files stored in <code>agents/{{ agent.id }}/kb/</code> — the agent can read these using the <code>read("filename.md")</code> tool.</p>
            <div id="kb-list" class="space-y-2">
                <!-- Files loaded dynamically -->
            </div>
            <div id="kb-empty" class="hidden text-center py-8 text-gray-400 text-sm">No knowledge files yet.</div>
        </div>

    </div>

    <!-- Tools Tab -->
    <div id="tab-tools" class="tab-content hidden flex-1 min-h-0 overflow-y-auto">
        <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6 max-w-3xl">
            <div class="flex items-center gap-2 mb-1">
                <h3 class="text-base font-semibold text-gray-800 dark:text-gray-100">Tools</h3>
                <span id="tools-badge" class="hidden px-2 py-0.5 text-xs font-medium rounded-full bg-gray-200 text-gray-600"></span>
            </div>
            <p class="text-xs text-gray-400 mb-4">Toggle tools on/off to assign them to this agent. Built-in tools are always available. Click a tool name to view details.</p>
            <!-- Filter bar -->
            <div class="flex flex-wrap items-center gap-2 mb-4">
                <input type="text" id="tool-filter" placeholder="Filter tools..." autocomplete="off"
                       class="flex-1 min-w-0 w-full sm:w-auto p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:placeholder-gray-400"
                       oninput="filterToolsList()">
                <div class="flex rounded-md border border-gray-200 overflow-hidden text-xs font-medium flex-shrink-0 w-full sm:w-auto dark:border-gray-600">
                    <button id="filter-btn-assigned" onclick="setFilterMode('assigned')"
                            class="flex-1 min-h-[44px] px-3 py-2 bg-indigo-500 text-white transition-colors dark:bg-indigo-600">Assigned</button>
                    <button id="filter-btn-available" onclick="setFilterMode('available')"
                            class="flex-1 min-h-[44px] px-3 py-2 bg-white text-gray-600 hover:bg-gray-50 transition-colors border-l border-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:border-gray-600">Available</button>
                    <button id="filter-btn-all" onclick="setFilterMode('all')"
                            class="flex-1 min-h-[44px] px-3 py-2 bg-white text-gray-600 hover:bg-gray-50 transition-colors border-l border-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:border-gray-600">All</button>
                </div>
            </div>
            <!-- Tools List -->
            <div id="tools-list" class="space-y-2">
                <!-- Dynamically populated -->
            </div>
            <div id="tools-empty" class="hidden text-center py-8 text-gray-400 text-sm">No tools found.</div>
            <span id="tools-save-status" class="mt-2 inline-block text-sm text-green-600 hidden">Saved!</span>
        </div>
    </div>

    <!-- Skills Tab -->
    {% if not agent.is_super %}
    <div id="tab-skills" class="tab-content hidden flex-1 min-h-0 overflow-y-auto">
        <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6 max-w-3xl">
            <h3 class="text-base font-semibold text-gray-800 dark:text-gray-100 mb-1">Skills</h3>
            <p class="text-xs text-gray-400 mb-4">Allow this agent to lazy-load skills via <code class="bg-gray-100 px-1 rounded">use_skill</code>. This only controls skill access — directly assigned tools are unaffected.</p>
            <div id="skills-list" class="space-y-2">
                <!-- Dynamically populated -->
            </div>
            <div id="skills-empty" class="hidden text-center py-8 text-gray-400 text-sm">No enabled skills found.</div>
            <span id="skills-save-status" class="mt-2 inline-block text-sm text-green-600 hidden">Saved!</span>
        </div>
    </div>
    {% endif %}

    <!-- Channels Tab -->
    <div id="tab-channels" class="tab-content hidden flex-1 min-h-0 overflow-y-auto">
        <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6 max-w-3xl">
            <div class="flex justify-between items-center mb-4">
                <h3 class="text-base font-semibold text-gray-800 dark:text-gray-100">Channels</h3>
                <button onclick="showAddChannelModal()" class="bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-md text-sm transition-colors">+ Add Channel</button>
            </div>
            <div id="channels-list" class="space-y-3">
                <!-- Channels loaded dynamically -->
            </div>
            <div id="channels-empty" class="hidden text-center py-8 text-gray-400 text-sm">No channels configured.</div>
        </div>
    </div>

    <!-- Portals Tab -->
    <div id="tab-portals" class="tab-content hidden flex-1 min-h-0 overflow-y-auto">
        <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6 max-w-3xl">
            <div class="flex justify-between items-center mb-4">
                <h3 class="text-base font-semibold text-gray-800 dark:text-gray-100">Portals</h3>
                <button onclick="showAddPortalModal()" class="bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-md text-sm transition-colors">+ Add Portal</button>
            </div>
            <p class="text-xs text-gray-400 mb-4">Virtual path mappings for file I/O tools (read_file, write_file, patch, str_replace). Portals do not work with bash or runpy.</p>
            <div id="portals-list" class="space-y-3">
                <!-- Portals loaded dynamically -->
            </div>
            <div id="portals-empty" class="hidden text-center py-8 text-gray-400 text-sm">No portals configured. Add a portal to give this agent access to files on another machine or directory via /_portal/ paths.</div>
        </div>
    </div>

    <!-- Artifacts Tab -->
    <div id="tab-artifacts" class="tab-content hidden flex-1 min-h-0 overflow-y-auto">
        <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6 max-w-5xl">
            <div class="flex justify-between items-center mb-4">
                <h3 class="text-base font-semibold text-gray-800 dark:text-gray-100">Artifacts</h3>
            </div>

            <!-- Search & Sort Bar -->
            <div class="flex flex-wrap items-center gap-3 mb-4">
                <div class="relative flex-1 min-w-[200px]">
                    <svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
                    </svg>
                    <input type="text" id="artifacts-search" placeholder="Search artifacts..." oninput="filterArtifacts()"
                        class="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
                </div>
                <select id="artifacts-sort" onchange="loadArtifacts()"
                    class="p-2 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
                    <option value="newest">Newest</option>
                    <option value="updated">Last Updated</option>
                    <option value="alpha">Alphabetical A-Z</option>
                    <option value="alpha_desc">Alphabetical Z-A</option>
                </select>
            </div>

            <!-- Filter Chips -->
            <div class="flex flex-wrap gap-2 mb-4" id="artifacts-filter-chips">
                <button class="artifacts-filter-chip px-3 py-1.5 text-xs font-medium rounded-full border border-indigo-500 text-indigo-600 bg-indigo-50 dark:bg-indigo-900/20 dark:text-indigo-400" data-type="all" onclick="setArtifactFilter('all')">All</button>
                <button class="artifacts-filter-chip px-3 py-1.5 text-xs font-medium rounded-full border border-gray-300 text-gray-600 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700" data-type="document" onclick="setArtifactFilter('document')">Document</button>
                <button class="artifacts-filter-chip px-3 py-1.5 text-xs font-medium rounded-full border border-gray-300 text-gray-600 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700" data-type="text" onclick="setArtifactFilter('text')">Text</button>
                <button class="artifacts-filter-chip px-3 py-1.5 text-xs font-medium rounded-full border border-gray-300 text-gray-600 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700" data-type="image" onclick="setArtifactFilter('image')">Image</button>
                <button class="artifacts-filter-chip px-3 py-1.5 text-xs font-medium rounded-full border border-gray-300 text-gray-600 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700" data-type="sound" onclick="setArtifactFilter('sound')">Sound</button>
                <button class="artifacts-filter-chip px-3 py-1.5 text-xs font-medium rounded-full border border-gray-300 text-gray-600 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700" data-type="video" onclick="setArtifactFilter('video')">Video</button>
                <button class="artifacts-filter-chip px-3 py-1.5 text-xs font-medium rounded-full border border-gray-300 text-gray-600 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700" data-type="data" onclick="setArtifactFilter('data')">Data / Other</button>
            </div>

            <!-- Artifacts Grid -->
            <div id="artifacts-grid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
                <!-- Artifacts loaded dynamically -->
            </div>

            <!-- Empty State -->
            <div id="artifacts-empty" class="hidden text-center py-12 text-gray-400">
                <svg class="w-12 h-12 mx-auto mb-3 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"></path>
                </svg>
                <p class="text-sm">No artifacts yet.</p>
                <p class="text-xs mt-1">Artifacts are files that agents save during their work. When an agent produces a report, analysis, or any output, it can be saved here.</p>
            </div>
        </div>
    </div>

    <!-- Chat Tab -->
    <div id="tab-chat" class="tab-content flex-1 min-h-0">
        <div class="flex flex-col lg:flex-row gap-4 h-full" style="min-height:0;">
            <!-- Chat Panel -->
            <div id="chat-panel" class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 flex-[3] flex flex-col min-w-0" style="min-height:0; overflow:hidden;">
                <div class="flex justify-between items-center p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
                    <h3 id="chatPanelTitle" class="text-base font-semibold text-gray-800 dark:text-gray-100 truncate max-w-[200px] sm:max-w-[320px] flex items-center gap-1.5">
                        <img id="agent-avatar-header" src="/api/agents/{{ agent.id }}/avatar" alt="" class="w-6 h-6 rounded-full object-cover bg-indigo-50 dark:bg-indigo-900/20 flex-shrink-0" onerror="this.style.display='none'">
                        <span class="truncate">{{ agent.name }}</span>
                        <span id="homePulseIcon" class="hidden inline-block w-2.5 h-2.5 rounded-full animate-pulse flex-shrink-0"></span>
                    </h3>
                    <div class="flex gap-3 items-center">
                        <button onclick="showCompiledPrompt()" class="text-xs text-gray-400 hover:text-gray-600 flex items-center gap-1" title="System Prompt">
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3.5 h-3.5"><path fill-rule="evenodd" d="M4.5 2A1.5 1.5 0 0 0 3 3.5v13A1.5 1.5 0 0 0 4.5 18h11a1.5 1.5 0 0 0 1.5-1.5V7.621a1.5 1.5 0 0 0-.44-1.06l-4.12-4.122A1.5 1.5 0 0 0 11.378 2H4.5Zm2.25 8.5a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Zm0 3a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Z" clip-rule="evenodd"/></svg>
                            <span class="hidden sm:inline">System Prompt</span>
                        </button>
                        <button onclick="confirmClearHistory()" class="text-xs text-gray-400 hover:text-red-600 flex items-center gap-1" title="Clear chat history">
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3.5 h-3.5"><path fill-rule="evenodd" d="M8.5 1a1 1 0 0 0-1 1v1h-2V2a1 1 0 0 0-2 0v1h-.32a2.5 2.5 0 0 0-2.34 1.655L.455 8.616A2.5 2.5 0 0 0 2.82 12h.32v5.5a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V12h.32a2.5 2.5 0 0 0 2.365-3.384L14.84 3.655A2.5 2.5 0 0 0 12.5.5H10V2a1 1 0 0 0-1-1H8.5ZM6 3h8v1H6V3Zm-3.5 4h10l.77 2.313a.5.5 0 0 1-.47.66H3.2a.5.5 0 0 1-.47-.66L3.5 7ZM4 12h8v5.5a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5V12Z" clip-rule="evenodd"/></svg>
                            <span class="hidden sm:inline">Clear History</span>
                        </button>
                        <button onclick="toggleChatSummary()" class="lg:hidden text-indigo-500 hover:text-indigo-700" title="View summary">
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
                        </button>
                        <span class="w-px h-4 bg-gray-200 self-center"></span>
                        <button onclick="toggleChatFullscreenMode()" id="chat-fullscreen-btn" title="Fullscreen" class="ml-auto text-gray-400 hover:text-gray-600 flex items-center">
                            <!-- Expand icon (shown when not fullscreen) -->
                            <svg id="chat-fs-expand-icon" xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
                                <path stroke-linecap="round" stroke-linejoin="round" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
                            </svg>
                            <!-- Collapse icon (shown when fullscreen) -->
                            <svg id="chat-fs-collapse-icon" xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
                                <path stroke-linecap="round" stroke-linejoin="round" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" />
                            </svg>
                        </button>
                    </div>
                </div>
                <div class="relative flex-1 flex flex-col min-h-0">
                    <div id="chat-messages" class="flex-1 overflow-y-auto p-4 space-y-3 min-h-0" style="min-height: 300px;">
                        <!-- Messages rendered here -->
                    </div>
                    <!-- Scroll-to-bottom button -->
                    <button id="scroll-to-bottom-btn" class="scroll-to-bottom-btn hidden" onclick="scrollChatToBottom()" title="Scroll to bottom">
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
                            <path fill-rule="evenodd" d="M10 3a.75.75 0 0 1 .75.75v10.638l3.96-4.158a.75.75 0 1 1 1.08 1.04l-5.25 5.5a.75.75 0 0 1-1.08 0l-5.25-5.5a.75.75 0 1 1 1.08-1.04l3.96 4.158V3.75A.75.75 0 0 1 10 3Z" clip-rule="evenodd"/>
                        </svg>
                    </button>
                </div>
                <!-- Workplace Connection Overlay (shown when agent workplace is not connected) -->
                <div id="workplace-connection-overlay" class="hidden border-t border-yellow-200 dark:border-yellow-800 bg-yellow-50 dark:bg-yellow-900/20 px-4 py-3 flex items-center gap-3">
                    <div class="w-4 h-4 border-2 border-yellow-500 border-t-transparent rounded-full animate-spin flex-shrink-0"></div>
                    <p id="workplace-overlay-msg" class="text-sm text-yellow-700 dark:text-yellow-400">Connecting to workplace...</p>
                    <button onclick="retryWorkplaceConnect()" class="ml-auto text-xs text-yellow-700 dark:text-yellow-400 hover:underline flex-shrink-0">Retry</button>
                </div>
                <div class="p-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
                    <div class="flex gap-2">
                        <textarea id="chat-input" placeholder="Type a message..." rows="1"
                            class="flex-1 p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 resize-none overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed"
                            style="max-height: 150px;"
                            onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendChat()}"
                            oninput="this.style.height='auto';this.style.height=Math.min(this.scrollHeight,150)+'px';this.style.overflowY=this.scrollHeight>150?'auto':'hidden'"></textarea>
                        <button onclick="handleChatBtn()" id="chat-send-btn" class="bg-indigo-500 hover:bg-indigo-600 text-white px-5 py-2 rounded-md text-sm transition-colors">Send</button>
                    </div>
                </div>
            </div>
            <!-- Summary Panel — hidden on mobile, sidebar on desktop -->
            <div class="hidden lg:flex flex-col gap-3 flex-[1] flex-shrink-0 min-h-0 w-1/3">
                <!-- Agent State Card -->
                {% if agent.enable_agent_state %}
                <div id="agent-state-card" class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 flex-col flex-shrink-0" style="min-height:0;">
                    <div class="flex justify-between items-center px-4 pt-4 pb-2 flex-shrink-0">
                        <div class="flex items-center gap-2">
                            <h3 class="font-semibold text-gray-800 dark:text-gray-100">Agent State</h3>
                            <span id="state-busy-dot" class="hidden items-center gap-1.5 text-xs font-medium text-amber-600 bg-amber-50 px-2 py-0.5 rounded-full"><span class="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse"></span>Busy</span>
                        </div>
                        <button onclick="refreshAgentState()" class="text-xs text-gray-400 hover:text-gray-600 flex items-center gap-1" title="Refresh">
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3.5 h-3.5"><path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H4.598a.75.75 0 0 0-.75.75v3.634a.75.75 0 0 0 1.5 0v-2.033l-.312.311a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm-10.625-2.85a5.5 5.5 0 0 1 9.201-2.465l.312.31H11.767a.75.75 0 0 0 0 1.5h3.634a.75.75 0 0 0 .75-.75V3.535a.75.75 0 0 0-1.5 0v2.033l-.312-.31A7 7 0 0 0 2.627 8.396a.75.75 0 0 0 1.449.39l.611.789Z" clip-rule="evenodd"/></svg>
                            Refresh
                        </button>
                    </div>
                    <div id="agent-state-content" class="overflow-y-auto px-4 pb-3 flex-shrink-0" style="max-height:260px;">
                        <p class="text-sm text-gray-400 italic">No state yet.</p>
                    </div>
                </div>
                {% endif %}
                <!-- Session State Card -->
                <div id="summary-card" class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 flex flex-col flex-1 min-h-0 overflow-hidden">
                    <div class="flex justify-between items-center px-4 pt-4 pb-2 flex-shrink-0">
                        <h3 class="font-semibold text-gray-800 dark:text-gray-100">Session State</h3>
                        <button onclick="refreshSessionState()" class="text-xs text-gray-400 hover:text-gray-600 flex items-center gap-1" title="Refresh">
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3.5 h-3.5"><path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H4.598a.75.75 0 0 0-.75.75v3.634a.75.75 0 0 0 1.5 0v-2.033l-.312.311a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm-10.625-2.85a5.5 5.5 0 0 1 9.201-2.465l.312.31H11.767a.75.75 0 0 0 0 1.5h3.634a.75.75 0 0 0 .75-.75V3.535a.75.75 0 0 0-1.5 0v2.033l-.312-.31A7 7 0 0 0 2.627 8.396a.75.75 0 0 0 1.449.39l.611.789Z" clip-rule="evenodd"/></svg>
                            Refresh
                        </button>
                    </div>
                    <div id="summary-content" class="flex-1 overflow-y-auto p-4 min-h-0">
                        <p class="text-sm text-gray-400 italic">No summary yet. Start chatting to generate one.</p>
                    </div>
                    <div id="summary-meta" class="hidden px-4 py-2 border-t border-gray-100 text-xs text-gray-400 flex-shrink-0"></div>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- Mobile Chat Summary Overlay -->
<div id="mobile-chat-summary-overlay" class="lg:hidden fixed inset-0 z-[70] hidden">
    <div class="absolute inset-0 bg-black/40" onclick="closeMobileChatSummary()"></div>
    <div class="absolute right-0 top-0 bottom-0 w-[85%] max-w-[340px] bg-white dark:bg-gray-800 flex flex-col shadow-xl">
        <div class="flex justify-between items-center px-4 py-3 border-b border-gray-200">
            <h4 class="font-semibold text-gray-800 dark:text-gray-100">Session State</h4>
            <div class="flex items-center gap-3">
                <button onclick="refreshSessionState()" class="text-xs text-gray-400 hover:text-gray-600" title="Refresh">
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4"><path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H4.598a.75.75 0 0 0-.75.75v3.634a.75.75 0 0 0 1.5 0v-2.033l-.312.311a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm-10.625-2.85a5.5 5.5 0 0 1 9.201-2.465l.312.31H11.767a.75.75 0 0 0 0 1.5h3.634a.75.75 0 0 0 .75-.75V3.535a.75.75 0 0 0-1.5 0v2.033l-.312-.31A7 7 0 0 0 2.627 8.396a.75.75 0 0 0 1.449.39l.611.789Z" clip-rule="evenodd"/></svg>
                </button>
                <button onclick="closeMobileChatSummary()" class="text-gray-400 hover:text-gray-600">
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"><path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"/></svg>
                </button>
            </div>
        </div>
        <div class="flex-1 overflow-y-auto min-h-0 flex flex-col p-4 space-y-4">
            {% if agent.enable_agent_state %}
            <!-- Mobile Agent State Card -->
            <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600">
                <div class="flex justify-between items-center px-4 py-2 border-b border-gray-100">
                    <div class="flex items-center gap-2">
                        <h5 class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Agent State</h5>
                        <span id="mobile-state-busy-dot" class="hidden items-center gap-1.5 text-xs font-medium text-amber-600 bg-amber-50 px-2 py-0.5 rounded-full"><span class="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse"></span>Busy</span>
                    </div>
                    <button onclick="refreshAgentState()" class="text-xs text-gray-400 hover:text-gray-600" title="Refresh">
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3.5 h-3.5"><path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H4.598a.75.75 0 0 0-.75.75v3.634a.75.75 0 0 0 1.5 0v-2.033l-.312.311a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm-10.625-2.85a5.5 5.5 0 0 1 9.201-2.465l.312.31H11.767a.75.75 0 0 0 0 1.5h3.634a.75.75 0 0 0 .75-.75V3.535a.75.75 0 0 0-1.5 0v2.033l-.312-.31A7 7 0 0 0 2.627 8.396a.75.75 0 0 0 1.449.39l.611.789Z" clip-rule="evenodd"/></svg>
                    </button>
                </div>
                <div id="mobile-agent-state-content" class="p-4">
                    <p class="text-sm text-gray-400 italic">No state yet.</p>
                </div>
            </div>
            {% endif %}
            <!-- Mobile Session State Card -->
            <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600">
                <div class="flex justify-between items-center px-4 py-2 border-b border-gray-100">
                    <h5 class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Session State</h5>
                </div>
                <div id="mobile-summary-content" class="p-4">
                    <p class="text-sm text-gray-400 italic">No summary yet. Start chatting to generate one.</p>
                </div>
            </div>
        </div>
        <div id="mobile-summary-meta" class="hidden px-4 py-2 border-t border-gray-100 text-xs text-gray-400"></div>
    </div>
</div>

<!-- New File Modal -->
<div id="new-file-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
    <div class="bg-white dark:bg-gray-800 rounded-xl w-[90%] max-w-[400px] shadow-xl">
        <div class="flex justify-between items-center p-5 border-b border-gray-200">
            <h3 class="text-gray-800 font-semibold dark:text-gray-100">New Knowledge File</h3>
            <button onclick="closeNewFileModal()" class="bg-transparent border-none text-gray-400 text-2xl cursor-pointer hover:text-gray-800">&times;</button>
        </div>
        <div class="p-5">
            <div class="mb-4">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Filename</label>
                <input type="text" id="new-file-name" placeholder="e.g., facilities.md" class="w-full p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
            </div>
            <div class="flex gap-3 justify-end">
                <button onclick="closeNewFileModal()" class="px-4 py-2 border border-gray-300 rounded-md text-sm text-gray-600 hover:bg-gray-50">Cancel</button>
                <button onclick="createNewFile()" class="px-4 py-2 bg-indigo-500 text-white rounded-md text-sm hover:bg-indigo-600">Create</button>
            </div>
        </div>
    </div>
</div>

<!-- Tool Detail Modal -->
<div id="tool-detail-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
    <div class="bg-white dark:bg-gray-800 rounded-xl w-[90%] max-w-[700px] shadow-xl flex flex-col" style="max-height: 85vh;">
        <div class="flex justify-between items-center p-5 border-b border-gray-200">
            <div class="flex items-center gap-2 flex-wrap">
                <h3 class="text-gray-800 font-semibold dark:text-gray-100" id="tool-detail-title"></h3>
                <span class="text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded font-mono" id="tool-detail-id"></span>
                <span id="tool-detail-skill" class="hidden text-xs bg-indigo-50 text-indigo-500 px-2 py-0.5 rounded"></span>
            </div>
            <button onclick="document.getElementById('tool-detail-modal').classList.add('hidden')" class="bg-transparent border-none text-gray-400 text-2xl cursor-pointer hover:text-gray-800 dark:hover:text-gray-200">&times;</button>
        </div>
        <div class="p-5 flex-1 overflow-y-auto space-y-4">
            <!-- Description -->
            <div>
                <label class="text-xs uppercase tracking-wide text-gray-400 font-semibold">Description</label>
                <p id="tool-detail-desc" class="text-sm text-gray-700 mt-1"></p>
            </div>
            <!-- Parameters (always visible) -->
            <div id="tool-detail-definition" class="space-y-3"></div>
            <!-- CODE section -->
            <div>
                <label class="text-xs uppercase tracking-wide text-gray-400 font-semibold">Code</label>
                <!-- Tab bar -->
                <div class="flex border-b border-gray-200 mt-2">
                    <button id="tool-tab-btn-prod" onclick="switchToolTab('prod')" class="px-4 py-2 text-sm font-medium border-b-2 border-indigo-500 text-indigo-600">Prod</button>
                    <button id="tool-tab-btn-mock" onclick="switchToolTab('mock')" class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-200">Mock</button>
                </div>
                <!-- Prod tab -->
                <div id="tool-tab-prod" class="pt-3">
                    <textarea id="tool-backend-code" class="w-full font-mono text-xs bg-gray-900 text-gray-100 border border-gray-700 rounded-md p-3 focus:outline-none focus:ring-2 focus:ring-indigo-500/30" rows="14" spellcheck="false" style="resize: vertical; tab-size: 4;"
                        onkeydown="if(event.key==='Tab'){event.preventDefault();const s=this.selectionStart,e=this.selectionEnd;this.value=this.value.substring(0,s)+'    '+this.value.substring(e);this.selectionStart=this.selectionEnd=s+4;}"></textarea>
                    <div class="flex items-center gap-3 mt-2">
                        <button id="saveBackendCodeBtn" onclick="saveBackendCode()" class="px-4 py-2 bg-indigo-500 text-white rounded-md text-sm hover:bg-indigo-600 transition-colors disabled:opacity-60 disabled:cursor-not-allowed">Save</button>
                        <span id="tool-backend-save-status" class="text-sm text-green-600 hidden">Saved!</span>
                    </div>
                    <!-- Test section (Prod) -->
                    <div class="mt-4 border-t border-gray-200 pt-4">
                        <label class="text-xs uppercase tracking-wide text-gray-400 font-semibold">Test</label>
                        <textarea id="tool-test-args-real" class="w-full font-mono text-xs bg-gray-50 border border-gray-200 rounded-md p-2 mt-1 focus:outline-none focus:ring-2 focus:ring-indigo-500/30" rows="3" spellcheck="false" placeholder='{"key": "value"}'></textarea>
                        <div class="flex items-center gap-3 mt-2">
                            <button onclick="testToolExec('real')" class="px-4 py-2 bg-emerald-500 text-white rounded-md text-sm hover:bg-emerald-600 transition-colors">Run</button>
                            <span id="tool-test-status-real" class="text-xs text-gray-400 hidden"></span>
                        </div>
                        <pre id="tool-test-result-real" class="hidden mt-2 bg-gray-50 border border-gray-200 rounded-md p-3 text-xs font-mono whitespace-pre-wrap overflow-x-auto" style="max-height: 200px;"></pre>
                    </div>
                </div>
                <!-- Mock tab -->
                <div id="tool-tab-mock" class="hidden pt-3">
                    <pre id="tool-detail-mock" class="bg-gray-50 border border-gray-200 rounded-md p-3 text-xs text-gray-700 font-mono whitespace-pre-wrap overflow-x-auto" style="max-height: 300px;"></pre>
                    <p id="tool-detail-no-mock" class="hidden text-sm text-gray-400 italic">No mock response defined.</p>
                    <!-- Test section (Mock) -->
                    <div class="mt-4 border-t border-gray-200 pt-4">
                        <label class="text-xs uppercase tracking-wide text-gray-400 font-semibold">Test</label>
                        <textarea id="tool-test-args-mock" class="w-full font-mono text-xs bg-gray-50 border border-gray-200 rounded-md p-2 mt-1 focus:outline-none focus:ring-2 focus:ring-indigo-500/30" rows="3" spellcheck="false" placeholder='{"key": "value"}'></textarea>
                        <div class="flex items-center gap-3 mt-2">
                            <button onclick="testToolExec('mock')" class="px-4 py-2 bg-emerald-500 text-white rounded-md text-sm hover:bg-emerald-600 transition-colors">Run</button>
                            <span id="tool-test-status-mock" class="text-xs text-gray-400 hidden"></span>
                        </div>
                        <pre id="tool-test-result-mock" class="hidden mt-2 bg-gray-50 border border-gray-200 rounded-md p-3 text-xs font-mono whitespace-pre-wrap overflow-x-auto" style="max-height: 200px;"></pre>
                    </div>
                </div>
            </div>
        </div>
        <div class="flex justify-end p-5 border-t border-gray-200">
            <a id="tool-detail-edit-link" href="#" class="px-4 py-2 text-sm text-gray-500 hover:text-indigo-600 transition-colors">Edit in Settings</a>
        </div>
    </div>
</div>

<!-- Compiled System Prompt Modal -->
<div id="compiled-prompt-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
    <div class="bg-white dark:bg-gray-800 rounded-xl w-[90%] max-w-[800px] shadow-xl flex flex-col relative" style="max-height: 85vh;">
        <div class="flex justify-between items-center p-5 border-b border-gray-200">
            <h3 class="text-gray-800 font-semibold dark:text-gray-100">Compiled Context
                <span id="compiled-token-badge" class="hidden text-[10px] font-medium px-2 py-0.5 rounded bg-indigo-100 text-indigo-700 dark:bg-indigo-500/30 dark:text-indigo-200 ml-2 align-middle"></span>
            </h3>
            <button onclick="document.getElementById('compiled-prompt-modal').classList.add('hidden')" class="bg-transparent border-none text-gray-400 text-2xl cursor-pointer hover:text-gray-800">&times;</button>
        </div>
        <!-- Loading Spinner Overlay -->
        <div id="compiled-prompt-loading" class="hidden absolute inset-0 bg-white/80 flex items-center justify-center z-10 rounded-xl">
            <div class="flex flex-col items-center gap-3">
                <svg class="animate-spin h-8 w-8 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                    <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                    <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
                </svg>
                <span class="text-sm text-gray-500">Loading...</span>
            </div>
        </div>
        <!-- Tabs -->
        <div class="flex border-b border-gray-200 px-5">
            <button onclick="switchPromptTab('prompt')" id="prompt-tab-btn-prompt" class="px-4 py-2 text-sm font-medium border-b-2 border-indigo-500 text-indigo-600">System Prompt</button>
            <button onclick="switchPromptTab('llm')" id="prompt-tab-btn-llm" class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700">LLM Request</button>
        </div>
        <!-- System Prompt Tab -->
        <div id="prompt-tab-prompt" class="p-5 flex-1 overflow-y-auto space-y-4">
            <div>
                <h4 class="text-sm font-semibold text-gray-700 mb-2 cursor-pointer flex items-center gap-1" onclick="document.getElementById('compiled-prompt-text').classList.toggle('hidden'); this.querySelector('.chevron').classList.toggle('rotated')">
                    System Prompt
                    <span class="chevron rotated text-gray-400 text-[10px]">&#9656;</span>
                    <span id="compiled-sp-tokens" class="text-xs font-normal text-gray-400 ml-1"></span>
                </h4>
                <pre id="compiled-prompt-text" class="bg-gray-50 border border-gray-200 rounded-md p-4 text-xs text-gray-700 font-mono whitespace-pre-wrap overflow-x-auto"></pre>
            </div>
            <div id="compiled-memories-section" class="hidden">
                <h4 class="text-sm font-semibold text-gray-700 mb-2 cursor-pointer flex items-center gap-1" onclick="document.getElementById('compiled-memories-text').classList.toggle('hidden'); this.querySelector('.chevron').classList.toggle('rotated')">
                    Memory
                    <span class="chevron text-gray-400 text-[10px]">&#9656;</span>
                    <span id="compiled-mem-tokens" class="text-xs font-normal text-gray-400 ml-1"></span>
                </h4>
                <pre id="compiled-memories-text" class="hidden bg-purple-50 border border-purple-200 rounded-md p-4 text-xs text-gray-700 font-mono whitespace-pre-wrap overflow-x-auto"></pre>
            </div>
            <div id="compiled-summary-section" class="hidden">
                <h4 class="text-sm font-semibold text-gray-700 mb-2 cursor-pointer flex items-center gap-1" onclick="document.getElementById('compiled-summary-text').classList.toggle('hidden'); this.querySelector('.chevron').classList.toggle('rotated')">
                    Prior Conversation Summary
                    <span class="chevron text-gray-400 text-[10px]">&#9656;</span>
                    <span id="compiled-sum-tokens" class="text-xs font-normal text-gray-400 ml-1"></span>
                </h4>
                <pre id="compiled-summary-text" class="hidden bg-amber-50 border border-amber-200 rounded-md p-4 text-xs text-gray-700 font-mono whitespace-pre-wrap overflow-x-auto"></pre>
            </div>
            <div>
                <h4 class="text-sm font-semibold text-gray-700 mb-2 cursor-pointer flex items-center gap-1" onclick="document.getElementById('compiled-tools-json').classList.toggle('hidden'); this.querySelector('.chevron').classList.toggle('rotated')">
                    Tool Definitions
                    <span class="chevron text-gray-400 text-[10px] transition-transform">&#9656;</span>
                    <span id="compiled-tools-count" class="text-xs font-normal text-gray-400 ml-1"></span>
                    <span id="compiled-tool-tokens" class="text-xs font-normal text-gray-400 ml-1"></span>
                </h4>
                <pre id="compiled-tools-json" class="hidden bg-gray-50 border border-gray-200 rounded-md p-4 text-xs text-gray-700 font-mono whitespace-pre-wrap overflow-x-auto" style="max-height: 400px;"></pre>
            </div>
        </div>
        <!-- LLM Request Tab -->
        <div id="prompt-tab-llm" class="hidden p-5 flex-1 overflow-y-auto">
            <div class="flex items-center gap-2 mb-3">
                <h4 class="text-sm font-semibold text-gray-700">Messages Array</h4>
                <span id="llm-preview-badge" class="hidden text-[10px] px-2 py-0.5 rounded font-medium bg-amber-100 text-amber-700">Summary Active</span>
            </div>
            <div id="llm-preview-messages" class="space-y-2">
                <p class="text-xs text-gray-400 italic">Loading...</p>
            </div>
        </div>
    </div>
</div>

<!-- KB Editor Modal -->
<div id="kb-editor-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
    <div class="bg-white dark:bg-gray-800 rounded-xl w-[90%] max-w-[700px] shadow-xl flex flex-col" style="max-height: 85vh;">
        <div class="flex justify-between items-center p-5 border-b border-gray-200">
            <h3 class="text-gray-800 font-semibold dark:text-gray-100">Editing: <span id="kb-editor-filename" class="font-mono text-indigo-600"></span></h3>
            <button onclick="closeEditor()" class="bg-transparent border-none text-gray-400 text-2xl cursor-pointer hover:text-gray-800">&times;</button>
        </div>
        <div class="p-5 flex-1 overflow-hidden flex flex-col">
            <textarea id="kb-editor-content" class="flex-1 w-full p-3 border border-gray-300 rounded-md text-sm font-mono resize-none focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10" style="min-height: 300px;"></textarea>
        </div>
        <div class="flex gap-3 justify-end p-5 border-t border-gray-200">
            <button onclick="closeEditor()" class="px-4 py-2 border border-gray-300 rounded-md text-sm text-gray-600 hover:bg-gray-50 dark:text-gray-200 dark:hover:text-gray-50 dark:hover:bg-gray-400">Cancel</button>
            <button id="saveFileBtn" onclick="saveFile()" class="px-4 py-2 bg-indigo-500 text-white rounded-md text-sm hover:bg-indigo-600 transition-colors disabled:opacity-60 disabled:cursor-not-allowed">Save</button>
        </div>
    </div>
</div>

<!-- Add Channel Modal -->
<div id="channel-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
    <div class="bg-white dark:bg-gray-800 rounded-xl w-[90%] max-w-[450px] shadow-xl">
        <div class="flex justify-between items-center p-5 border-b border-gray-200">
            <h3 class="text-gray-800 font-semibold dark:text-gray-100">Add Channel</h3>
            <button onclick="closeChannelModal()" class="bg-transparent border-none text-gray-400 text-2xl cursor-pointer hover:text-gray-800">&times;</button>
        </div>
        <div class="p-5">
            <div class="mb-4">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Type</label>
                <select id="channel-type" class="w-full p-2.5 border border-gray-300 rounded-md text-sm" onchange="updateChannelConfigFields()">
                    <option value="telegram">Telegram</option>
                    <option value="whatsapp">WhatsApp</option>
                    <option value="discord">Discord</option>
                </select>
            </div>
            <div class="mb-4">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Name</label>
                <input type="text" id="channel-name" placeholder="e.g., Main Bot" class="w-full p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
            </div>
            <div id="channel-config-fields" class="mb-4">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Bot Token</label>
                <input type="text" id="channel-bot-token" placeholder="Telegram bot token" class="w-full p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
            </div>
            <div class="flex gap-3 justify-end">
                <button onclick="closeChannelModal()" class="px-4 py-2 border border-gray-300 rounded-md text-sm text-gray-600 hover:bg-gray-600 dark:text-gray-50">Cancel</button>
                <button onclick="addChannel()" class="px-4 py-2 bg-indigo-500 text-white rounded-md text-sm hover:bg-indigo-600">Add</button>
            </div>
        </div>
    </div>
</div>

<!-- Portal Modal -->
<div id="portal-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
    <div class="bg-white dark:bg-gray-800 rounded-xl w-[90%] max-w-[500px] shadow-xl">
        <div class="flex justify-between items-center p-5 border-b border-gray-200">
            <h3 class="text-gray-800 font-semibold dark:text-gray-100" id="portal-modal-title">Add Portal</h3>
            <button onclick="closePortalModal()" class="bg-transparent border-none text-gray-400 text-2xl cursor-pointer hover:text-gray-800">&times;</button>
        </div>
        <div class="p-5">
            <input type="hidden" id="portal-id" value="">
            <div class="mb-4">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Name</label>
                <input type="text" id="portal-name" placeholder="e.g., Server B Data" class="w-full p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
            </div>
            <div class="mb-4">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Virtual Path</label>
                <div class="flex items-center">
                    <span class="px-2 py-2.5 border border-r-0 border-gray-300 rounded-l-md bg-gray-50 text-gray-500 text-sm">/_portal/</span>
                    <input type="text" id="portal-virtual-path" placeholder="data_b" class="flex-1 p-2.5 border border-gray-300 rounded-r-md text-sm font-mono focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
                </div>
                <p class="text-xs text-gray-400 mt-1">The virtual path prefix agents use. E.g., "data_b" → /_portal/data_b/...</p>
            </div>
            <div class="mb-4">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Backend Type</label>
                <select id="portal-backend-type" class="w-full p-2.5 border border-gray-300 rounded-md text-sm" onchange="updatePortalConfigFields()">
                    <option value="local">Local (host filesystem)</option>
                    <option value="ssh">SSH (remote server)</option>
                    <option value="evonet">Evonet (tunnel-connected machine)</option>
                </select>
            </div>
            <!-- Workplace picker (for SSH and evonet) -->
            <div id="portal-workplace-picker" class="mb-4 hidden">
                <label class="flex items-center gap-2 cursor-pointer select-none mb-2">
                    <input type="checkbox" id="portal-use-workplace" onchange="updatePortalConfigFields()" class="rounded border-gray-300 text-indigo-500 focus:ring-indigo-400">
                    <span class="text-sm text-gray-500">Use existing workplace</span>
                </label>
                <select id="portal-workplace-select" class="w-full p-2.5 border border-gray-300 rounded-md text-sm hidden">
                    <option value="">-- Select workplace --</option>
                </select>
                <p class="text-xs text-gray-400 mt-1">Reuse an existing workplace connection instead of creating a new one.</p>
            </div>
            <!-- SSH config fields -->
            <div id="portal-ssh-config" class="hidden space-y-3">
                <div>
                    <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Host</label>
                    <input type="text" id="portal-ssh-host" placeholder="192.168.1.100" class="w-full p-2.5 border border-gray-300 rounded-md text-sm font-mono focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
                </div>
                <div class="grid grid-cols-2 gap-3">
                    <div>
                        <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Port</label>
                        <input type="number" id="portal-ssh-port" value="22" class="w-full p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
                    </div>
                    <div>
                        <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Username</label>
                        <input type="text" id="portal-ssh-username" placeholder="root" class="w-full p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
                    </div>
                </div>
                <div>
                    <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Auth Type</label>
                    <select id="portal-ssh-auth-type" class="w-full p-2.5 border border-gray-300 rounded-md text-sm" onchange="updatePortalConfigFields()">
                        <option value="key">SSH Key</option>
                        <option value="password">Password</option>
                    </select>
                </div>
                <div id="portal-ssh-key-fields">
                    <div>
                        <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Key Path</label>
                        <input type="text" id="portal-ssh-key-path" placeholder="~/.ssh/id_rsa" class="w-full p-2.5 border border-gray-300 rounded-md text-sm font-mono focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
                    </div>
                    <div class="mt-3">
                        <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Passphrase (optional)</label>
                        <input type="password" id="portal-ssh-passphrase" placeholder="Key passphrase" class="w-full p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
                    </div>
                </div>
                <div id="portal-ssh-password-field" class="hidden">
                    <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Password</label>
                    <input type="password" id="portal-ssh-password" placeholder="SSH password" class="w-full p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
                </div>
            </div>
            <div class="mb-4 mt-4">
                <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Real Path</label>
                <input type="text" id="portal-real-path" placeholder="/home/data_b" class="w-full p-2.5 border border-gray-300 rounded-md text-sm font-mono focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
                <p class="text-xs text-gray-400 mt-1">The actual filesystem path on the target machine.</p>
            </div>
            <div class="flex gap-3 justify-end">
                <button onclick="closePortalModal()" class="px-4 py-2 border border-gray-300 rounded-md text-sm text-gray-600 hover:bg-gray-600 dark:text-gray-50">Cancel</button>
                <button onclick="savePortal()" class="px-4 py-2 bg-indigo-500 text-white rounded-md text-sm hover:bg-indigo-600">Save</button>
            </div>
        </div>
    </div>
</div>

<!-- WhatsApp QR Modal -->
<div id="whatsapp-qr-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
    <div class="bg-white dark:bg-gray-800 rounded-xl w-[90%] max-w-[380px] shadow-xl">
        <div class="flex justify-between items-center p-5 border-b border-gray-200 dark:border-gray-700">
            <h3 class="text-gray-800 font-semibold dark:text-gray-100">Connect WhatsApp</h3>
            <button onclick="closeQRModal()" class="bg-transparent border-none text-gray-400 text-2xl cursor-pointer hover:text-gray-800">&times;</button>
        </div>
        <div class="p-5 text-center">
            <div id="qr-status-text" class="text-sm text-gray-500 dark:text-gray-400 mb-4">Loading QR code...</div>
            <div id="qr-image-container" class="flex justify-center mb-4">
                <img id="qr-image" src="" class="hidden w-56 h-56 rounded-lg border border-gray-200" alt="WhatsApp QR Code">
                <div id="qr-spinner" class="w-56 h-56 flex items-center justify-center">
                    <div class="w-8 h-8 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
                </div>
            </div>
            <p id="qr-instructions" class="text-xs text-gray-400 dark:text-gray-500">Open WhatsApp on your phone, go to Settings > Linked Devices > Link a Device, then scan this QR code.</p>
            <div id="qr-connected-msg" class="hidden mt-2">
                <p class="text-green-600 font-semibold">Connected!</p>
                <p class="text-sm text-gray-500 mt-1">WhatsApp is now linked. You can close this window.</p>
            </div>
        </div>
        <div class="flex justify-end p-5 border-t border-gray-200 dark:border-gray-700">
            <button onclick="closeQRModal()" class="px-4 py-2 border border-gray-300 rounded-md text-sm text-gray-600 hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-gray-700">Close</button>
        </div>
    </div>
</div>

<!-- Channel Detail Modal (pending approvals + allowlist) -->
<div id="channel-detail-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
    <div class="bg-white dark:bg-gray-800 rounded-xl w-[90%] max-w-[550px] shadow-xl max-h-[90vh] flex flex-col">
        <div class="flex justify-between items-center p-5 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
            <h3 class="text-gray-800 dark:text-gray-100 font-semibold" id="channel-detail-title">Channel Detail</h3>
            <button onclick="closeChannelDetail()" class="bg-transparent border-none text-gray-400 text-2xl cursor-pointer hover:text-gray-800 dark:hover:text-gray-200">&times;</button>
        </div>
        <div class="p-5 overflow-y-auto flex-1">
            <!-- Channel Info -->
            <div class="mb-4 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
                <div class="flex items-center gap-2 mb-2">
                    <span id="cd-channel-name" class="text-sm font-semibold text-gray-800 dark:text-gray-100"></span>
                    <span id="cd-channel-type-badge" class="text-xs px-2 py-0.5 rounded font-medium"></span>
                    <label class="flex items-center gap-1 cursor-pointer ml-auto select-none" title="Toggle channel mode">
                        <span class="text-[10px] text-gray-400" id="cd-mode-label-open">Open</span>
                        {{ toggle_widget(id='cd-mode-toggle', onchange='toggleChannelMode(this.checked)') }}
                        <span class="text-[10px] text-gray-400" id="cd-mode-label-restricted">Restricted</span>
                    </label>
                </div>
                <div class="flex items-center gap-1.5">
                    <span id="cd-channel-status-dot" class="inline-block w-2 h-2 rounded-full"></span>
                    <span id="cd-channel-status-text" class="text-xs text-gray-400"></span>
                </div>
            </div>

            <!-- Pending Approvals -->
            <div class="mb-4">
                <div class="flex items-center justify-between mb-2">
                    <h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Pending Approvals</h4>
                    <span id="cd-pending-count" class="text-xs bg-amber-100 text-amber-700 px-2 py-0.5 rounded-full"></span>
                </div>
                <div id="cd-pending-list" class="space-y-2">
                    <p class="text-xs text-gray-400 text-center py-3">No pending approvals.</p>
                </div>
            </div>

            <!-- Pairing Code Generator -->
            <div class="mb-4">
                <h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Pairing Code</h4>
                <div class="flex gap-2 items-center">
                    <button onclick="generatePairCode()" class="px-3 py-2 bg-indigo-500 text-white rounded-md text-sm hover:bg-indigo-600 flex-shrink-0">Generate</button>
                    <span id="cd-pair-code-display" class="text-sm font-mono font-bold text-indigo-600 dark:text-indigo-400 flex-1 text-center"></span>
                    <button onclick="copyPairCode()" id="cd-pair-copy-btn" class="hidden px-2 py-2 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-md text-xs hover:bg-gray-200 dark:hover:bg-gray-600 flex-shrink-0" title="Copy code">📋</button>
                </div>
                <p class="text-[10px] text-gray-400 mt-1">Share this 6-character code with users so they can auto-approve via WhatsApp/Telegram.<br>Codes expire after 5 minutes.</p>
            </div>

            <!-- Allowlist -->
            <div>
                <h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Allowed Users</h4>
                <div id="cd-allowlist" class="space-y-1.5 mb-3">
                    <p class="text-xs text-gray-400 text-center py-2">No users in allowlist.</p>
                </div>
                <div class="flex gap-2">
                    <input type="text" id="cd-add-user-input" placeholder="Enter external_user_id..."
                        class="flex-1 p-2 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100 dark:border-gray-600">
                    <button onclick="addAllowedUser()" class="px-3 py-2 bg-indigo-500 text-white rounded-md text-sm hover:bg-indigo-600 flex-shrink-0">Add</button>
                </div>
            </div>
        </div>
    </div>
</div>

<script src="{{ url_for('static', filename='js/chat-ui.js') }}?v=42"></script>
<script src="{{ url_for('static', filename='js/agent-state.js') }}?v=3"></script>
<script>
function showToast(message, type = 'success') {
    const container = document.getElementById('toast-container');
    if (!container) return;
    
    const toast = document.createElement('div');
    toast.className = `px-4 py-3 rounded-lg shadow-lg text-white font-medium transition-opacity duration-500 ${
        type === 'success' ? 'bg-green-600' : 'bg-red-600'
    }`;
    toast.textContent = message;
    
    container.appendChild(toast);
    
    setTimeout(() => {
        toast.style.opacity = '0';
        setTimeout(() => toast.remove(), 500);
    }, 3000);
}
</script>
<script>
const AGENT_ID = '{{ agent.id }}';
const ENABLE_AGENT_STATE = {{ 'true' if agent.enable_agent_state else 'false' }};
const ORIGINAL_WORKSPACE = '{{ agent.workspace or '' }}';
let currentEditFile = null;
let agentTools = [];
let chatLoaded = false;

// --- Chat input draft persistence (localStorage) ---
(function() {
    const DRAFT_KEY = 'chat_draft_' + AGENT_ID;

    function saveDraft() {
        const input = document.getElementById('chat-input');
        if (!input) return;
        const val = input.value;
        if (val.trim()) {
            try { localStorage.setItem(DRAFT_KEY, val); } catch(e) { /* quota exceeded, ignore */ }
        } else {
            try { localStorage.removeItem(DRAFT_KEY); } catch(e) {}
        }
    }

    function restoreDraft() {
        const input = document.getElementById('chat-input');
        if (!input) return;
        try {
            const saved = localStorage.getItem(DRAFT_KEY);
            if (saved && saved.trim()) {
                input.value = saved;
                // Trigger height auto-resize
                input.style.height = 'auto';
                input.style.height = Math.min(input.scrollHeight, 150) + 'px';
                input.style.overflowY = input.scrollHeight > 150 ? 'auto' : 'hidden';
            }
        } catch(e) { /* ignore */ }
    }

    function clearDraft() {
        try { localStorage.removeItem(DRAFT_KEY); } catch(e) {}
    }

    // Expose clearDraft so sendChat() can call it
    window._clearChatDraft = clearDraft;

    // Save on input
    document.addEventListener('DOMContentLoaded', function() {
        const input = document.getElementById('chat-input');
        if (input) {
            input.addEventListener('input', saveDraft);
            // Also save on blur for extra safety
            input.addEventListener('blur', saveDraft);
        }
    });

    // Restore on load
    $(function() { restoreDraft(); });

    // Safety net: save on page unload
    $(window).on('beforeunload', saveDraft);
})();

// Avatar upload handler
document.addEventListener('DOMContentLoaded', function() {
    const avatarInput = document.getElementById('avatar-input');
    if (avatarInput) {
        avatarInput.addEventListener('change', async function() {
            const file = this.files[0];
            if (!file) return;
            const formData = new FormData();
            formData.append('file', file);
            const res = await fetch(`/api/agents/${AGENT_ID}/avatar`, { method: 'POST', body: formData });
            const data = await res.json();
            if (data.error) { showToast(data.error, 'error'); return; }
            showToast('Avatar updated!', 'success');
            // Refresh avatar preview
            const preview = document.getElementById('avatar-preview');
            if (preview) preview.src = `/api/agents/${AGENT_ID}/avatar?t=${Date.now()}`;
            const headerAvatar = document.getElementById('agent-avatar-header');
            if (headerAvatar) { headerAvatar.src = `/api/agents/${AGENT_ID}/avatar?t=${Date.now()}`; headerAvatar.style.display = ''; headerAvatar.onerror = function() { this.style.display = 'none'; }; }
        });
    }
});

async function removeAvatar() {
    const confirmed = await showConfirm({ title: 'Remove avatar?', message: 'This will delete the agent avatar image.', confirmText: 'Remove', danger: true });
    if (!confirmed) return;
    const res = await fetch(`/api/agents/${AGENT_ID}/avatar`, { method: 'DELETE' });
    const data = await res.json();
    if (data.error) { showToast(data.error, 'error'); return; }
    showToast('Avatar removed!', 'success');
    // Reset previews to default
    const defaultSrc = `data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 40 40%22 fill=%22none%22><rect width=%2240%22 height=%2240%22 rx=%2220%22 fill=%22%23e0e7ff%22/><path d=%22M20 8a5 5 0 100 10 5 5 0 000-10zm-8 18.5a8 8 0 0116 0%22 fill=%22%234f46e5%22/></svg>`;
    const preview = document.getElementById('avatar-preview');
    if (preview) preview.src = defaultSrc;
    const headerAvatar = document.getElementById('agent-avatar-header');
    if (headerAvatar) { headerAvatar.src = defaultSrc; headerAvatar.style.display = ''; }
}

// chatUI is initialized on DOMContentLoaded by the init script at the bottom of this page.
var chatUI = null;

// Scroll-to-bottom button logic
(function() {
    const msgs = document.getElementById('chat-messages');
    const btn = document.getElementById('scroll-to-bottom-btn');
    if (!msgs || !btn) return;
    msgs.addEventListener('scroll', function() {
        const nearBottom = msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 80;
        btn.classList.toggle('hidden', nearBottom);
    });
})();

function scrollChatToBottom() {
    chatUI.scrollToBottom();
    document.getElementById('scroll-to-bottom-btn').classList.add('hidden');
}

// ==================== Tab Management ====================
function enterChatFullscreen() {
    const isMobile = window.innerWidth < 1024;
    if (isMobile) {
        // On mobile let CSS handle layout — JS calc is unreliable before scroll settles
        $('#tab-chat').css('height', '');
        $('html, body').css('overflow', '');
        return;
    }
    window.scrollTo(0, 0);
    // Use requestAnimationFrame so getBoundingClientRect is measured after scroll+layout
    requestAnimationFrame(function () {
        const $tab = $('#tab-chat');
        const rect = $tab[0].getBoundingClientRect();
        $tab.css('height', Math.max(window.innerHeight - rect.top, 400) + 'px');
        $('html, body').css('overflow', 'hidden');
        if (chatUI) chatUI.scrollToBottom();
    });
}

function exitChatFullscreen() {
    $('#tab-chat').css('height', '');
    $('html, body').css('overflow', '');
}

let _chatFullscreenActive = false;

function toggleChatFullscreenMode() {
    const $tab = $('#tab-chat');
    // The page header and tab bar are the two flex-shrink-0 siblings above #tab-chat
    const $header = $tab.closest('.flex-col').children('.flex-shrink-0');

    _chatFullscreenActive = !_chatFullscreenActive;

    if (_chatFullscreenActive) {
        $tab.addClass('chat-fullscreen');
        $('#chat-panel').removeClass('rounded-lg');
        $header.hide();
        $('body').css('overflow', 'hidden');
        $('#chat-fs-expand-icon').addClass('hidden');
        $('#chat-fs-collapse-icon').removeClass('hidden');
    } else {
        $tab.removeClass('chat-fullscreen');
        $('#chat-panel').addClass('rounded-lg');
        $header.show();
        $('body').css('overflow', '');
        $('#chat-fs-expand-icon').removeClass('hidden');
        $('#chat-fs-collapse-icon').addClass('hidden');
        // Re-apply height sizing for the non-fullscreen state
        enterChatFullscreen();
    }

    requestAnimationFrame(function () {
        if (chatUI) chatUI.scrollToBottom();
    });
}

// Escape key exits fullscreen
$(document).on('keydown', function (e) {
    if (e.key === 'Escape' && _chatFullscreenActive) toggleChatFullscreenMode();
});

// Click-outside to close compiled-prompt-modal
$(document).on('click', '#compiled-prompt-modal', function (e) {
    if (e.target === e.currentTarget) {
        $(this).addClass('hidden');
    }
});

function showTab(name) {
    $('.tab-content').addClass('hidden');
    $('.tab-btn').removeClass('border-indigo-500 text-indigo-600')
                 .addClass('border-transparent text-gray-500');
    $('#tab-' + name).removeClass('hidden');
    $(`[data-tab="${name}"]`).addClass('border-indigo-500 text-indigo-600')
                              .removeClass('border-transparent text-gray-500');

    if (name === 'knowledge') loadKBFiles();
    if (name === 'tools') loadTools();
    if (name === 'skills') loadSkills();
    if (name === 'channels') loadChannels();
    if (name === 'portals') loadPortals();
    if (name === 'artifacts') loadArtifacts();
    if (name === 'chat' && !chatLoaded) { loadChatHistory(); refreshSessionState(); refreshAgentState(); }

    if (name === 'chat') {
        enterChatFullscreen();
        $('#chat-input').focus();
    } else {
        exitChatFullscreen();
    }

    location.hash = name;
}

$(window).on('resize', function () {
    if (!$('#tab-chat').hasClass('hidden')) enterChatFullscreen();
});

// ==================== Settings Tab ====================
function toggleAdvanced() {
    const section = document.getElementById('advanced-section');
    const arrow = document.getElementById('adv-arrow');
    section.classList.toggle('hidden');
    arrow.style.transform = section.classList.contains('hidden') ? '' : 'rotate(90deg)';
}

// ==================== Agent Variables ====================
let _agentVariables = [];

function addVariableRow(key = '', value = '', isSecret = false) {
    const list = document.getElementById('agent-variables-list');
    const row = document.createElement('div');
    row.className = 'flex gap-2 items-center';
    row.innerHTML = `
        <input type="text" placeholder="KEY_NAME" value="${key.replace(/"/g, '&quot;')}"
            class="var-key w-40 p-2 border border-gray-300 rounded-md text-sm font-mono focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10"
            style="text-transform: uppercase;">
        <input type="${isSecret ? 'password' : 'text'}" placeholder="value" value="${value.replace(/"/g, '&quot;')}"
            class="var-value flex-1 p-2 border border-gray-300 rounded-md text-sm font-mono focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10">
        <label class="flex items-center gap-1 cursor-pointer select-none text-xs text-gray-500 whitespace-nowrap" title="Mark as secret (masked in UI)">
            <input type="checkbox" class="var-secret" ${isSecret ? 'checked' : ''}
                onchange="this.closest('.flex').querySelector('.var-value').type = this.checked ? 'password' : 'text'; scheduleVarSave()">
            Secret
        </label>
        <button type="button" onclick="removeVariableRow(this)" class="text-red-400 hover:text-red-600 text-lg leading-none" title="Remove">&times;</button>
    `;
    list.appendChild(row);
    row.querySelector('.var-key').addEventListener('input', scheduleVarSave);
    row.querySelector('.var-value').addEventListener('input', scheduleVarSave);
}

function collectVariables() {
    const rows = document.querySelectorAll('#agent-variables-list > div');
    const vars = [];
    rows.forEach(row => {
        const key = row.querySelector('.var-key').value.trim().toUpperCase();
        const value = row.querySelector('.var-value').value;
        const isSecret = row.querySelector('.var-secret').checked;
        if (key) vars.push({ key, value, is_secret: isSecret });
    });
    return vars;
}

async function loadVariables() {
    const res = await fetch(`/api/agents/${AGENT_ID}/variables`);
    const data = await res.json();
    _agentVariables = data.variables || [];
    const list = document.getElementById('agent-variables-list');
    list.innerHTML = '';
    _agentVariables.forEach(v => addVariableRow(v.key, v.value, v.is_secret));
}

let _varSaveTimer = null;
function scheduleVarSave() {
    clearTimeout(_varSaveTimer);
    _varSaveTimer = setTimeout(() => saveVariables(true), 800);
}

function removeVariableRow(btn) {
    btn.closest('.flex').remove();
    saveVariables(true);
}

async function saveVariables(silent = false) {
    const vars = collectVariables();
    await fetch(`/api/agents/${AGENT_ID}/variables`, {
        method: 'PUT',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({ variables: vars })
    });
    if (!silent) showToast("Variables saved successfully!", "success");
}

// ==================== Agent Safety Rules (HMADS) ====================
let _specificRules = [];
let _assignedRuleIds = new Set();

async function loadAgentSafetyRules() {
    const listEl = document.getElementById('agent-safety-rules-list');
    try {
        const [rulesRes, assignedRes] = await Promise.all([
            fetch('/api/safety-rules'),
            fetch(`/api/agents/${AGENT_ID}/safety-rules`),
        ]);
        const rulesData = await rulesRes.json();
        const assignedData = await assignedRes.json();
        _specificRules = (rulesData.rules || []).filter(r => r.scope === 'specific' && !r.is_system);
        _assignedRuleIds = new Set(assignedData.rule_ids || []);

        if (!_specificRules.length) {
            listEl.innerHTML = '<div class="text-xs text-gray-400">No specific-scope rules available. <a href="/system#hmads" class="text-indigo-500 hover:underline">Create one</a></div>';
            return;
        }
        listEl.innerHTML = _specificRules.map(r => {
            const checked = _assignedRuleIds.has(r.id) ? 'checked' : '';
            const weightColor = r.weight >= 15 ? 'text-red-600' : r.weight >= 8 ? 'text-orange-600' : r.weight >= 4 ? 'text-yellow-600' : 'text-gray-500';
            return `<label class="flex items-center gap-2 py-1 px-1 rounded hover:bg-gray-100 dark:hover:bg-gray-600/50 cursor-pointer select-none">
                <input type="checkbox" value="${r.id}" ${checked} onchange="saveAgentSafetyRules()" class="agent-safety-rule-cb rounded border-gray-300 text-indigo-500 focus:ring-indigo-400">
                <span class="text-sm text-gray-700 dark:text-gray-200">${r.name}</span>
                <span class="text-[10px] ${weightColor} ml-auto">${r.weight}</span>
            </label>`;
        }).join('');
    } catch(e) {
        listEl.innerHTML = '<div class="text-xs text-red-500">Failed to load rules</div>';
        console.error('Failed to load agent safety rules:', e);
    }
}

async function saveAgentSafetyRules() {
    const checkboxes = document.querySelectorAll('.agent-safety-rule-cb');
    const ruleIds = Array.from(checkboxes).filter(cb => cb.checked).map(cb => cb.value);
    try {
        await fetch(`/api/agents/${AGENT_ID}/safety-rules`, {
            method: 'PUT',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({ rule_ids: ruleIds }),
        });
    } catch(e) { console.error('Failed to save agent safety rules:', e); }
}

const SPINNER_SVG = '<svg class="animate-spin h-4 w-4 mr-1.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';

function setSaveLoading(btnId, loading) {
    const btn = document.getElementById(btnId);
    if (!btn) return;
    if (loading) {
        btn.disabled = true;
        btn.dataset.originalText = btn.textContent;
        btn.innerHTML = '<span class="inline-flex items-center">' + SPINNER_SVG + 'Saving...</span>';
    } else {
        btn.disabled = false;
        btn.textContent = btn.dataset.originalText || btn.textContent;
    }
}

async function saveGeneral() {
    const btnId = 'saveGeneralBtn';
    setSaveLoading(btnId, true);
    let workspaceChanged = false;
    try {
    const homeSel = document.getElementById('agent-workplace-id');
    const selectedOpt = homeSel ? homeSel.options[homeSel.selectedIndex] : null;
    const homeType = selectedOpt ? selectedOpt.dataset.type : '';
    const isRemoteOrTunnel = (homeType === 'remote' || homeType === 'tunnel');
    const newWorkspace = document.getElementById('agent-workspace').value || null;
    workspaceChanged = (newWorkspace || null) !== (ORIGINAL_WORKSPACE || null);
    const res = await fetch(`/api/agents/${AGENT_ID}`, {
        method: 'PUT',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({
            name: document.getElementById('agent-name').value,
            description: document.getElementById('agent-desc').value,
            workspace: newWorkspace,
            workplace_id: homeSel ? homeSel.value || null : null,
            system_prompt: document.getElementById('agent-system-prompt').value,
            vision_enabled: document.getElementById('agent-vision').checked ? 1 : 0,
            attachments_enabled: document.getElementById('agent-attachments').checked ? 1 : 0,
            attachment_max_size_mb: Math.min(20, Math.max(1, parseInt(document.getElementById('agent-attachment-max-mb').value) || 20)),
            sandbox_enabled: isRemoteOrTunnel ? 0 : (document.getElementById('agent-sandbox').checked ? 1 : 0),
            safety_checker_enabled: document.getElementById('agent-safety-checker').checked ? 1 : 0,
            inject_agent_id: document.getElementById('agent-inject-id').checked ? 1 : 0,
            inject_datetime: document.getElementById('agent-inject-datetime').checked ? 1 : 0,
        })
    });
    const data = await res.json();
    if (!data.success) { showToast("Failed to save settings.", "error"); return; }

    const selectedModelId = document.getElementById('agent-model-select').value || null;
    const selectedFallbackModelId = document.getElementById('agent-fallback-model-select').value || null;
    await fetch(`/api/agents/${AGENT_ID}/model`, {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({ model_id: selectedModelId, fallback_model_id: selectedFallbackModelId })
    });

    if (workspaceChanged) {
        location.reload();
        return;
    }
    showToast("Settings saved successfully!", "success");
    } finally {
        if (!workspaceChanged) setSaveLoading(btnId, false);
    }
}

async function saveAdvanced() {
    const btnId = 'saveAdvancedBtn';
    setSaveLoading(btnId, true);
    try {
    const res = await fetch(`/api/agents/${AGENT_ID}`, {
        method: 'PUT',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({
            summarize_threshold: parseInt(document.getElementById('agent-summarize-threshold').value) || 0,
            summarize_tail: parseInt(document.getElementById('agent-summarize-tail').value) || 5,
            summarize_prompt: document.getElementById('agent-summarize-prompt').value || null,
            message_buffer_seconds: parseFloat(document.getElementById('agent-buffer-seconds').value) || 0,
            send_intermediate_responses: document.getElementById('agent-send-intermediate').checked ? 1 : 0,
            enable_agent_state: document.getElementById('agent-state-enabled').checked ? 1 : 0,
            disable_parallel_tool_execution: document.getElementById('agent-parallel-exec').checked ? 1 : 0,
            disable_turn_prefetch: document.getElementById('agent-prefetch').checked ? 1 : 0,
            agent_messaging_enabled: document.getElementById('agent-messaging-enabled').checked ? 1 : 0,
            artifacts_enabled: document.getElementById('agent-artifacts').checked ? 1 : 0,
        })
    });
    const data = await res.json();
    if (!data.success) { showToast("Failed to save advanced settings.", "error"); return; }
    showToast("Advanced settings saved!", "success");
    } finally {
        setSaveLoading(btnId, false);
    }
}

// ==================== Knowledge Tab ====================
async function loadKBFiles() {
    const res = await fetch(`/api/agents/${AGENT_ID}/kb`);
    const data = await res.json();
    const files = data.files || [];
    const list = document.getElementById('kb-list');
    const empty = document.getElementById('kb-empty');
    if (files.length === 0) {
        list.innerHTML = '';
        empty.classList.remove('hidden');
        return;
    }
    empty.classList.add('hidden');
    list.innerHTML = files.map(f => `
        <div class="flex items-center justify-between p-3 bg-gray-50 rounded-md border border-gray-200 cursor-pointer hover:bg-gray-100 transition-colors dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-300" onclick="editFile('${f.filename}')">
            <div class="flex flex-col min-w-0">
                <span class="text-sm font-mono text-gray-800 truncate dark:text-gray-200" title="${f.filename}">${f.filename}</span>
                <span class="text-xs text-gray-400">${(f.size / 1024).toFixed(1)} KB</span>
            </div>
            <div class="flex gap-1">
                <button onclick="event.stopPropagation(); editFile('${f.filename}')" title="Edit" class="p-1.5 rounded text-indigo-500 hover:text-indigo-700 hover:bg-indigo-50 transition-colors">
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4"><path d="m5.433 13.917 1.262-3.155A4 4 0 0 1 7.58 9.42l6.92-6.918a2.121 2.121 0 0 1 3 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 0 1-.65-.65Z"/><path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0 0 10 3H4.75A2.75 2.75 0 0 0 2 5.75v9.5A2.75 2.75 0 0 0 4.75 18h9.5A2.75 2.75 0 0 0 17 15.25V10a.75.75 0 0 0-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5Z"/></svg>
                </button>
                <button onclick="event.stopPropagation(); deleteFile('${f.filename}')" title="Delete" class="p-1.5 rounded text-red-400 hover:text-red-600 hover:bg-red-50 transition-colors"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4"><path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 0 0 6 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 1 0 .23 1.482l.149-.022.841 10.518A2.75 2.75 0 0 0 7.596 19h4.807a2.75 2.75 0 0 0 2.742-2.53l.841-10.52.149.023a.75.75 0 0 0 .23-1.482A41.03 41.03 0 0 0 14 4.193V3.75A2.75 2.75 0 0 0 11.25 1h-2.5ZM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4ZM8.58 7.72a.75.75 0 0 0-1.5.06l.3 7.5a.75.75 0 1 0 1.5-.06l-.3-7.5Zm4.34.06a.75.75 0 1 0-1.5-.06l-.3 7.5a.75.75 0 1 0 1.5.06l.3-7.5Z" clip-rule="evenodd"/></svg></button>
            </div>
        </div>
    `).join('');
}

async function editFile(filename) {
    const res = await fetch(`/api/agents/${AGENT_ID}/kb/${encodeURIComponent(filename)}`);
    const data = await res.json();
    if (data.error) return alert(data.error);
    currentEditFile = filename;
    document.getElementById('kb-editor-filename').textContent = filename;
    document.getElementById('kb-editor-content').value = data.content;
    document.getElementById('kb-editor-modal').classList.remove('hidden');
}

function closeEditor() {
    document.getElementById('kb-editor-modal').classList.add('hidden');
    currentEditFile = null;
}

async function saveFile() {
    if (!currentEditFile) return;
    const btnId = 'saveFileBtn';
    setSaveLoading(btnId, true);
    try {
    const content = document.getElementById('kb-editor-content').value;
    await fetch(`/api/agents/${AGENT_ID}/kb/${encodeURIComponent(currentEditFile)}`, {
        method: 'PUT',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({content})
    });
    closeEditor();
    loadKBFiles();
    } finally {
        setSaveLoading(btnId, false);
    }
}

async function deleteFile(filename) {
    if (!await showConfirm({title: 'Delete File', message: `Delete "${filename}"? This cannot be undone.`, confirmText: 'Delete'})) return;
    await fetch(`/api/agents/${AGENT_ID}/kb/${encodeURIComponent(filename)}`, {method: 'DELETE'});
    if (currentEditFile === filename) closeEditor();
    loadKBFiles();
}

async function uploadFile(input) {
    const file = input.files[0];
    if (!file) return;
    const form = new FormData();
    form.append('file', file);
    await fetch(`/api/agents/${AGENT_ID}/kb`, {method: 'POST', body: form});
    input.value = '';
    loadKBFiles();
}

function showNewFileModal() {
    document.getElementById('new-file-modal').classList.remove('hidden');
    document.getElementById('new-file-name').value = '';
    document.getElementById('new-file-name').focus();
}
function closeNewFileModal() {
    document.getElementById('new-file-modal').classList.add('hidden');
}
async function createNewFile() {
    const fname = document.getElementById('new-file-name').value.trim();
    if (!fname) return alert('Filename is required.');
    await fetch(`/api/agents/${AGENT_ID}/kb`, {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({filename: fname, content: ''})
    });
    closeNewFileModal();
    loadKBFiles();
    editFile(fname);
}

// ==================== Tools Tab ====================
let allToolsDefs = [];
let assignedToolIds = [];
let currentFilterMode = 'assigned';

async function loadTools() {
    const [toolsRes, agentToolsRes] = await Promise.all([
        fetch('/api/settings/tools'),
        fetch(`/api/agents/${AGENT_ID}/tools`)
    ]);
    allToolsDefs = (await toolsRes.json()).tools || [];
    assignedToolIds = (await agentToolsRes.json()).tools || [];
    renderToolsList();
}

function isToolAssigned(toolId) {
    if (assignedToolIds.includes(toolId)) return true;
    // backward compat: bare function name stored in DB
    const tool = allToolsDefs.find(t => t.id === toolId);
    if (!tool) return false;
    const fnName = (tool.function || {}).name || tool.name || tool.id;
    return assignedToolIds.includes(fnName);
}

function renderToolsList() {
    const list = document.getElementById('tools-list');
    const knownIds = new Set(allToolsDefs.map(t => t.id));
    const fnNameToTool = {};
    for (const t of allToolsDefs) {
        const fn = (t.function || {}).name || t.name || t.id;
        if (!fnNameToTool[fn]) fnNameToTool[fn] = t;
    }
    const orphaned = assignedToolIds.filter(id => !knownIds.has(id) && !fnNameToTool[id]);

    const sortByName = (a, b) => (a.name || a.id).localeCompare(b.name || b.id);
    const builtins  = allToolsDefs.filter(t => t._builtin).sort(sortByName);
    const assigned  = allToolsDefs.filter(t => !t._builtin && isToolAssigned(t.id)).sort(sortByName);
    const available = allToolsDefs.filter(t => !t._builtin && !isToolAssigned(t.id)).sort(sortByName);

    let html = '';

    // Warning banner + rows for orphaned tools
    if (orphaned.length > 0) {
        html += `<div class="tool-row flex items-start gap-2.5 p-3 bg-amber-50 dark:bg-amber-900/40 border border-amber-200 dark:border-amber-700 rounded-md" data-assigned="true" data-builtin="false" data-tool-id="">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5 text-amber-500 flex-shrink-0 mt-0.5">
                <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 6a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 6Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd"/>
            </svg>
            <div>
                <p class="text-sm font-medium text-amber-800 dark:text-amber-200">${orphaned.length} tool(s) missing</p>
                <p class="text-xs text-amber-600 dark:text-amber-300 mt-0.5">The skill providing these tools may have been uninstalled. Remove them or reinstall the skill.</p>
            </div>
        </div>`;
        html += orphaned.map(id => `
        <div class="tool-row flex items-center justify-between p-3 bg-amber-50/60 dark:bg-amber-900/40 rounded-md border border-amber-200 dark:border-amber-700/50"
             data-assigned="true" data-builtin="false" data-tool-id="${escapeHtml(id)}">
            <div class="min-w-0 flex-1 flex items-center gap-2.5">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5 text-amber-500 flex-shrink-0">
                    <path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-8-5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0v-4.5A.75.75 0 0 1 10 5Zm0 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd"/>
                </svg>
                <div>
                    <span class="text-sm font-medium text-amber-700 dark:text-amber-200 font-mono">${escapeHtml(id)}</span>
                    <p class="text-xs text-amber-500 dark:text-amber-300/70">Missing — skill uninstalled</p>
                </div>
            </div>
            <button onclick="event.stopPropagation(); removeOrphan('${escapeHtml(id)}')" title="Remove"
                    class="ml-2 p-1.5 rounded text-amber-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4"><path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"/></svg>
            </button>
        </div>`).join('');
    }

    const renderRow = (t) => {
        const isBuiltin = !!t._builtin;
        const isAsgn = isBuiltin || isToolAssigned(t.id);
        const safeId = escapeHtml(t.id);
        return `
        <div class="tool-row flex items-center justify-between p-3 dark:bg-gray-800 dark:border-gray-600 rounded-md border transition-colors cursor-pointer
                    ${isAsgn ? 'bg-gray-50 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700' : 'bg-white border-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700'}"
             data-assigned="${isAsgn && !isBuiltin}"
             data-builtin="${isBuiltin}"
             data-tool-id="${safeId}"
             onclick="showToolDetail('${safeId}')">
            <div class="min-w-0 flex-1 overflow-hidden">
                <div class="flex flex-wrap items-center gap-1.5">
                    <span class="text-sm font-medium text-gray-800 dark:text-gray-200">${escapeHtml(t.name || t.id)}</span>
                    ${isBuiltin ? '<span class="text-xs bg-green-50 text-green-600 px-1.5 py-0.5 rounded">builtin</span>' : ''}
                    ${t._skill_id ? `<span class="text-xs bg-indigo-50 text-indigo-500 px-1.5 py-0.5 rounded">${escapeHtml(t._skill_id)}</span>` : ''}
                </div>
                <p class="text-xs text-gray-400 font-mono mt-0.5">${safeId}</p>
                <p class="text-xs text-gray-500 mt-0.5">${escapeHtml(t.description || '')}</p>
            </div>
            <label class="relative flex-shrink-0 ml-3 ${isBuiltin ? 'cursor-not-allowed' : 'cursor-pointer'}"
                   onclick="event.stopPropagation()"
                   title="${isBuiltin ? 'Always available' : (isAsgn ? 'Remove from agent' : 'Add to agent')}">
                <input type="checkbox" ${isAsgn ? 'checked' : ''} ${isBuiltin ? 'disabled' : ''}
                       onchange="toggleTool('${safeId}')" class="sr-only peer">
                <span class="dyn-toggle"></span>
            </label>
        </div>`;
    };

    html += [...builtins, ...assigned, ...available].map(renderRow).join('');
    list.innerHTML = html;
    filterToolsList();
    updateToolsBadge(assigned.length);
}

function updateToolsBadge(count) {
    const badge = document.getElementById('tools-badge');
    if (count > 0) {
        badge.textContent = count;
        badge.classList.remove('hidden');
    } else {
        badge.classList.add('hidden');
    }
}

function filterToolsList() {
    const query = (document.getElementById('tool-filter').value || '').toLowerCase().trim();
    const mode = currentFilterMode;
    const rows = document.querySelectorAll('#tools-list .tool-row');
    let visibleCount = 0;
    rows.forEach(row => {
        const isBuiltin = row.dataset.builtin === 'true';
        const isAssignedRow = row.dataset.assigned === 'true';
        const toolId = row.dataset.toolId;

        let show = true;
        if (mode === 'assigned') {
            show = isBuiltin || isAssignedRow || toolId === ''; // '' = orphan banner
        } else if (mode === 'available') {
            show = !isBuiltin && !isAssignedRow && toolId !== '';
        }

        if (show && query && toolId !== '') {
            const tool = allToolsDefs.find(t => t.id === toolId);
            const searchable = [
                tool ? (tool.name || tool.id) : toolId,
                tool ? (tool.description || '') : '',
                tool ? (tool._skill_id || '') : '',
                toolId
            ].join(' ').toLowerCase();
            show = searchable.includes(query);
        }

        row.classList.toggle('hidden', !show);
        if (show) visibleCount++;
    });
    document.getElementById('tools-empty').classList.toggle('hidden', visibleCount > 0);
}

function setFilterMode(mode) {
    currentFilterMode = mode;
    ['assigned', 'available', 'all'].forEach(m => {
        const btn = document.getElementById(`filter-btn-${m}`);
        const active = m === mode;
        btn.classList.toggle('bg-indigo-500', active);
        btn.classList.toggle('text-white', active);
        btn.classList.toggle('dark:bg-indigo-600', active);
        btn.classList.toggle('bg-white', !active);
        btn.classList.toggle('text-gray-600', !active);
        btn.classList.toggle('dark:bg-gray-700', !active);
        btn.classList.toggle('dark:text-gray-300', !active);
    });
    filterToolsList();
}

async function toggleTool(toolId) {
    const tool = allToolsDefs.find(t => t.id === toolId);
    const fnName = tool ? ((tool.function || {}).name || tool.name || tool.id) : toolId;
    if (isToolAssigned(toolId)) {
        // Remove by exact ID and bare function name (backward compat)
        assignedToolIds = assignedToolIds.filter(id => id !== toolId && id !== fnName);
    } else {
        // Remove any existing assignment with the same function name (swap)
        assignedToolIds = assignedToolIds.filter(aid => {
            if (aid === fnName) return false;
            const aDef = allToolsDefs.find(t => t.id === aid);
            if (aDef && ((aDef.function || {}).name || aDef.name || aDef.id) === fnName) return false;
            return true;
        });
        assignedToolIds.push(toolId);
    }
    renderToolsList();
    await saveToolsQuiet();
}

async function removeOrphan(toolId) {
    assignedToolIds = assignedToolIds.filter(id => id !== toolId);
    renderToolsList();
    await saveToolsQuiet();
}

async function saveToolsQuiet() {
    await fetch(`/api/agents/${AGENT_ID}/tools`, {
        method: 'PUT',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({tools: assignedToolIds})
    });
    const st = document.getElementById('tools-save-status');
    st.classList.remove('hidden');
    setTimeout(() => st.classList.add('hidden'), 1500);
}

// ==================== Skills Tab ====================
let allSkillsDefs = [];
let allowedSkillIds = [];

async function loadSkills() {
    const [skillsRes, agentSkillsRes] = await Promise.all([
        fetch('/api/skills'),
        fetch(`/api/agents/${AGENT_ID}/skills`)
    ]);
    const skillsData = await skillsRes.json();
    allSkillsDefs = (skillsData.skills || []).filter(s => s.enabled);
    allowedSkillIds = (await agentSkillsRes.json()).skills || [];
    renderSkillsList();
}

function renderSkillsList() {
    const list = document.getElementById('skills-list');
    const empty = document.getElementById('skills-empty');
    if (!list) return;
    if (!allSkillsDefs.length) {
        list.innerHTML = '';
        empty.classList.remove('hidden');
        return;
    }
    empty.classList.add('hidden');
    list.innerHTML = allSkillsDefs.map(skill => {
        const allowed = allowedSkillIds.includes(skill.id);
        return `<div class="flex items-start gap-3 p-3 rounded-lg border ${allowed ? 'border-indigo-200 bg-indigo-50' : 'border-gray-100 bg-white'} hover:border-indigo-200 transition-colors">
            <input type="checkbox" id="skill-${escapeHtml(skill.id)}" ${allowed ? 'checked' : ''}
                onchange="toggleSkill('${escapeHtml(skill.id)}')"
                class="mt-0.5 h-4 w-4 rounded border-gray-300 text-indigo-600 cursor-pointer flex-shrink-0">
            <label for="skill-${escapeHtml(skill.id)}" class="flex-1 cursor-pointer min-w-0">
                <div class="flex items-center gap-2 flex-wrap">
                    <span class="text-sm font-medium text-gray-800">${escapeHtml(skill.name || skill.id)}</span>
                    <span class="text-xs font-mono text-gray-400">${escapeHtml(skill.id)}</span>
                    ${skill.tool_count ? `<span class="text-xs bg-gray-100 text-gray-500 px-1.5 py-0.5 rounded">${skill.tool_count} tool${skill.tool_count !== 1 ? 's' : ''}</span>` : ''}
                </div>
                ${skill.description ? `<p class="text-xs text-gray-500 mt-0.5">${escapeHtml(skill.description)}</p>` : ''}
            </label>
        </div>`;
    }).join('');
}

async function toggleSkill(skillId) {
    if (allowedSkillIds.includes(skillId)) {
        allowedSkillIds = allowedSkillIds.filter(id => id !== skillId);
    } else {
        allowedSkillIds.push(skillId);
    }
    renderSkillsList();
    await saveSkillsQuiet();
}

async function saveSkillsQuiet() {
    await fetch(`/api/agents/${AGENT_ID}/skills`, {
        method: 'PUT',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({skills: allowedSkillIds})
    });
    const st = document.getElementById('skills-save-status');
    st.classList.remove('hidden');
    setTimeout(() => st.classList.add('hidden'), 1500);
}

let currentToolDetailId = null;

function renderToolDefinition(container, tool) {
    const fn = tool.function || {};
    const params = fn.parameters || {};
    const properties = params.properties || {};
    const required = new Set(params.required || []);

    const TYPE_COLORS = {
        string:  'bg-blue-50 text-blue-600',
        integer: 'bg-amber-50 text-amber-600',
        number:  'bg-amber-50 text-amber-600',
        boolean: 'bg-purple-50 text-purple-600',
        array:   'bg-teal-50 text-teal-600',
        object:  'bg-rose-50 text-rose-600',
    };

    const typeTag = (type) => {
        const cls = TYPE_COLORS[type] || 'bg-gray-100 text-gray-500';
        return `<span class="text-xs font-mono px-1.5 py-0.5 rounded ${cls}">${escapeHtml(type || 'any')}</span>`;
    };

    const entries = Object.entries(properties);
    if (entries.length === 0) {
        container.innerHTML = '';
        return;
    }

    const paramsHtml = entries.map(([name, schema]) => {
        const isReq = required.has(name);
        const itemType = schema.items ? schema.items.type : null;
        return `
        <div class="flex items-start gap-3 py-2.5 border-b border-gray-100 last:border-0">
            <div class="flex-1 min-w-0">
                <div class="flex flex-wrap items-center gap-1.5">
                    <span class="text-sm font-mono font-medium text-gray-800">${escapeHtml(name)}</span>
                    ${typeTag(schema.type)}
                    ${itemType ? `<span class="text-xs text-gray-400">of ${typeTag(itemType)}</span>` : ''}
                    ${isReq
                        ? `<span class="text-xs font-medium text-red-500 bg-red-50 px-1.5 py-0.5 rounded">required</span>`
                        : `<span class="text-xs text-gray-400 bg-gray-50 px-1.5 py-0.5 rounded">optional</span>`}
                </div>
                ${schema.description ? `<p class="text-xs text-gray-500 mt-1">${escapeHtml(schema.description)}</p>` : ''}
                ${schema.enum ? `<p class="text-xs text-gray-400 mt-1">One of: ${schema.enum.map(v => `<code class="bg-gray-100 px-1 rounded">${escapeHtml(String(v))}</code>`).join(', ')}</p>` : ''}
            </div>
        </div>`;
    }).join('');

    container.innerHTML = `
        <div>
            <p class="text-xs uppercase tracking-wide text-gray-400 font-semibold mb-1">Parameters
                <span class="normal-case font-normal text-gray-400 ml-1">(${entries.length} total, ${required.size} required)</span>
            </p>
            <div class="border border-gray-200 rounded-md px-3 divide-y divide-gray-100">${paramsHtml}</div>
        </div>`;
}

function switchToolTab(tab) {
    const tabs = ['prod', 'mock'];
    tabs.forEach(t => {
        document.getElementById(`tool-tab-${t}`).classList.toggle('hidden', t !== tab);
        const btn = document.getElementById(`tool-tab-btn-${t}`);
        btn.classList.toggle('border-indigo-500', t === tab);
        btn.classList.toggle('text-indigo-600', t === tab);
        btn.classList.toggle('border-transparent', t !== tab);
        btn.classList.toggle('text-gray-500', t !== tab);
    });
}

async function showToolDetail(toolId) {
    currentToolDetailId = toolId;
    try {
        // Look up from already-loaded allToolsDefs — avoids API/URL issues with skill IDs
        const tool = allToolsDefs.find(t => t.id === toolId);
        if (!tool) { console.warn('showToolDetail: tool not found', toolId); return; }

        document.getElementById('tool-detail-title').textContent = tool.name || tool.id;
        document.getElementById('tool-detail-id').textContent = tool.id;
        document.getElementById('tool-detail-desc').textContent = tool.description || 'No description';

        // Skill / builtin badge
        const skillBadge = document.getElementById('tool-detail-skill');
        if (tool._skill_id) {
            skillBadge.textContent = tool._skill_id;
            skillBadge.classList.remove('hidden');
        } else if (tool._builtin) {
            skillBadge.textContent = 'builtin';
            skillBadge.classList.remove('hidden');
        } else {
            skillBadge.classList.add('hidden');
        }

        // Parameters section (always visible, above CODE)
        renderToolDefinition(document.getElementById('tool-detail-definition'), tool);

        // Mock tab
        const mockPre = document.getElementById('tool-detail-mock');
        const noMock = document.getElementById('tool-detail-no-mock');
        if (tool.mock_response) {
            mockPre.classList.remove('hidden');
            noMock.classList.add('hidden');
            mockPre.textContent = typeof tool.mock_response === 'string'
                ? tool.mock_response
                : JSON.stringify(tool.mock_response, null, 2);
        } else {
            mockPre.classList.add('hidden');
            noMock.classList.remove('hidden');
        }

        // Prod tab — fetch backend code by bare function name (safe for API)
        const fnName = (tool.function && tool.function.name) || toolId;
        const backendRes = await fetch(`/api/settings/tools/${encodeURIComponent(fnName)}/backend`).catch(() => null);
        const backend = backendRes ? await backendRes.json().catch(() => ({})) : {};
        const codeEl = document.getElementById('tool-backend-code');
        if (backend.exists) {
            codeEl.value = backend.code;
        } else {
            codeEl.value = `"""Backend implementation for the ${fnName} tool."""\n\n\ndef execute(agent, args: dict) -> dict:\n    # agent: dict with agent_id, agent_name, user_id, channel_id, session_id\n    # args: dict of arguments passed by the LLM\n    return {"result": "not implemented"}\n`;
        }

        // Pre-fill test args from parameter schema
        let sampleArgs = '{}';
        const params = tool.function?.parameters?.properties;
        if (params) {
            const sample = {};
            for (const [k, v] of Object.entries(params)) {
                sample[k] = v.type === 'number' ? 0 : v.type === 'boolean' ? false : '';
            }
            sampleArgs = JSON.stringify(sample, null, 2);
        }
        document.getElementById('tool-test-args-real').value = sampleArgs;
        document.getElementById('tool-test-args-mock').value = sampleArgs;
        document.getElementById('tool-test-result-real').classList.add('hidden');
        document.getElementById('tool-test-result-mock').classList.add('hidden');
        document.getElementById('tool-test-status-real').classList.add('hidden');
        document.getElementById('tool-test-status-mock').classList.add('hidden');

        document.getElementById('tool-detail-edit-link').href = `/system#tools-${toolId}`;
        document.getElementById('tool-backend-save-status').classList.add('hidden');
        switchToolTab('prod');
        document.getElementById('tool-detail-modal').classList.remove('hidden');
    } catch(e) { console.error('showToolDetail error:', e); }
}

async function saveBackendCode() {
    if (!currentToolDetailId) return;
    const btnId = 'saveBackendCodeBtn';
    setSaveLoading(btnId, true);
    const code = document.getElementById('tool-backend-code').value;
    try {
        const res = await fetch(`/api/settings/tools/${currentToolDetailId}/backend`, {
            method: 'PUT',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({code})
        });
        const data = await res.json();
        const st = document.getElementById('tool-backend-save-status');
        if (data.success) {
            st.textContent = 'Saved!';
            st.classList.remove('hidden', 'text-red-600');
            st.classList.add('text-green-600');
        } else {
            st.textContent = data.error || 'Save failed';
            st.classList.remove('hidden', 'text-green-600');
            st.classList.add('text-red-600');
        }
        setTimeout(() => st.classList.add('hidden'), 2000);
    } catch(e) {
        const st = document.getElementById('tool-backend-save-status');
        st.textContent = 'Network error';
        st.classList.remove('hidden', 'text-green-600');
        st.classList.add('text-red-600');
        setTimeout(() => st.classList.add('hidden'), 2000);
    } finally {
        setSaveLoading(btnId, false);
    }
}

async function testToolExec(mode) {
    if (!currentToolDetailId) return;
    const argsEl = document.getElementById(`tool-test-args-${mode}`);
    const resultEl = document.getElementById(`tool-test-result-${mode}`);
    const statusEl = document.getElementById(`tool-test-status-${mode}`);

    let args = {};
    const raw = argsEl.value.trim();
    if (raw) {
        try { args = JSON.parse(raw); }
        catch(e) {
            resultEl.textContent = 'Invalid JSON: ' + e.message;
            resultEl.classList.remove('hidden', 'text-gray-700', 'border-gray-200');
            resultEl.classList.add('text-red-600', 'border-red-200');
            return;
        }
    }

    statusEl.textContent = 'Running...';
    statusEl.classList.remove('hidden');
    resultEl.classList.add('hidden');

    // Mock mode: run entirely client-side
    if (mode === 'mock') {
        const tool = allToolsDefs.find(t => t.id === currentToolDetailId);
        statusEl.classList.add('hidden');
        if (!tool || tool.mock_response == null) {
            resultEl.textContent = 'Error: No mock response defined for this tool';
            resultEl.className = 'mt-2 bg-red-50 border border-red-200 rounded-md p-3 text-xs font-mono whitespace-pre-wrap overflow-x-auto text-red-700';
            resultEl.classList.remove('hidden');
            resultEl.style.maxHeight = '200px';
            return;
        }
        try {
            let result;
            if (tool.mock_response_type === 'javascript') {
                // Capture console.log output and return value
                const lines = [];
                const mockFn = new Function('ARGS', 'console', tool.mock_response);
                const fakeConsole = { log: (...a) => lines.push(a.map(String).join(' ')) };
                const retVal = mockFn(args, fakeConsole);
                if (lines.length > 0) {
                    result = lines.join('\n');
                } else if (retVal !== undefined) {
                    result = retVal;
                } else {
                    result = '(no output)';
                }
            } else {
                result = tool.mock_response;
            }
            resultEl.textContent = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
            resultEl.className = 'mt-2 bg-gray-50 border border-gray-200 rounded-md p-3 text-xs font-mono whitespace-pre-wrap overflow-x-auto text-gray-700';
        } catch(e) {
            resultEl.textContent = 'Error: ' + e.message;
            resultEl.className = 'mt-2 bg-red-50 border border-red-200 rounded-md p-3 text-xs font-mono whitespace-pre-wrap overflow-x-auto text-red-700';
        }
        resultEl.classList.remove('hidden');
        resultEl.style.maxHeight = '200px';
        return;
    }

    try {
        const res = await fetch(`/api/settings/tools/${currentToolDetailId}/test`, {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({args, mode})
        });
        const data = await res.json();
        statusEl.classList.add('hidden');

        if (data.error) {
            resultEl.textContent = 'Error: ' + data.error;
            resultEl.className = 'mt-2 bg-red-50 border border-red-200 rounded-md p-3 text-xs font-mono whitespace-pre-wrap overflow-x-auto text-red-700';
        } else {
            resultEl.textContent = JSON.stringify(data.result, null, 2);
            resultEl.className = 'mt-2 bg-gray-50 border border-gray-200 rounded-md p-3 text-xs font-mono whitespace-pre-wrap overflow-x-auto text-gray-700';
        }
        resultEl.style.maxHeight = '200px';
    } catch(e) {
        statusEl.classList.add('hidden');
        resultEl.textContent = 'Network error: ' + e.message;
        resultEl.className = 'mt-2 bg-red-50 border border-red-200 rounded-md p-3 text-xs font-mono whitespace-pre-wrap overflow-x-auto text-red-700';
        resultEl.style.maxHeight = '200px';
    }
}

// ==================== Channels Tab ====================
async function loadChannels() {
    const res = await fetch(`/api/agents/${AGENT_ID}/channels`);
    const data = await res.json();
    const channels = data.channels || [];
    const list = document.getElementById('channels-list');
    const empty = document.getElementById('channels-empty');
    if (channels.length === 0) {
        list.innerHTML = '';
        empty.classList.remove('hidden');
        return;
    }
    empty.classList.add('hidden');
    list.innerHTML = channels.map(ch => {
        const typeBadge = ch.type === 'telegram' ? 'bg-blue-100 text-blue-700' :
                          ch.type === 'whatsapp' ? 'bg-green-100 text-green-700' :
                          'bg-purple-100 text-purple-700';
        const running = ch.running;
        const statusDot = running ? 'bg-green-500' : 'bg-gray-300';
        const statusText = running ? 'Running' : 'Stopped';
        const chMode = (ch.config && ch.config.mode) || 'open';
        const modeBadgeCls = chMode === 'restricted' ? 'bg-amber-100 text-amber-700' : 'bg-gray-100 text-gray-600';
        const isPrimary = ch.is_primary;
        const primaryIcon = isPrimary ? '<span class="text-yellow-500" title="Primary channel">&#9733;</span>' : '';
        const toggleBtn = running
            ? `<button onclick="event.stopPropagation(); toggleChannel('${ch.id}', false)" class="text-xs px-3 py-1 rounded bg-red-50 text-red-600 hover:bg-red-100">Stop</button>`
            : `<button onclick="event.stopPropagation(); toggleChannel('${ch.id}', true)" class="text-xs px-3 py-1 rounded bg-green-50 text-green-600 hover:bg-green-100">Start</button>`;
        const qrBtn = (ch.type === 'whatsapp' && running)
            ? `<button onclick="event.stopPropagation(); openQRModal('${ch.id}')" class="text-xs px-3 py-1 rounded bg-green-50 text-green-700 hover:bg-green-100">Scan QR</button>`
            : '';
        const primaryBtn = isPrimary
            ? `<button onclick="event.stopPropagation(); unsetPrimaryChannel('${ch.id}')" class="text-xs px-3 py-1 rounded bg-yellow-50 text-yellow-600 hover:bg-yellow-100" title="Remove as primary">&#9733;</button>`
            : `<button onclick="event.stopPropagation(); setPrimaryChannel('${ch.id}')" class="text-xs px-3 py-1 rounded bg-gray-100 text-gray-600 hover:bg-gray-200" title="Set as primary channel for message routing">&#9734;</button>`;
        return `
        <div class="flex items-center justify-between p-4 bg-gray-50 rounded-md border border-gray-100 cursor-pointer" onclick="showChannelDetail('${ch.id}')">
            <div>
                <div class="flex items-center gap-2 mb-1">
                    <span class="text-sm font-medium text-gray-800">${ch.name || 'Unnamed'}</span>
                    ${primaryIcon}
                </div>
                <div class="flex items-center gap-1.5 mb-1">
                    <span class="text-xs px-2 py-0.5 rounded font-medium ${typeBadge}">${ch.type}</span>
                    <span class="text-xs px-2 py-0.5 rounded font-medium ${modeBadgeCls}">${chMode}</span>
                </div>
                <div class="flex items-center gap-1.5">
                    <span class="inline-block w-2 h-2 rounded-full ${statusDot}"></span>
                    <span class="text-xs text-gray-400">${statusText}</span>
                </div>
            </div>
            <div class="flex items-center gap-2">
                ${primaryBtn}
                ${qrBtn}
                ${toggleBtn}
                <button onclick="event.stopPropagation(); deleteChannel('${ch.id}')" class="text-xs text-red-500 hover:text-red-700">Delete</button>
            </div>
        </div>`;
    }).join('');
}

function showAddChannelModal() {
    document.getElementById('channel-modal').classList.remove('hidden');
    updateChannelConfigFields();
}
function closeChannelModal() {
    document.getElementById('channel-modal').classList.add('hidden');
}
function updateChannelConfigFields() {
    const type = document.getElementById('channel-type').value;
    const container = document.getElementById('channel-config-fields');
    const nameInput = document.getElementById('channel-name');
    if (type === 'telegram') {
        nameInput.placeholder = 'e.g., Main Bot';
        container.innerHTML = `
            <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Bot Token</label>
            <input type="text" id="channel-bot-token" placeholder="123456:ABC-DEF..." class="w-full p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">`;
    } else if (type === 'whatsapp') {
        nameInput.placeholder = 'e.g., Main WhatsApp Bot';
        container.innerHTML = `
            <p class="text-sm text-gray-500 dark:text-gray-400">After adding, scan the QR code with WhatsApp on your phone to connect. No API keys required.</p>`;
    } else {
        nameInput.placeholder = 'e.g., Main Discord Bot';
        container.innerHTML = `
            <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Bot Token</label>
            <input type="text" id="channel-bot-token" placeholder="Discord bot token" class="w-full p-2.5 border border-gray-300 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">`;
    }
}

async function addChannel() {
    const type = document.getElementById('channel-type').value;
    const name = document.getElementById('channel-name').value.trim();
    let config = {};
    if (type === 'telegram' || type === 'discord') {
        config.bot_token = (document.getElementById('channel-bot-token')?.value || '').trim();
    } else if (type === 'whatsapp') {
        // Baileys-based: no API keys needed
    }
    const res = await fetch(`/api/agents/${AGENT_ID}/channels`, {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({type, name, config: {...config, mode: 'restricted'}})
    });
    const data = await res.json();
    if (data.error) return alert(data.error);
    closeChannelModal();
    loadChannels();
    if (type === 'whatsapp' && data.channel?.id) {
        openQRModal(data.channel.id);
    }
}

// ---- WhatsApp QR Modal ----
let _qrPollInterval = null;
let _qrChannelId = null;

function openQRModal(channelId) {
    _qrChannelId = channelId;
    document.getElementById('whatsapp-qr-modal').classList.remove('hidden');
    document.getElementById('qr-image').classList.add('hidden');
    document.getElementById('qr-spinner').classList.remove('hidden');
    document.getElementById('qr-connected-msg').classList.add('hidden');
    document.getElementById('qr-instructions').classList.remove('hidden');
    document.getElementById('qr-status-text').textContent = 'Loading QR code...';
    _startQRPolling();
}

function closeQRModal() {
    document.getElementById('whatsapp-qr-modal').classList.add('hidden');
    _stopQRPolling();
}

function _startQRPolling() {
    _stopQRPolling();
    _qrPollInterval = setInterval(_pollQR, 3000);
    _pollQR(); // immediate first call
}

function _stopQRPolling() {
    if (_qrPollInterval) {
        clearInterval(_qrPollInterval);
        _qrPollInterval = null;
    }
}

async function _pollQR() {
    if (!_qrChannelId) return;
    try {
        const res = await fetch(`/api/agents/${AGENT_ID}/channels/${_qrChannelId}/qr`);
        if (!res.ok) return;
        const data = await res.json();
        if (data.status === 'connected') {
            _stopQRPolling();
            document.getElementById('qr-image').classList.add('hidden');
            document.getElementById('qr-spinner').classList.add('hidden');
            document.getElementById('qr-instructions').classList.add('hidden');
            document.getElementById('qr-connected-msg').classList.remove('hidden');
            document.getElementById('qr-status-text').textContent = '';
            loadChannels();
        } else if (data.qr) {
            document.getElementById('qr-image').src = data.qr;
            document.getElementById('qr-image').classList.remove('hidden');
            document.getElementById('qr-spinner').classList.add('hidden');
            document.getElementById('qr-status-text').textContent = 'Scan with WhatsApp on your phone';
        } else {
            document.getElementById('qr-status-text').textContent = 'Waiting for QR code...';
        }
    } catch (e) {
        // ignore poll errors
    }
}

async function toggleChannel(channelId, start) {
    const action = start ? 'start' : 'stop';
    const res = await fetch(`/api/agents/${AGENT_ID}/channels/${channelId}/${action}`, {method: 'POST'});
    const data = await res.json();
    if (data.error) return alert(data.error);
    loadChannels();
}

async function deleteChannel(channelId) {
    if (!await showConfirm({title: 'Delete Channel', message: 'Delete this channel? This cannot be undone.', confirmText: 'Delete'})) return;
    await fetch(`/api/agents/${AGENT_ID}/channels/${channelId}`, {method: 'DELETE'});
    loadChannels();
}

async function setPrimaryChannel(channelId) {
    const res = await fetch(`/api/agents/${AGENT_ID}/channels/${channelId}/set-primary`, {method: 'POST'});
    const data = await res.json();
    if (data.error) return alert(data.error);
    loadChannels();
}

async function unsetPrimaryChannel(channelId) {
    const res = await fetch(`/api/agents/${AGENT_ID}/channels/${channelId}/unset-primary`, {method: 'POST'});
    const data = await res.json();
    if (data.error) return alert(data.error);
    loadChannels();
}

// ==================== Channel Detail Modal ====================
let _cdChannelId = null;
let _cdPollInterval = null;

function _cdGetChannelConfig() {
    // Fetch fresh channel data from API
    return fetch(`/api/agents/${AGENT_ID}/channels`).then(r => r.json()).then(d => {
        const ch = (d.channels || []).find(c => c.id === _cdChannelId);
        return ch || null;
    });
}

async function showChannelDetail(channelId) {
    _cdChannelId = channelId;
    document.getElementById('channel-detail-modal').classList.remove('hidden');
    await _cdRefresh();
    // Auto-refresh pending approvals every 30s
    _cdPollInterval = setInterval(_cdRefreshPending, 30000);
}

function closeChannelDetail() {
    document.getElementById('channel-detail-modal').classList.add('hidden');
    _cdChannelId = null;
    if (_cdPollInterval) {
        clearInterval(_cdPollInterval);
        _cdPollInterval = null;
    }
}

async function _cdRefresh() {
    const ch = await _cdGetChannelConfig();
    if (!ch) return;
    const config = ch.config || {};
    const mode = config.mode || 'open';

    // Channel info
    document.getElementById('cd-channel-name').textContent = ch.name || 'Unnamed';
    document.getElementById('channel-detail-title').textContent = `Channel: ${ch.name || 'Unnamed'} - ${AGENT_ID}`;
    const badge = ch.type === 'telegram' ? 'bg-blue-100 text-blue-700' :
                  ch.type === 'whatsapp' ? 'bg-green-100 text-green-700' :
                  'bg-purple-100 text-purple-700';
    document.getElementById('cd-channel-type-badge').textContent = ch.type;
    document.getElementById('cd-channel-type-badge').className = `text-xs px-2 py-0.5 rounded font-medium ${badge}`;
    document.getElementById('cd-mode-toggle').checked = (mode === 'restricted');
    const running = ch.running;
    document.getElementById('cd-channel-status-dot').className = `inline-block w-2 h-2 rounded-full ${running ? 'bg-green-500' : 'bg-gray-300'}`;
    document.getElementById('cd-channel-status-text').textContent = running ? 'Running' : 'Stopped';

    await _cdRefreshPending();
    _cdRenderAllowlist(config.allowed_users || [], config.user_names || {}, config.names_needed || []);
}

async function _cdRefreshPending() {
    if (!_cdChannelId) return;
    try {
        const res = await fetch(`/api/agents/${AGENT_ID}/channels/${_cdChannelId}/pending-approvals`);
        const data = await res.json();
        const approvals = data.pending_approvals || [];
        _cdRenderPending(approvals);
    } catch(e) {
        // ignore network errors silently
    }
}

function _cdRenderPending(approvals) {
    const container = document.getElementById('cd-pending-list');
    const countEl = document.getElementById('cd-pending-count');
    countEl.textContent = approvals.length > 0 ? `${approvals.length} pending` : '';
    if (approvals.length === 0) {
        container.innerHTML = '<p class="text-xs text-gray-400 text-center py-3">No pending approvals.</p>';
        return;
    }
    container.innerHTML = approvals.map(a => {
        const pairDisplay = a.pair_code || '-';
        const userName = a.user_name || '-';
        const createdAt = a.created_at ? new Date(a.created_at + 'Z').toLocaleString() : '-';
        return `
        <div class="flex items-center justify-between p-2.5 bg-amber-50 dark:bg-amber-900/20 rounded-lg border border-amber-200 dark:border-amber-800">
            <div class="min-w-0 flex-1 mr-2">
                <div class="text-xs font-medium text-gray-800 dark:text-gray-100 truncate">${userName}</div>
                <div class="text-xs text-gray-500 dark:text-gray-400">ID: ${a.external_user_id}</div>
                <div class="text-xs text-gray-500 dark:text-gray-400">Code: ${pairDisplay}</div>
                <div class="text-xs text-gray-400">${createdAt}</div>
            </div>
            <div class="flex gap-1.5 flex-shrink-0">
                <button onclick="approvePending('${a.id}')" class="px-2.5 py-1 text-xs rounded bg-green-500 text-white hover:bg-green-600">Approve</button>
                <button onclick="rejectPending('${a.id}')" class="px-2.5 py-1 text-xs rounded bg-red-500 text-white hover:bg-red-600">Reject</button>
            </div>
        </div>`;
    }).join('');
}

function _cdRenderAllowlist(users, userNames, namesNeeded) {
    const container = document.getElementById('cd-allowlist');
    if (!users || users.length === 0) {
        container.innerHTML = '<p class="text-xs text-gray-400 text-center py-2">No users in allowlist.</p>';
        return;
    }
    container.innerHTML = users.map(u => {
        const name = userNames[u] || '';
        const needsName = namesNeeded && namesNeeded.includes(u);
        const nameDisplay = needsName
            ? '<span class="text-amber-600 dark:text-amber-400 italic">(name pending...)</span>'
            : (name ? `<span class="text-gray-600 dark:text-gray-400">— ${name}</span>` : '<span class="text-gray-400 italic">unknown</span>');
        return `
        <div class="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700/50 rounded-md border border-gray-100 dark:border-gray-600">
            <div class="flex items-center gap-2 min-w-0">
                <span class="text-xs text-gray-700 dark:text-gray-300 font-mono flex-shrink-0">${u}</span>
                <span class="text-xs truncate">${nameDisplay}</span>
            </div>
            <button onclick="removeAllowedUser('${u}')" class="text-xs text-red-500 hover:text-red-700 flex-shrink-0 ml-2">Remove</button>
        </div>`;
    }).join('');
}

async function approvePending(pendingId) {
    const res = await fetch(`/api/agents/${AGENT_ID}/channels/${_cdChannelId}/pending-approvals/${pendingId}/approve`, {method: 'POST'});
    const data = await res.json();
    if (data.error) { showToast(data.error, 'error'); return; }
    showToast('User approved!', 'success');
    await _cdRefresh();
}

async function rejectPending(pendingId) {
    const res = await fetch(`/api/agents/${AGENT_ID}/channels/${_cdChannelId}/pending-approvals/${pendingId}/reject`, {method: 'POST'});
    const data = await res.json();
    if (data.error) { showToast(data.error, 'error'); return; }
    showToast('User rejected.', 'success');
    await _cdRefresh();
}

async function removeAllowedUser(userId) {
    if (!await showConfirm({title: 'Remove user', message: `Remove ${userId} from allowlist?`, confirmText: 'Remove'})) return;
    const ch = await _cdGetChannelConfig();
    if (!ch) return;
    const config = ch.config || {};
    let users = config.allowed_users || [];
    users = users.filter(u => u !== userId);
    config.allowed_users = users;
    const res = await fetch(`/api/agents/${AGENT_ID}/channels/${_cdChannelId}`, {
        method: 'PUT',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({config})
    });
    const data = await res.json();
    if (data.error) { showToast(data.error, 'error'); return; }
    showToast('User removed from allowlist.', 'success');
    _cdRenderAllowlist(users, config.user_names || {}, config.names_needed || []);
}

async function addAllowedUser() {
    const input = document.getElementById('cd-add-user-input');
    const userId = input.value.trim();
    if (!userId) { showToast('Please enter a user ID.', 'error'); return; }
    const ch = await _cdGetChannelConfig();
    if (!ch) return;
    const config = ch.config || {};
    let users = config.allowed_users || [];
    if (users.includes(userId)) { showToast('User already in allowlist.', 'error'); return; }
    users.push(userId);
    config.allowed_users = users;
    const res = await fetch(`/api/agents/${AGENT_ID}/channels/${_cdChannelId}`, {
        method: 'PUT',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({config})
    });
    const data = await res.json();
    if (data.error) { showToast(data.error, 'error'); return; }
    showToast('User added to allowlist!', 'success');
    input.value = '';
    _cdRenderAllowlist(users, config.user_names || {}, config.names_needed || []);
}

// ==================== Pairing Code Generator ====================

async function generatePairCode() {
    try {
        const res = await fetch(`/api/agents/${AGENT_ID}/channels/${_cdChannelId}/generate-pair-code`, { method: 'POST' });
        const data = await res.json();
        if (data.error) { showToast(data.error, 'error'); return; }
        const display = document.getElementById('cd-pair-code-display');
        const copyBtn = document.getElementById('cd-pair-copy-btn');
        display.textContent = data.pair_code;
        copyBtn.classList.remove('hidden');
        showToast('Pairing code generated! Valid for 5 minutes.', 'success');
    } catch(e) {
        showToast('Failed to generate pairing code.', 'error');
    }
}

function copyPairCode() {
    const code = document.getElementById('cd-pair-code-display').textContent;
    if (!code) return;
    navigator.clipboard.writeText(code).then(() => {
        showToast('Code copied to clipboard!', 'success');
    }).catch(() => {
        showToast('Failed to copy code.', 'error');
    });
}

async function toggleChannelMode(enabled) {
    const newMode = enabled ? 'restricted' : 'open';
    const ch = await _cdGetChannelConfig();
    if (!ch) return;
    const config = ch.config || {};
    config.mode = newMode;
    const res = await fetch(`/api/agents/${AGENT_ID}/channels/${_cdChannelId}`, {
        method: 'PUT',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({config})
    });
    const data = await res.json();
    if (data.error) { showToast(data.error, 'error'); return; }
    showToast(`Channel mode set to ${newMode}`, 'success');
    // Refresh both the channels list and detail modal
    loadChannels();
    _cdRefresh();
}

// ==================== Chat ====================
let lastChatTs = 0;  // epoch-ms cursor for JSONL-based polling
let lastTurnEndTs = 0;  // ts of last turn_end rendered from chatlog history
let _chatSessionId = null;  // cached session_id for this user

async function _getChatSessionId() {
    if (_chatSessionId) return _chatSessionId;
    try {
        const res = await fetch(`/api/agents/${AGENT_ID}/chat/session?user_id=web_test`);
        const data = await res.json();
        _chatSessionId = data.session_id || null;
    } catch(e) { /* ignore */ }
    return _chatSessionId;
}

// Pass { persistent: true } when called from incremental idle polling so the thinking
// bubble persists across multiple poll calls within a single turn.
function renderEntries(entries, { persistent = false } = {}) {
    // Render JSONL entries into the chat UI, grouping thinking/tool events into bubbles.
    let thinkingId = persistent ? _pollThinkingId : null;
    for (const entry of entries) {
        if (entry.ts > lastChatTs) lastChatTs = entry.ts;
        const type = entry.type;
        if (type === 'user') {
            if (thinkingId) { chatUI.finalizeThinkingBubble(thinkingId); thinkingId = null; }
            chatUI.appendMessage('user', entry.content, {metadata: entry.metadata});
        } else if (type === 'thinking') {
            if (!thinkingId) thinkingId = chatUI.showThinkingIndicator();
            chatUI.appendTimelineEntry(thinkingId, {type: 'thinking', content: entry.content});
        } else if (type === 'tool_call') {
            if (!thinkingId) thinkingId = chatUI.showThinkingIndicator();
            chatUI.appendTimelineEntry(thinkingId, {
                type: 'tool_call', tool: entry.function, args: entry.params || {}, param_types: {}
            });
        } else if (type === 'tool_output') {
            if (!thinkingId) thinkingId = chatUI.showThinkingIndicator();
            let result;
            try { result = JSON.parse(entry.content); } catch(e) { result = {data: entry.content}; }
            chatUI.appendTimelineEntry(thinkingId, {type: 'tool_result', tool: entry.function, result, error: !!entry.error});
        } else if (type === 'intermediate') {
            if (!thinkingId) thinkingId = chatUI.showThinkingIndicator();
            chatUI.appendTimelineEntry(thinkingId, {type: 'response', content: entry.content});
        } else if (type === 'final') {
            const meta = entry.metadata || {};
            if (thinkingId) { chatUI.finalizeThinkingBubble(thinkingId, meta.thinking_duration); thinkingId = null; }
            // Concurrency-limited busy-ack: render as a system notification with distinct styling
            if (meta.concurrency_limited || meta.busy_ack) {
                chatUI.appendMessage('system', '[SYSTEM/Concurrency]\n' + entry.content, {metadata: meta});
            } else if (meta.error) {
                chatUI.appendMessage('error', entry.content, {metadata: meta});
            } else {
                chatUI.appendMessage('assistant', entry.content, {metadata: meta});
            }
        } else if (type === 'error') {
            const meta = entry.metadata || {};
            if (thinkingId) { chatUI.finalizeThinkingBubble(thinkingId, meta.thinking_duration); thinkingId = null; }
            chatUI.appendMessage('error', entry.content, {metadata: meta});
        } else if (type === 'system') {
            chatUI.appendMessage('system', entry.content, {metadata: entry.metadata});
        } else if (type === 'turn_end') {
            if (thinkingId) { chatUI.finalizeThinkingBubble(thinkingId, entry.thinking_duration); thinkingId = null; }
            if (entry.ts && entry.ts > lastTurnEndTs) lastTurnEndTs = entry.ts;
        }
        // turn_begin, pending: no rendering needed for history
    }
    if (persistent) {
        _pollThinkingId = thinkingId;  // save for next incremental poll call
    } else if (thinkingId) {
        // In-progress turn detected: remove the incomplete bubble so restoreActiveReasoning
        // can create a fresh one from the event buffer without duplicating entries.
        chatUI.removeThinkingIndicator(thinkingId);
    }
}

function _showChatLoadingSkeleton() {
    const c = document.getElementById('chat-messages');
    if (!c) return;
    const skeletonRow = (wide) => `
        <div class="flex items-end gap-2 ${wide ? '' : 'flex-row-reverse'}">
            <div class="w-7 h-7 rounded-full bg-gray-200 dark:bg-gray-700 animate-pulse flex-shrink-0"></div>
            <div class="space-y-1.5 ${wide ? 'max-w-xs' : 'max-w-sm'} w-full">
                <div class="h-3 rounded-full bg-gray-200 dark:bg-gray-700 animate-pulse ${wide ? 'w-3/4' : 'w-full'}"></div>
                <div class="h-3 rounded-full bg-gray-200 dark:bg-gray-700 animate-pulse ${wide ? 'w-1/2' : 'w-5/6'}"></div>
            </div>
        </div>`;
    c.innerHTML = `
        <div id="chat-loading-skeleton" class="space-y-4 py-2">
            ${skeletonRow(false)}
            ${skeletonRow(true)}
            ${skeletonRow(false)}
            ${skeletonRow(true)}
            ${skeletonRow(false)}
        </div>`;
}

async function loadChatHistory() {
    _pollThinkingId = null;
    lastChatTs = 0;
    _showChatLoadingSkeleton();
    try {
        // Fetch session_id and history in parallel — don't block history on session lookup
        const sidPromise = _getChatSessionId();
        const res = await fetch(`/api/agents/${AGENT_ID}/chat?user_id=web_test&limit=50`);
        await sidPromise; // ensure cache is primed before restoreActiveReasoning runs
        const data = await res.json();
        chatUI.clearContainer();
        chatUI.batchRender(() => renderEntries(data.entries || []));
    } catch(e) { /* ignore load errors */ }
    chatLoaded = true;
    setTimeout(() => chatUI.scrollToBottom(), 50);
    if (!_chatBusy) startIdlePoll();
    restoreActiveReasoning(); // fire-and-forget — history already rendered
}

async function restoreActiveReasoning() {
    try {
        // Use cached session_id — already fetched in loadChatHistory()
        const sessionId = await _getChatSessionId();
        if (!sessionId || _chatBusy || _currentTurn) return; // bail if user already sent a message or turn exists

        // Fetch all buffered events for this session
        const eRes = await fetch(`/api/agents/${encodeURIComponent(AGENT_ID)}/chat/events?session_id=${encodeURIComponent(sessionId)}&after=0`);
        if (!eRes.ok || _chatBusy || _currentTurn) return; // bail if user sent a message while fetching
        const eData = await eRes.json();
        if (_chatBusy || _currentTurn) return; // bail before touching shared state
        const events = eData.events || [];
        if (!events.length) return;

        // Only replay events from the current in-progress turn.
        // Discard everything up to and including the last 'done' OR 'turn_split' so
        // that pre-split events (already rendered by renderEntries from JSONL) are not
        // replayed again — which would create a duplicate thinking bubble for turn 1.
        const lastDoneIdx = events.reduce((idx, ev, i) => ev.event === 'done' ? i : idx, -1);
        const lastSplitIdx = events.reduce((idx, ev, i) => ev.event === 'turn_split' ? i : idx, -1);
        const cutIdx = Math.max(lastDoneIdx, lastSplitIdx);
        const replayEvents = cutIdx >= 0 ? events.slice(cutIdx + 1) : events;
        if (!replayEvents.length) return;

        // Guard: if the in-progress turn started before the last turn_end rendered
        // from chatlog history, that turn was already fully finalized — skip to avoid
        // creating a duplicate thinking bubble above completed messages.
        const firstReplayEvt = replayEvents.find(ev => ev.event === 'turn_begin');
        if (firstReplayEvt && firstReplayEvt.data && firstReplayEvt.data.ts &&
                firstReplayEvt.data.ts < lastTurnEndTs) {
            return;
        }

        // Stop the idle poll before creating the live bubble. The idle poll may
        // have fired during our async awaits (before _currentTurn was set) and
        // created a stale _pollThinkingId that would duplicate the live bubble.
        // pollForResponse → setChatBusy(false) will restart the idle poll when done.
        stopIdlePoll();

        // Replay trace into a new thinking bubble.
        // Find the last user message to anchor the bubble below it.
        const container = document.getElementById('chat-messages');
        const userWrappers = container ? container.querySelectorAll('[data-msg-role="user"]') : [];
        const userMsgEl = userWrappers.length ? userWrappers[userWrappers.length - 1] : null;
        let resumeSeq = 0;
        const turnBeginEvt = replayEvents.find(ev => ev.event === 'turn_begin');
        let thinkingId = chatUI.showThinkingIndicator(turnBeginEvt ? turnBeginEvt.data.ts : null, userMsgEl);
        for (const ev of replayEvents) {
            if (ev.seq > resumeSeq) resumeSeq = ev.seq;
            if (ev.event === 'thinking') {
                chatUI.appendTimelineEntry(thinkingId, {type: 'thinking', content: ev.data.content});
            } else if (ev.event === 'tool_call_started') {
                chatUI.appendTimelineEntry(thinkingId, {type: 'tool_call', tool: ev.data.tool, args: ev.data.args, param_types: ev.data.param_types || {}});
            } else if (ev.event === 'tool_executed') {
                chatUI.appendTimelineEntry(thinkingId, {type: 'tool_result', tool: ev.data.tool, result: ev.data.result, error: ev.data.error});
            } else if (ev.event === 'response_chunk' && ev.data.is_final && ev.data.content) {
                chatUI.appendTimelineEntry(thinkingId, {type: 'response', content: ev.data.content});
            } else if (ev.event === 'turn_split') {
                chatUI.finalizeThinkingBubble(thinkingId);
                thinkingId = chatUI.showThinkingIndicator(null, userMsgEl);
            }
        }

        _currentTurn = { abortController: null, thinkingId, stream: null, pollTimer: null };

        chatUI.scrollToBottom();

        // Connect SSE from resumeSeq so the backend only sends the gap
        _currentTurn.stream = chatUI.connectThinkingStream(
            `/api/agents/${encodeURIComponent(AGENT_ID)}/chat/stream?session_id=${encodeURIComponent(sessionId)}&after=${resumeSeq}`,
            thinkingId,
            { userMsgEl, onSplit: (newId) => { _currentTurn.thinkingId = newId; }, onDone: () => { if (_currentTurn) _currentTurn.thinkingId = null; } }
        );

        // Poll until the final response lands
        pollForResponse(thinkingId);
    } catch(e) { /* silently ignore */ }
}

let chatPollTimer = null;
let chatIdleTimer = null;
let chatPolling = false;
let _chatBusy = false;
let _currentTurn = null;  // { abortController, thinkingId, stream, pollTimer }
let _pollThinkingId = null;  // active thinking bubble from idle poll (persists across calls)

function updateBusyBadge(busy) {
    const dot = document.getElementById('state-busy-dot');
    const mobileDot = document.getElementById('mobile-state-busy-dot');
    [dot, mobileDot].forEach(el => {
        if (!el) return;
        if (busy) {
            el.classList.remove('hidden');
            el.classList.add('inline-flex');
        } else {
            el.classList.add('hidden');
            el.classList.remove('inline-flex');
        }
    });
}

function _destroyCurrentTurn() {
    if (!_currentTurn) return;
    if (_currentTurn.abortController) _currentTurn.abortController.abort();
    // Explicitly stop the SSEAdapter — chatUI.closeStream() is a no-op shim.
    // Without this, old adapters keep reconnecting from a stale _lastSeq,
    // fetching all historical events on every reconnect → OOM renderer crash.
    if (_currentTurn.stream && _currentTurn.stream.stop) {
        console.warn('[_destroyCurrentTurn] stopping stream _lastSeq=', _currentTurn.stream._lastSeq);
        _currentTurn.stream.stop();
        _currentTurn.stream = null;
    }
    if (_currentTurn.thinkingId) {
        chatUI.removeThinkingIndicator(_currentTurn.thinkingId);
    }
    if (_currentTurn.pollTimer) {
        clearInterval(_currentTurn.pollTimer);
    }
    _currentTurn = null;
    // Safety net: if the SSE gap-fill prevented 'done' delivery, the Turn object
    // may still be in chatUI._turns with _finalized=false. hasActiveStream() would
    // then return true permanently, blocking the idle poll from showing the response.
    // Force-clear any stranded non-finalized turns so the idle poll can resume.
    if (chatUI.hasActiveStream()) {
        console.warn('[_destroyCurrentTurn] stranded active turn detected — force-clearing so idle poll can resume');
        chatUI.clearActiveSpinner();
    }
}

function setChatBusy(busy) {
    _chatBusy = busy;
    updateBusyBadge(busy);
    const input = document.getElementById('chat-input');
    if (!busy) {
        input.focus();
        if (!chatUI.hasActiveStream()) _destroyCurrentTurn();
        startIdlePoll();
    } else {
        stopIdlePoll();
    }
}

// Background poll for busy state set by external sessions (notifier, other channels)
let _busyPolling = false;
setInterval(async () => {
    if (_chatBusy || _busyPolling) return; // skip if already busy or previous request still in-flight
    _busyPolling = true;
    try {
        const res = await fetch(`/api/agents/${AGENT_ID}/busy`);
        const data = await res.json();
        updateBusyBadge(data.busy);
    } catch (e) { /* ignore */ } finally { _busyPolling = false; }
}, 5000);

function startIdlePoll() {
    if (chatIdleTimer) return; // already running
    chatIdleTimer = setInterval(async () => {
        if (_chatBusy || chatUI.hasActiveStream()) return; // skip during active turn or SSE stream
        try {
            const sid = await _getChatSessionId();
            const qs = sid
                ? `session_id=${encodeURIComponent(sid)}&after_ts=${lastChatTs}&limit=20`
                : `user_id=web_test&after_ts=${lastChatTs}&limit=20`;
            const res = await fetch(`/api/agents/${AGENT_ID}/chat?${qs}`);
            const data = await res.json();
            if (_chatBusy || chatUI.hasActiveStream()) return; // re-check: user may have sent a message while fetching
            const entries = data.entries || [];
            if (!entries.length) return;
            const wasAtBottom = chatUI.isNearBottom(80);
            // Advance cursor over ALL entries (including user echoes) so we don't re-fetch them
            for (const e of entries) { if (e.ts > lastChatTs) lastChatTs = e.ts; }
            // Skip user echoes (already shown locally); render assistant/system events
            const newEntries = entries.filter(e => e.type !== 'user');
            renderEntries(newEntries, { persistent: true });
            if (newEntries.length && wasAtBottom) chatUI.scrollToBottom();
        } catch(e) { /* ignore network errors during idle poll */ }
    }, 3000);
}

function stopIdlePoll() {
    if (chatIdleTimer) {
        clearInterval(chatIdleTimer);
        chatIdleTimer = null;
    }
    if (_pollThinkingId) {
        chatUI.removeThinkingIndicator(_pollThinkingId);
        _pollThinkingId = null;
    }
}

function handleChatBtn() {
    sendChat();
}

function stopChat() {
    _destroyCurrentTurn();
    if (chatPollTimer) {
        clearInterval(chatPollTimer);
        chatPollTimer = null;
    }
    chatPolling = false;
    setChatBusy(false); // will call startIdlePoll()
}

async function sendChat() {
    const input = document.getElementById('chat-input');
    const msg = input.value.trim();
    if (!msg) return;
    input.value = '';
    input.style.height = 'auto';
    window._clearChatDraft();
    chatUI.appendMessage('user', msg);
    // Capture the user message wrapper for anchoring the thinking bubble
    const container = document.getElementById('chat-messages');
    const userWrappers = container ? container.querySelectorAll('[data-msg-role="user"]') : [];
    const userMsgEl = userWrappers.length ? userWrappers[userWrappers.length - 1] : null;
    chatUI.scrollToBottom();
    setChatBusy(true);

    // If an SSE stream is already active, the agent is mid-turn — inject the message
    if (chatUI.hasActiveStream()) {
        try {
            const res = await fetch(`/api/agents/${AGENT_ID}/chat`, {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({message: msg, user_id: 'web_test'}),
            });
            const data = await res.json();
            if (data.injected) {
                chatUI.markLastUserBubbleQueued();
                setChatBusy(false);
                return;
            }
            if (!data.success) {
                chatUI.appendMessage('error', 'Error: ' + (data.error || 'Unknown error'));
            } else {
                const $resp = chatUI.appendMessage('assistant', data.response);
                // Reposition the slash command response immediately after the user
                // message so it doesn't appear below an active thinking bubble that
                // turn_split may have inserted in the meantime.
                if (userMsgEl && $resp && $resp.length) $(userMsgEl).after($resp);
                await syncLastChatTs();
            }
        } catch(e) {
            chatUI.appendMessage('error', 'Network error: ' + e.message);
        }
        setChatBusy(false);
        setTimeout(refreshSessionState, 0);
        setTimeout(refreshAgentState, 1000);
        return;
    }

    // Destroy any previous turn before starting a new one
    _destroyCurrentTurn();
    chatUI.clearActiveSpinner();
    // Kill any stale poll (e.g. from restoreActiveReasoning running concurrently)
    if (chatPollTimer) {
        clearInterval(chatPollTimer);
        chatPollTimer = null;
        chatPolling = false;
    }

    // Create thinking bubble eagerly, anchored right below the user message.
    // This guarantees correct DOM ordering — the bubble always sits between
    // the user message and whatever follows, never above a prior finalized bubble.
    let thinkingId = chatUI.showThinkingIndicator(null, userMsgEl);

    const abortController = new AbortController();
    const signal = abortController.signal;

    // Fetch session_id so we can open SSE stream before the LLM starts
    let sessionId = null;
    try {
        const sRes = await fetch(`/api/agents/${AGENT_ID}/chat/session?user_id=web_test`, {signal});
        const sData = await sRes.json();
        sessionId = sData.session_id || null;
    } catch(e) { /* SSE won't work but chat still functions */ }

    _currentTurn = { abortController, thinkingId, stream: null, pollTimer: null };

    if (sessionId) {
        _currentTurn.stream = chatUI.connectThinkingStream(
            `/api/agents/${AGENT_ID}/chat/stream?session_id=${encodeURIComponent(sessionId)}`,
            thinkingId,
            {
                userMsgEl: userMsgEl,
                onSplit: (newId) => { thinkingId = newId; _currentTurn.thinkingId = newId; },
                onDone: () => { thinkingId = null; if (_currentTurn) _currentTurn.thinkingId = null; },
            }
        );
    }

    try {
        const res = await fetch(`/api/agents/${AGENT_ID}/chat`, {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({message: msg, user_id: 'web_test'}),
            signal
        });
        const data = await res.json();
        if (data.buffered) {
            pollForResponse(thinkingId);
            return;
        } else if (data.injected) {
            chatUI.removeThinkingIndicator(thinkingId);
            chatUI.markLastUserBubbleQueued();
        } else if (!data.success) {
            chatUI.removeThinkingIndicator(thinkingId);
            chatUI.appendMessage('error', 'Error: ' + (data.error || 'Unknown error'));
        } else if (data.clear_ui) {
            // /clear slash command — clear the UI and show confirmation as system message
            chatUI.removeThinkingIndicator(thinkingId);
            chatUI.clearContainer();
            chatUI.appendMessage('system', data.response);
            thinkingId = null; if (_currentTurn) _currentTurn.thinkingId = null;
            chatUI.closeStream();
            await syncLastChatTs();
        } else if (data.slash_command) {
            // Slash command response — no LLM involved, remove the eager thinking bubble
            chatUI.removeThinkingIndicator(thinkingId);
            thinkingId = null; if (_currentTurn) _currentTurn.thinkingId = null;
            chatUI.closeStream();
            chatUI.appendMessage('assistant', data.response);
            await syncLastChatTs();
        } else {
            // Non-buffered success: agent responded immediately.
            // Thinking bubble was already created eagerly; populate it and close SSE.
            // Only populate if SSE hasn't already (avoids duplicate entries)
            if (chatUI.getTimelineEntryCount(thinkingId) === 0) {
                if (data.tool_trace && data.tool_trace.length > 0) {
                    for (const t of data.tool_trace) {
                        chatUI.appendTimelineEntry(thinkingId, {
                            type: 'tool_call', tool: t.tool, args: t.args || {}, param_types: {}
                        });
                        if (t.result) {
                            chatUI.appendTimelineEntry(thinkingId, {
                                type: 'tool_result', tool: t.tool, result: t.result, error: !!t.result.error
                            });
                        }
                    }
                }
                // Always add a thinking summary so the bubble isn't empty
                if (chatUI.getTimelineEntryCount(thinkingId) === 0) {
                    chatUI.appendTimelineEntry(thinkingId, {type: 'thinking', content: data.response || ''});
                }
            }
            chatUI.finalizeThinkingBubble(thinkingId, (data.metadata || {}).thinking_duration);
            thinkingId = null; if (_currentTurn) _currentTurn.thinkingId = null;
            chatUI.closeStream();
            chatUI.appendMessage(data.error ? 'error' : 'assistant', data.response);
            await syncLastChatTs();
        }
    } catch(e) {
        if (e.name === 'AbortError') return; // stopChat() already cleaned up
        chatUI.closeStream();
        chatUI.removeThinkingIndicator(thinkingId);
        chatUI.appendMessage('error', 'Network error: ' + e.message);
    }
    setChatBusy(false);
    // Refresh summary and agent state after response completes
    setTimeout(refreshSessionState, 0);
    setTimeout(refreshAgentState, 1000);
}

async function syncLastChatTs() {
    try {
        const sid = await _getChatSessionId();
        const qs = sid ? `session_id=${encodeURIComponent(sid)}&limit=1` : `user_id=web_test&limit=1`;
        const res = await fetch(`/api/agents/${AGENT_ID}/chat?${qs}`);
        const data = await res.json();
        for (const e of (data.entries || [])) {
            if (e.ts > lastChatTs) lastChatTs = e.ts;
        }
    } catch(e) { /* ignore */ }
}

function pollForResponse(existingThinkingId) {
    // Poll every 1s until a 'final' or 'error' entry arrives in JSONL.
    // existingThinkingId: thinking bubble already shown (created eagerly in sendChat/restoreActiveReasoning).
    if (!_currentTurn) return;  // no active turn to poll for
    if (_currentTurn.pollTimer) clearInterval(_currentTurn.pollTimer);
    let thinkingId = existingThinkingId || null;
    // Collect intermediate entries across poll ticks for SSE fallback rendering.
    // If SSE fails to deliver activity entries (race: agent completed before SSE connected),
    // these are replayed into the thinking bubble when 'final' arrives.
    const collectedIntermediates = [];
    const pollStartTs = lastChatTs; // snapshot cursor at poll start
    _currentTurn.pollTimer = setInterval(async () => {
        if (chatPolling) return;
        chatPolling = true;
        try {
            // Sync thinkingId from _currentTurn in case turn_split created a new bubble
            if (_currentTurn && _currentTurn.thinkingId && _currentTurn.thinkingId !== thinkingId) {
                thinkingId = _currentTurn.thinkingId;
            }
            const sid = await _getChatSessionId();
            const qs = sid
                ? `session_id=${encodeURIComponent(sid)}&after_ts=${lastChatTs}&limit=50`
                : `user_id=web_test&after_ts=${lastChatTs}&limit=50`;
            const res = await fetch(`/api/agents/${AGENT_ID}/chat?${qs}`);
            const data = await res.json();
            const entries = data.entries || [];
            if (!entries.length) return;  // bubble already exists with spinner, just wait
            const wasAtBottom = chatUI.isNearBottom(80);
            let gotFinal = false;
            for (const entry of entries) {
                if (entry.ts <= lastChatTs) continue;
                lastChatTs = entry.ts;
                if (entry.type === 'final' || entry.type === 'error') {
                    chatUI.closeStream();
                    // SSE fallback: if SSE race-lost (agent finished before stream connected),
                    // the thinking bubble will be empty. Replay intermediates from JSONL history.
                    if (collectedIntermediates.length > 0) {
                        if (chatUI.getTimelineEntryCount(thinkingId) === 0) {
                            for (const ie of collectedIntermediates) {
                                if (ie.type === 'thinking') {
                                    chatUI.appendTimelineEntry(thinkingId, {type: 'thinking', content: ie.content});
                                } else if (ie.type === 'tool_call') {
                                    chatUI.appendTimelineEntry(thinkingId, {type: 'tool_call', tool: ie.function, args: ie.params || {}, param_types: {}});
                                } else if (ie.type === 'tool_output') {
                                    let result;
                                    try { result = JSON.parse(ie.content); } catch(e) { result = {data: ie.content}; }
                                    chatUI.appendTimelineEntry(thinkingId, {type: 'tool_result', tool: ie.function, result, error: !!ie.error});
                                } else if (ie.type === 'intermediate') {
                                    chatUI.appendTimelineEntry(thinkingId, {type: 'response', content: ie.content});
                                }
                            }
                        }
                    }
                    chatUI.finalizeThinkingBubble(thinkingId, (entry.metadata || {}).thinking_duration);
                    thinkingId = null; if (_currentTurn) _currentTurn.thinkingId = null;
                    const fMeta = entry.metadata || {};
                    if (fMeta.concurrency_limited || fMeta.busy_ack) {
                        chatUI.appendMessage('system', '[SYSTEM/Concurrency]\n' + entry.content, {metadata: fMeta});
                    } else {
                        chatUI.appendMessage(entry.type === 'error' ? 'error' : 'assistant',
                                            entry.content, {metadata: fMeta});
                    }
                    gotFinal = true;
                } else if (['thinking', 'tool_call', 'tool_output', 'intermediate'].includes(entry.type)) {
                    collectedIntermediates.push(entry);
                    // Render progressively if SSE isn't connected (SSE handles live rendering otherwise)
                    if (!chatUI.hasActiveStream()) {
                        if (entry.type === 'thinking') {
                            chatUI.appendTimelineEntry(thinkingId, {type: 'thinking', content: entry.content});
                        } else if (entry.type === 'tool_call') {
                            chatUI.appendTimelineEntry(thinkingId, {type: 'tool_call', tool: entry.function, args: entry.params || {}, param_types: {}});
                        } else if (entry.type === 'tool_output') {
                            let result;
                            try { result = JSON.parse(entry.content); } catch(e) { result = {data: entry.content}; }
                            chatUI.appendTimelineEntry(thinkingId, {type: 'tool_result', tool: entry.function, result, error: !!entry.error});
                        } else if (entry.type === 'intermediate') {
                            chatUI.appendTimelineEntry(thinkingId, {type: 'response', content: entry.content});
                        }
                        if (wasAtBottom) chatUI.scrollToBottom();
                    }
                }
                // Other types are handled by SSE live stream; skip to avoid duplication
            }
            if (wasAtBottom) chatUI.scrollToBottom();
            if (gotFinal) {
                clearInterval(_currentTurn.pollTimer);
                _currentTurn.pollTimer = null;
                setChatBusy(false);
                setTimeout(refreshSessionState, 0);
                setTimeout(refreshAgentState, 1000);
            }
        } catch(e) { console.error('[pollForResponse]', e); }
        finally { chatPolling = false; }
    }, 1000);
}

function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}


function switchPromptTab(tab) {
    document.getElementById('prompt-tab-prompt').classList.toggle('hidden', tab !== 'prompt');
    document.getElementById('prompt-tab-llm').classList.toggle('hidden', tab !== 'llm');
    const btnPrompt = document.getElementById('prompt-tab-btn-prompt');
    const btnLlm = document.getElementById('prompt-tab-btn-llm');
    btnPrompt.className = 'px-4 py-2 text-sm font-medium border-b-2 ' + (tab === 'prompt' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700');
    btnLlm.className = 'px-4 py-2 text-sm font-medium border-b-2 ' + (tab === 'llm' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700');
}

async function showCompiledPrompt() {
    // Show modal immediately with loading spinner
    const modal = document.getElementById('compiled-prompt-modal');
    const spinner = document.getElementById('compiled-prompt-loading');
    modal.classList.remove('hidden');
    spinner.classList.remove('hidden');

    // Fetch both in parallel
    const [promptRes, llmRes] = await Promise.all([
        fetch(`/api/agents/${AGENT_ID}/compiled-prompt`),
        fetch(`/api/agents/${AGENT_ID}/chat/llm-preview?user_id=web_test`)
    ]);
    const data = await promptRes.json();
    const llmData = await llmRes.json();

    // System Prompt tab
    document.getElementById('compiled-prompt-text').textContent = data.system_prompt || '(empty)';
    const tools = data.tools || [];
    document.getElementById('compiled-tools-count').textContent = `(${tools.length} tools)`;
    document.getElementById('compiled-tools-json').textContent = JSON.stringify(tools, null, 2);

    // Token estimates
    const tokens = data.tokens || {};
    const spTokens = tokens.system_prompt || 0;
    const toolTokens = tokens.tool_definitions || 0;
    const memTokens = tokens.memories || 0;
    const sumTokens = tokens.summary || 0;
    const totalTokens = tokens.total || (spTokens + toolTokens + memTokens + sumTokens);
    document.getElementById('compiled-sp-tokens').textContent = `\u2014 ~${spTokens.toLocaleString()} tokens`;
    document.getElementById('compiled-tool-tokens').textContent = `\u2014 ~${toolTokens.toLocaleString()} tokens`;
    document.getElementById('compiled-mem-tokens').textContent = `\u2014 ~${memTokens.toLocaleString()} tokens`;
    document.getElementById('compiled-sum-tokens').textContent = `\u2014 ~${sumTokens.toLocaleString()} tokens`;
    const tokenBadge = document.getElementById('compiled-token-badge');
    tokenBadge.textContent = `~${totalTokens.toLocaleString()} tokens`;
    tokenBadge.classList.remove('hidden');

    // Long-term Memory section
    const memSection = document.getElementById('compiled-memories-section');
    const memText = document.getElementById('compiled-memories-text');
    if (data.memories) {
        memSection.classList.remove('hidden');
        memText.textContent = data.memories;
    } else {
        memSection.classList.add('hidden');
    }

    // Prior Conversation Summary section
    const sumSection = document.getElementById('compiled-summary-section');
    const sumText = document.getElementById('compiled-summary-text');
    if (data.summary) {
        sumSection.classList.remove('hidden');
        sumText.textContent = data.summary;
    } else {
        sumSection.classList.add('hidden');
    }

    // LLM Request tab
    const badge = document.getElementById('llm-preview-badge');
    badge.classList.toggle('hidden', !llmData.has_summary);
    const container = document.getElementById('llm-preview-messages');
    const msgs = llmData.messages || [];
    if (!msgs.length) {
        container.innerHTML = '<p class="text-xs text-gray-400 italic">No messages yet. Start chatting first.</p>';
    } else {
        container.innerHTML = msgs.map((m, i) => {
            const role = m.role || 'unknown';
            const colors = {
                system: 'border-l-purple-400 bg-purple-50',
                user: 'border-l-blue-400 bg-blue-50',
                assistant: 'border-l-green-400 bg-green-50',
                tool: 'border-l-amber-400 bg-amber-50'
            };
            const labelColors = {
                system: 'text-purple-600',
                user: 'text-blue-600',
                assistant: 'text-green-600',
                tool: 'text-amber-600'
            };
            const cls = colors[role] || 'border-l-gray-400 bg-gray-50';
            const labelCls = labelColors[role] || 'text-gray-600';
            const content = typeof m.content === 'string' ? escapeHtml(m.content) : escapeHtml(JSON.stringify(m.content, null, 2));
            const toolInfo = m.tool_calls ? `<div class="mt-1 text-[10px] text-gray-500">tool_calls: ${escapeHtml(JSON.stringify(m.tool_calls))}</div>` : '';
            const toolId = m.tool_call_id ? `<span class="text-[10px] text-gray-400 ml-2">id: ${escapeHtml(m.tool_call_id)}</span>` : '';
            return `<div class="border-l-4 ${cls} rounded-r-md p-3">
                <div class="text-[10px] uppercase tracking-wide font-semibold ${labelCls} mb-1">[${i}] ${role}${toolId}</div>
                <pre class="text-xs text-gray-700 font-mono whitespace-pre-wrap break-words">${content}</pre>
                ${toolInfo}
            </div>`;
        }).join('');
    }

    // Hide spinner and switch to System Prompt tab by default
    spinner.classList.add('hidden');
    switchPromptTab('prompt');
}

async function confirmClearHistory() {
    const confirmed = await showConfirm({
        title: 'Clear Chat History',
        message: 'Clear all chat history for this session? This cannot be undone.',
        confirmText: 'Clear',
        danger: true
    });
    if (!confirmed) return;
    clearChat();
}

async function clearChat() {
    document.getElementById('chat-messages').innerHTML = '';
    chatLoaded = false;
    lastChatTs = 0;
    await fetch(`/api/agents/${AGENT_ID}/chat/clear`, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({user_id: 'web_test'})});
    // Clear summary panels (desktop + mobile)
    const emptySummary = '<p class="text-sm text-gray-400 italic">No summary yet. Start chatting to generate one.</p>';
    document.getElementById('summary-content').innerHTML = emptySummary;
    document.getElementById('summary-meta').classList.add('hidden');
    document.getElementById('mobile-summary-content').innerHTML = emptySummary;
    document.getElementById('mobile-summary-meta').classList.add('hidden');
    // Refresh agent state display (clear_session also deletes agent_state from DB)
    refreshAgentState();
}

function toggleChatSummary() {
    document.getElementById('mobile-chat-summary-overlay').classList.remove('hidden');
}

function closeMobileChatSummary() {
    document.getElementById('mobile-chat-summary-overlay').classList.add('hidden');
}

async function refreshSessionState() {
    try {
        // Fetch both session summary and agent state in parallel
        const sid = _chatSessionId ? `&session_id=${encodeURIComponent(_chatSessionId)}` : '';
        const [sumRes, stateRes] = await Promise.all([
            fetch(`/api/agents/${AGENT_ID}/chat/summary?user_id=web_test${sid}`),
            fetch(`/api/agents/${AGENT_ID}/chat/state?user_id=web_test${sid}`)
        ]);
        const data = await sumRes.json();
        const stateData = stateRes.ok ? await stateRes.json() : {};

        // Build mode badge + plan file + tasks header from agent state
        let headerHtml = '';
        if (stateData.mode) {
            const modeColor = stateData.mode === 'execute' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700';
            headerHtml += `<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${modeColor}">${esc(stateData.mode)}</span> `;
        }
        if (stateData.plan_file) {
            headerHtml += `<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300 ml-1">📋 ${esc(stateData.plan_file)}</span> `;
        }
        if (stateData.tasks && stateData.tasks.length > 0) {
            const icons = {pending: '\u2610', in_progress: '\u27f3', done: '\u2713'};
            const iconColors = {pending: 'text-gray-400', in_progress: 'text-amber-500', done: 'text-green-500'};
            headerHtml += '<div class="mt-2"><div class="text-gray-500 dark:text-gray-400 font-medium mb-1 text-xs uppercase tracking-wide">Tasks</div><ul class="space-y-0.5">';
            for (const t of stateData.tasks) {
                const icon = icons[t.status] || '\u2610';
                const color = iconColors[t.status] || 'text-gray-400';
                const textClass = t.status === 'done' ? 'line-through text-gray-400 dark:text-gray-500' : 'text-gray-700 dark:text-gray-200';
                headerHtml += `<li class="flex items-start gap-1.5"><span class="${color} text-xs mt-0.5">${icon}</span><span class="${textClass} text-xs">${marked.parseInline(t.text)}</span></li>`;
            }
            headerHtml += '</ul></div>';
        }

        // Build skill badges from agent state
        const skills = Array.isArray(stateData.loaded_skills) ? stateData.loaded_skills : [];
        const skBadges = _buildSkillBadges(skills);
        if (skBadges.hasBadges) {
            headerHtml += '<div class="mt-2 pt-2 border-t border-gray-100 dark:border-gray-700"><div class="text-gray-500 dark:text-gray-400 font-medium mb-1 text-xs uppercase tracking-wide">📜 ' + skBadges.count + ' Skills:</div><div class="flex flex-wrap gap-1">' + skBadges.html + '</div></div>';
        }

        const content = document.getElementById('summary-content');
        const meta = document.getElementById('summary-meta');
        const mobileContent = document.getElementById('mobile-summary-content');
        const mobileMeta = document.getElementById('mobile-summary-meta');
        if (data.summary) {
            const html = (headerHtml ? `<div class="mb-3 pb-3 border-b border-gray-100 dark:border-gray-700">${headerHtml}</div>` : '') +
                '<div class="recap-prose">' + marked.parse(data.summary) + '</div>';
            const metaText = `${data.message_count} messages summarized` + (data.updated_at ? ` \u00b7 ${data.updated_at}` : '');
            content.innerHTML = html;
            meta.textContent = metaText;
            meta.classList.remove('hidden');
            mobileContent.innerHTML = html;
            mobileMeta.textContent = metaText;
            mobileMeta.classList.remove('hidden');
        } else if (headerHtml) {
            // Show state-only card when no summary yet
            content.innerHTML = `<div>${headerHtml}</div>`;
            meta.textContent = '';
            meta.classList.remove('hidden');
            mobileContent.innerHTML = `<div>${headerHtml}</div>`;
            mobileMeta.textContent = '';
            mobileMeta.classList.remove('hidden');
        } else {
            const empty = '<p class="text-sm text-gray-400 italic">No state yet. Start chatting to generate one.</p>';
            content.innerHTML = empty;
            meta.classList.add('hidden');
            mobileContent.innerHTML = empty;
            mobileMeta.classList.add('hidden');
        }
    } catch(e) { /* ignore */ }
}

async function refreshAgentState() {
    if (!ENABLE_AGENT_STATE) return;
    renderAgentState(AGENT_ID, 'web_test', ['agent-state-content', 'mobile-agent-state-content'], _chatSessionId);
}

document.addEventListener('evonic:agent-state-changed', function() {
    refreshAgentState();
    refreshSessionState();
});

// ==================== Model Selector ====================
async function loadAgentModelSelector() {
    try {
        const [modelsRes, agentModelRes] = await Promise.all([
            fetch('/api/models'),
            fetch(`/api/agents/${AGENT_ID}/model`)
        ]);
        const modelsData = await modelsRes.json();
        const agentModelData = await agentModelRes.json();
        const currentModelId = agentModelData.default_model_id || '';
        const currentFallbackModelId = agentModelData.fallback_model_id || '';

        const sel = document.getElementById('agent-model-select');
        const fbSel = document.getElementById('agent-fallback-model-select');
        (modelsData.models || []).forEach(m => {
            const opt = document.createElement('option');
            opt.value = m.id;
            opt.textContent = `${m.name} (${m.model_name})`;
            if (m.id === currentModelId) opt.selected = true;
            sel.appendChild(opt);

            const fbOpt = document.createElement('option');
            fbOpt.value = m.id;
            fbOpt.textContent = `${m.name} (${m.model_name})`;
            if (m.id === currentFallbackModelId) fbOpt.selected = true;
            fbSel.appendChild(fbOpt);
        });
        if (!currentModelId) sel.value = '';
        if (!currentFallbackModelId) fbSel.value = '';
    } catch (err) {
        console.error('[ModelSelector] Failed to load:', err);
    }
}
loadAgentModelSelector();
loadVariables();
loadAgentSafetyRules();

// ==================== Workplace Selector ====================
const CURRENT_HOME_ID = '{{ agent.workplace_id or "" }}';
const INITIAL_SANDBOX_STATE = {{ (agent.sandbox_enabled is none or agent.sandbox_enabled) | lower }};
const HOME_STATUS_DOT_CLS = {
    connected:    'bg-green-500',
    disconnected: 'bg-gray-400',
    connecting:   'bg-yellow-400',
    error:        'bg-red-500',
};

function updateWorkspaceVisibility() {
    const sel = document.getElementById('agent-workplace-id');
    const workspaceRow = document.getElementById('agent-workspace-row');
    if (!sel || !workspaceRow) return;
    const selectedOpt = sel.options[sel.selectedIndex];
    const homeType = selectedOpt ? selectedOpt.dataset.type : '';
    if (homeType === 'tunnel') {
        workspaceRow.classList.add('hidden');
    } else {
        workspaceRow.classList.remove('hidden');
    }
    updateSandboxVisibility();
}

function updateChatPanelTitle(connectionStatus) {
    const sel = document.getElementById('agent-workplace-id');
    const icon = document.getElementById('homePulseIcon');
    if (!icon) return;
    const selectedOpt = sel ? sel.options[sel.selectedIndex] : null;
    const homeType = selectedOpt ? selectedOpt.dataset.type : '';
    const isDisconnected = connectionStatus && connectionStatus !== 'connected';
    if (homeType === 'remote') {
        icon.classList.remove('hidden');
        icon.className = 'inline-block w-2.5 h-2.5 rounded-full animate-pulse flex-shrink-0 ' + (isDisconnected ? 'bg-red-500' : 'bg-green-500');
    } else if (homeType === 'tunnel') {
        icon.classList.remove('hidden');
        icon.className = 'inline-block w-2.5 h-2.5 rounded-full animate-pulse flex-shrink-0 ' + (isDisconnected ? 'bg-red-500' : 'bg-blue-500');
    } else {
        icon.classList.add('hidden');
    }
}

function updateSandboxVisibility() {
    const sel = document.getElementById('agent-workplace-id');
    const sandboxInput = document.getElementById('agent-sandbox');
    const sandboxLabel = document.getElementById('sandbox-label');
    const sandboxWarning = document.getElementById('sandbox-warning');
    if (!sel || !sandboxInput || !sandboxWarning || !sandboxLabel) return;
    const selectedOpt = sel.options[sel.selectedIndex];
    const homeType = selectedOpt ? selectedOpt.dataset.type : '';
    if (homeType === 'remote' || homeType === 'tunnel') {
        sandboxInput.disabled = true;
        sandboxInput.checked = false;
        sandboxLabel.classList.add('opacity-50', 'cursor-not-allowed');
        sandboxWarning.classList.remove('hidden');
    } else {
        sandboxInput.disabled = false;
        sandboxInput.checked = INITIAL_SANDBOX_STATE;
        sandboxLabel.classList.remove('opacity-50', 'cursor-not-allowed');
        sandboxWarning.classList.add('hidden');
    }
}

async function loadWorkplaceSelector() {
    try {
        const res = await fetch('/api/workplaces');
        const workplaces = await res.json();
        const sel = document.getElementById('agent-workplace-id');
        if (!sel) return;
        workplaces.forEach(h => {
            const opt = document.createElement('option');
            opt.value = h.id;
            opt.dataset.type = h.type;
            const typeLabel = {local: 'Local', remote: 'Remote SSH', tunnel: 'Tunnel'}[h.type] || h.type;
            opt.textContent = `${h.name} [${typeLabel}]`;
            if (h.id === CURRENT_HOME_ID) opt.selected = true;
            sel.appendChild(opt);
        });
        updateWorkspaceVisibility();
        if (CURRENT_HOME_ID) pollWorkplaceStatus();
        updateChatPanelTitle();
    } catch (err) {
        console.error('[WorkplaceSelector] Failed to load:', err);
    }
}

async function pollWorkplaceStatus() {
    const homeId = document.getElementById('agent-workplace-id') ? document.getElementById('agent-workplace-id').value : '';
    let connectionStatus = null;
    if (!homeId) {
        const dot = document.getElementById('workplace-status-indicator');
        if (dot) dot.className = 'flex-shrink-0 inline-block w-2.5 h-2.5 rounded-full bg-gray-300';
        hideWorkplaceConnectionOverlay();
        updateChatPanelTitle();
        return;
    }
    try {
        const res = await fetch(`/api/workplaces/${homeId}/status`);
        const data = await res.json();
        connectionStatus = data.status || 'disconnected';
        const dot = document.getElementById('workplace-status-indicator');
        if (dot) dot.className = 'flex-shrink-0 inline-block w-2.5 h-2.5 rounded-full ' + (HOME_STATUS_DOT_CLS[connectionStatus] || 'bg-gray-400');
        if (connectionStatus !== 'connected') {
            showWorkplaceConnectionOverlay(homeId, connectionStatus);
        } else {
            hideWorkplaceConnectionOverlay();
        }
    } catch (e) {}
    updateChatPanelTitle(connectionStatus);
}

function showWorkplaceConnectionOverlay(homeId, status) {
    let overlay = document.getElementById('workplace-connection-overlay');
    if (!overlay) return;
    overlay.classList.remove('hidden');
    const sel = document.getElementById('agent-workplace-id');
    const selectedOpt = sel ? sel.options[sel.selectedIndex] : null;
    const homeName = selectedOpt && selectedOpt.value ? selectedOpt.dataset.name || selectedOpt.textContent.split(' [')[0] : 'workplace';
    const msg = status === 'error' ? `Connection error — ${homeName}.` : `Connecting to ${homeName}...`;
    document.getElementById('workplace-overlay-msg').textContent = msg;
}

function hideWorkplaceConnectionOverlay() {
    const overlay = document.getElementById('workplace-connection-overlay');
    if (overlay) overlay.classList.add('hidden');
}

async function retryWorkplaceConnect() {
    const homeId = document.getElementById('agent-workplace-id').value;
    if (!homeId) return;
    try {
        await fetch(`/api/workplaces/${homeId}/connect`, { method: 'POST' });
        setTimeout(pollWorkplaceStatus, 1000);
    } catch (e) {}
}

document.getElementById('agent-workplace-id').addEventListener('change', function() {
    updateWorkspaceVisibility();
    pollWorkplaceStatus();
    updateChatPanelTitle();
});
loadWorkplaceSelector();
// Poll workplace status every 5s
setInterval(pollWorkplaceStatus, 5000);

// ==================== Tab persistence ====================
// Restore tab from URL hash on page load (same pattern as settings.html)
(function() {
    const valid = ['chat', 'general', 'knowledge', 'tools', 'skills', 'channels', 'portals', 'artifacts'];
    const hash = location.hash.replace('#', '');
    window._tabInitHash = (hash && valid.includes(hash)) ? hash : 'chat';
    if (window._tabInitHash !== 'chat') {
        showTab(window._tabInitHash);
    }
})();

// ==================== Gear Dropdown ====================
function toggleGearDropdown() {
    const dd = document.getElementById('gear-dropdown');
    dd.classList.toggle('hidden');
}
document.addEventListener('click', function(e) {
    const wrap = document.getElementById('gear-dropdown-wrap');
    const dd = document.getElementById('gear-dropdown');
    if (wrap && dd && !wrap.contains(e.target)) {
        dd.classList.add('hidden');
    }
});
document.addEventListener('keydown', function(e) {
    if (e.key === 'Escape') {
        const dd = document.getElementById('gear-dropdown');
        if (dd) dd.classList.add('hidden');
    }
});


// ==================== Artifacts Tab ====================
let _artifactsData = [];
let _artifactsFilterType = 'all';

async function loadArtifacts() {
    const sort = document.getElementById('artifacts-sort').value;
    try {
        const res = await fetch(`/api/agents/${AGENT_ID}/artifacts?sort=${sort}`);
        const data = await res.json();
        _artifactsData = data.files || [];
        renderArtifacts();
    } catch(e) {
        console.error('Failed to load artifacts:', e);
    }
}

function renderArtifacts() {
    const grid = document.getElementById('artifacts-grid');
    const empty = document.getElementById('artifacts-empty');
    
    // Apply search filter
    const query = (document.getElementById('artifacts-search').value || '').trim().toLowerCase();
    let filtered = _artifactsData;
    if (query) {
        filtered = filtered.filter(f => f.filename.toLowerCase().includes(query));
    }
    
    // Apply type filter
    if (_artifactsFilterType !== 'all') {
        filtered = filtered.filter(f => f.category === _artifactsFilterType);
    }
    
    if (filtered.length === 0) {
        grid.innerHTML = '';
        empty.classList.remove('hidden');
        return;
    }
    empty.classList.add('hidden');
    
    grid.innerHTML = filtered.map(f => {
        const sizeStr = f.size < 1024 ? f.size + ' B' : f.size < 1048576 ? (f.size / 1024).toFixed(1) + ' KB' : (f.size / 1048576).toFixed(1) + ' MB';
        const date = new Date(f.modified * 1000);
        const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
        const icon = getArtifactIcon(f.category);
        const ext = f.filename.split('.').pop();
        
        return `
            <div class="bg-gray-50 dark:bg-gray-700/50 border border-gray-200 dark:border-gray-600 rounded-lg p-2.5 hover:border-indigo-300 dark:hover:border-indigo-500 transition-colors cursor-pointer flex items-center gap-2.5 group" onclick="openArtifact('${f.filename}', '${f.category}')">
                <div class="flex items-center justify-center w-8 h-8 rounded-md flex-shrink-0 ${icon.bg}">
                    <svg class="w-4 h-4 ${icon.color}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        ${icon.path}
                    </svg>
                </div>
                <div class="flex-1 min-w-0">
                    <p class="text-sm font-medium text-gray-800 dark:text-gray-200 truncate" title="${f.filename}">${f.filename}</p>
                    <p class="text-xs text-gray-400">${sizeStr} &middot; ${dateStr}</p>
                </div>
                <div class="flex items-center gap-0.5 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
                    <button onclick="event.stopPropagation(); downloadArtifact('${f.filename}')" title="Download" class="p-1.5 text-indigo-500 hover:text-indigo-700 rounded hover:bg-indigo-50 dark:hover:bg-indigo-900/20 transition-colors">
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
                    </button>
                    <button onclick="event.stopPropagation(); deleteArtifact('${f.filename}')" title="Delete" class="p-1.5 text-red-400 hover:text-red-600 rounded hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors">
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
                    </button>
                </div>
            </div>
        `;
    }).join('');
}

function getArtifactIcon(category) {
    switch(category) {
        case 'document':
            return { bg: 'bg-blue-50 dark:bg-blue-900/20', color: 'text-blue-500', path: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path>' };
        case 'image':
            return { bg: 'bg-green-50 dark:bg-green-900/20', color: 'text-green-500', path: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>' };
        case 'sound':
            return { bg: 'bg-purple-50 dark:bg-purple-900/20', color: 'text-purple-500', path: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"></path>' };
        case 'video':
            return { bg: 'bg-rose-50 dark:bg-rose-900/20', color: 'text-rose-500', path: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path>' };
        case 'text':
            return { bg: 'bg-amber-50 dark:bg-amber-900/20', color: 'text-amber-500', path: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>' };
        default:
            return { bg: 'bg-gray-50 dark:bg-gray-800', color: 'text-gray-500', path: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"></path>' };
    }
}

function filterArtifacts() {
    renderArtifacts();
}

function setArtifactFilter(type) {
    _artifactsFilterType = type;
    document.querySelectorAll('.artifacts-filter-chip').forEach(chip => {
        if (chip.dataset.type === type) {
            chip.classList.add('border-indigo-500', 'text-indigo-600', 'bg-indigo-50', 'dark:bg-indigo-900/20', 'dark:text-indigo-400');
            chip.classList.remove('border-gray-300', 'text-gray-600', 'hover:bg-gray-50', 'dark:border-gray-600', 'dark:text-gray-400', 'dark:hover:bg-gray-700');
        } else {
            chip.classList.remove('border-indigo-500', 'text-indigo-600', 'bg-indigo-50', 'dark:bg-indigo-900/20', 'dark:text-indigo-400');
            chip.classList.add('border-gray-300', 'text-gray-600', 'hover:bg-gray-50', 'dark:border-gray-600', 'dark:text-gray-400', 'dark:hover:bg-gray-700');
        }
    });
    renderArtifacts();
}

function downloadArtifact(filename) {
    window.open(`/api/agents/${AGENT_ID}/artifacts/${filename}`, '_blank');
}

function openArtifact(filename, category) {
    const existing = document.getElementById('artifact-viewer-modal');
    if (existing) existing.remove();

    const fileUrl = `/api/agents/${AGENT_ID}/artifacts/${filename}`;
    const ext = filename.split('.').pop().toLowerCase();

    const html = `
    <div id="artifact-viewer-modal" class="fixed inset-0 z-[200] flex items-center justify-center bg-black/60 p-4" onclick="if(event.target===this)closeArtifactViewer()">
        <div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-4xl mx-4 max-h-[90vh] flex flex-col overflow-hidden" id="artifact-viewer-box">
            <div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-600 flex-shrink-0">
                <div class="flex items-center gap-3 min-w-0">
                    <span class="flex items-center justify-center w-8 h-8 rounded-lg bg-indigo-50 dark:bg-indigo-900/20 text-indigo-500 flex-shrink-0">
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path></svg>
                    </span>
                    <div class="min-w-0">
                        <h3 class="text-base font-semibold text-gray-800 dark:text-gray-100 truncate">${filename}</h3>
                        <p class="text-xs text-gray-400">${category.charAt(0).toUpperCase() + category.slice(1)}</p>
                    </div>
                </div>
                <div class="flex items-center gap-2 flex-shrink-0">
                    <a href="${fileUrl}" download class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded-md transition-colors" title="Download">
                        <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
                        Download
                    </a>
                    <button onclick="closeArtifactViewer()" class="flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors" title="Close">
                        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
                    </button>
                </div>
            </div>
            <div class="flex-1 overflow-y-auto p-6" id="artifact-viewer-body">
                <div class="flex items-center justify-center py-16">
                    <svg class="animate-spin w-8 h-8 text-indigo-500" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
                </div>
            </div>
        </div>
    </div>`;

    document.body.insertAdjacentHTML('beforeend', html);
    document.addEventListener('keydown', _artifactViewerEscHandler);

    renderArtifactContent(filename, category, fileUrl, ext);
}

function closeArtifactViewer() {
    const modal = document.getElementById('artifact-viewer-modal');
    if (modal) modal.remove();
    document.removeEventListener('keydown', _artifactViewerEscHandler);
}

function _artifactViewerEscHandler(e) {
    if (e.key === 'Escape') closeArtifactViewer();
}

async function renderArtifactContent(filename, category, fileUrl, ext) {
    const body = document.getElementById('artifact-viewer-body');
    if (!body) return;

    try {
        if (ext === 'md') {
            // Fetch and render markdown
            const res = await fetch(fileUrl);
            const text = await res.text();
            body.innerHTML = '<div class="recap-prose max-w-none">' + marked.parse(text) + '</div>';
        } else if (ext === 'pdf') {
            // Embed PDF viewer
            body.innerHTML = `<iframe src="${fileUrl}" class="w-full rounded-md border border-gray-200 dark:border-gray-600" style="min-height: 70vh;"></iframe>`;
        } else if (['mp4', 'webm', 'mov', 'avi', 'mkv', 'm4v'].includes(ext)) {
            // Video player
            body.innerHTML = `
                <div class="flex justify-center">
                    <video controls class="max-w-full max-h-[70vh] rounded-md shadow-lg">
                        <source src="${fileUrl}" type="video/${ext === 'mov' ? 'quicktime' : ext === 'mkv' ? 'x-matroska' : ext === 'm4v' ? 'mp4' : ext}">
                        Your browser does not support the video tag.
                    </video>
                </div>`;
        } else if (['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma'].includes(ext)) {
            // Audio player
            body.innerHTML = `
                <div class="flex flex-col items-center gap-4 py-8">
                    <div class="flex items-center justify-center w-20 h-20 rounded-full bg-purple-50 dark:bg-purple-900/20 text-purple-500">
                        <svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"></path></svg>
                    </div>
                    <div class="text-center mb-2">
                        <p class="text-sm font-medium text-gray-700 dark:text-gray-300">${filename}</p>
                    </div>
                    <audio controls class="w-full max-w-lg">
                        <source src="${fileUrl}" type="audio/${ext === 'm4a' ? 'mp4' : ext === 'wma' ? 'x-ms-wma' : ext}">
                        Your browser does not support the audio tag.
                    </audio>
                </div>`;
        } else if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'ico'].includes(ext)) {
            // Image viewer
            body.innerHTML = `
                <div class="flex justify-center">
                    <img src="${fileUrl}" alt="${filename}" class="max-w-full max-h-[70vh] rounded-md shadow-lg object-contain">
                </div>`;
        } else if (['txt', 'csv', 'json', 'yaml', 'yml', 'xml', 'log',
                     'py', 'c', 'rs', 'js', 'ts', 'jsx', 'tsx', 'cpp', 'cc', 'cxx',
                     'h', 'hpp', 'java', 'go', 'rb', 'php', 'cs', 'swift', 'kt',
                     'scala', 'r', 'm', 'sh', 'bash', 'zsh', 'ps1', 'sql',
                     'html', 'css', 'scss', 'less', 'toml', 'ini', 'cfg', 'conf',
                     'env', 'lock', 'diff', 'patch', 'Makefile', 'Dockerfile',
                     'vue', 'svelte', 'lua', 'pl', 'pm', 'gradle', 'groovy'].includes(ext)) {
            // Plain text / code viewer
            const res = await fetch(fileUrl);
            const text = await res.text();
            const escaped = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
            body.innerHTML = `<pre class="bg-gray-50 dark:bg-gray-900 rounded-md p-4 text-sm text-gray-800 dark:text-gray-200 overflow-x-auto max-h-[70vh] whitespace-pre-wrap font-mono">${escaped}</pre>`;
        } else {
            // Unknown type - show file info + download prompt
            body.innerHTML = `
                <div class="flex flex-col items-center gap-4 py-12 text-gray-400">
                    <svg class="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path></svg>
                    <p class="text-sm">Preview not available for this file type.</p>
                    <a href="${fileUrl}" download class="inline-flex items-center gap-1.5 px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded-md text-sm font-medium transition-colors">
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
                        Download File
                    </a>
                </div>`;
        }
    } catch(e) {
        body.innerHTML = `
            <div class="flex flex-col items-center gap-3 py-12 text-red-400">
                <svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M10.29 3.86l-8.6 14.86A1 1 0 002.56 20h18.88a1 1 0 00.87-1.5l-8.6-14.86a1 1 0 00-1.74 0z"></path></svg>
                <p class="text-sm">Failed to load file content.</p>
            </div>`;
        console.error('Failed to render artifact:', e);
    }
}

async function deleteArtifact(filename) {
    if (!await showConfirm({title: 'Delete Artifact', message: `Delete artifact "${filename}"? This cannot be undone.`, confirmText: 'Delete'})) return;
    try {
        const res = await fetch(`/api/agents/${AGENT_ID}/artifacts/${filename}`, { method: 'DELETE' });
        const data = await res.json();
        if (data.success) {
            showToast('Artifact deleted.', 'success');
            loadArtifacts();
        } else {
            showToast(data.error || 'Failed to delete artifact.', 'error');
        }
    } catch(e) {
        showToast('Network error.', 'error');
    }
}
// ==================== Clone Agent ====================
function showCloneModal() {
    // Remove existing clone modal if any
    const existing = document.getElementById('clone-modal');
    if (existing) existing.remove();

    const sourceName = document.querySelector('h2')?.textContent?.trim() || '{{ agent.name }}';
    const suggestedId = (sourceName.toLowerCase().replace(/[^a-z0-9_]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '') || 'agent') + '_clone';

    const html = `
    <div id="clone-modal" class="fixed inset-0 z-[200] flex items-center justify-center bg-black/50" onclick="if(event.target===this)closeCloneModal()">
        <div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-md mx-4 scale-100 opacity-100 transition-all duration-150" id="clone-box">
            <div class="flex items-center gap-3 px-6 pt-6 pb-2">
                <span class="flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 flex-shrink-0">
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
                </span>
                <h3 class="text-lg font-semibold text-gray-800 dark:text-gray-100">Clone Agent</h3>
            </div>
            <div class="px-6 pb-5">
                <p class="text-sm text-gray-500 dark:text-gray-400 mb-4">Create a copy of <strong>${sourceName}</strong>. All settings, tools, skills, knowledge files, and variables will be duplicated.</p>
                <div class="mb-3">
                    <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">New Agent ID <span class="text-red-500">*</span></label>
                    <input type="text" id="clone-id" value="${suggestedId}" class="w-full p-2.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100 font-mono" placeholder="agent_clone">
                    <p class="text-xs text-gray-400 mt-1">Lowercase letters, numbers, and underscores only.</p>
                </div>
                <div class="mb-3">
                    <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Name <span class="text-red-500">*</span></label>
                    <input type="text" id="clone-name" value="${sourceName} (Clone)" class="w-full p-2.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100">
                </div>
                <div class="mb-4">
                    <label class="block mb-1 text-gray-600 dark:text-gray-300 text-sm font-medium">Description</label>
                    <textarea id="clone-desc" rows="2" class="w-full p-2.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/10 dark:bg-gray-700 dark:text-gray-100" placeholder="Optional description for the clone"></textarea>
                </div>
            </div>
            <div class="flex gap-3 justify-end px-6 pb-5">
                <button onclick="closeCloneModal()" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">Cancel</button>
                <button id="clone-submit-btn" onclick="doClone()" class="px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded-md text-sm font-medium transition-colors">Clone</button>
            </div>
        </div>
    </div>`;
    document.body.insertAdjacentHTML('beforeend', html);

    // Focus the name field
    setTimeout(() => document.getElementById('clone-name')?.focus(), 100);

    // ESC to close
    document.addEventListener('keydown', _cloneEscHandler);
}

function _cloneEscHandler(e) {
    if (e.key === 'Escape') { closeCloneModal(); document.removeEventListener('keydown', _cloneEscHandler); }
}

function closeCloneModal() {
    const modal = document.getElementById('clone-modal');
    if (modal) modal.remove();
    document.removeEventListener('keydown', _cloneEscHandler);
}

async function doClone() {
    const btn = document.getElementById('clone-submit-btn');
    const newId = document.getElementById('clone-id').value.trim();
    const newName = document.getElementById('clone-name').value.trim();
    const newDesc = document.getElementById('clone-desc').value.trim();

    if (!newId || !newName) {
        showToast('Agent ID and Name are required.', 'error');
        return;
    }

    btn.disabled = true;
    btn.textContent = 'Cloning...';

    try {
        const res = await fetch(`/api/agents/${AGENT_ID}/clone`, {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({id: newId, name: newName, description: newDesc})
        });
        const data = await res.json();
        if (data.error) {
            showToast(data.error, 'error');
            btn.disabled = false;
            btn.textContent = 'Clone';
            return;
        }
        closeCloneModal();
        showToast('Agent cloned successfully!', 'success');
        // Redirect to new agent after short delay
        setTimeout(() => { window.location.href = `/agents/${data.agent.id}`; }, 800);
    } catch (err) {
        showToast('Network error. Please try again.', 'error');
        btn.disabled = false;
        btn.textContent = 'Clone';
    }
}

function cloneAgent() {
    document.getElementById('gear-dropdown').classList.add('hidden');
    showCloneModal();
}

// ==================== Delete Agent ====================
async function deleteAgent() {
    document.getElementById('gear-dropdown').classList.add('hidden');
    if (!await showConfirm({title: 'Delete Agent', message: 'Are you sure you want to delete this agent? This cannot be undone.', confirmText: 'Delete'})) return;
    const res = await fetch(`/api/agents/${AGENT_ID}`, {method: 'DELETE'});
    const data = await res.json();
    if (data.error) { alert(data.error); return; }
    window.location.href = '/agents';
}

// ==================== Enable / Disable Agent ====================
let _agentEnabled = {{ 1 if agent.enabled != 0 else 0 }};

async function toggleEnabled() {
    document.getElementById('gear-dropdown').classList.add('hidden');
    const newEnabled = !_agentEnabled;
    const action = newEnabled ? 'enable' : 'disable';
    const confirmMsg = newEnabled
        ? 'Enable this agent so it can process messages?'
        : 'Disable this agent? It will stop processing new messages.';
    if (!await showConfirm({title: action.charAt(0).toUpperCase() + action.slice(1) + ' Agent', message: confirmMsg, confirmText: action.charAt(0).toUpperCase() + action.slice(1)})) return;
    const res = await fetch(`/api/agents/${AGENT_ID}`, {
        method: 'PUT',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({enabled: newEnabled})
    });
    const data = await res.json();
    if (data.error) { alert(data.error); return; }
    _agentEnabled = newEnabled;
    const btn = document.getElementById('toggle-enabled-btn');
    if (btn) {
        const label = newEnabled ? 'Disable' : 'Enable';
        btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4"><path d="M5.636 5.636a9 9 0 1 0 12.728 0M12 3v9"/></svg><span>${label}</span>`;
        btn.title = `${label} Agent`;
        btn.className = `w-full text-left flex items-center gap-2 px-4 py-2 text-sm transition-colors ${newEnabled ? 'text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600' : 'text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-300'}`;
    }
}

// ==================== Portals Tab ====================
let _portalEditingId = null;

async function loadPortals() {
    const res = await fetch(`/api/agents/${AGENT_ID}/portals`);
    const data = await res.json();
    const portals = data.portals || [];
    const list = document.getElementById('portals-list');
    const empty = document.getElementById('portals-empty');
    if (portals.length === 0) {
        list.innerHTML = '';
        empty.classList.remove('hidden');
        return;
    }
    empty.classList.add('hidden');
    list.innerHTML = portals.map(p => {
        const cfg = p.backend_config || {};
        const cfgDisplay = typeof cfg === 'string' ? cfg : JSON.stringify(cfg);
        const typeBadge = p.backend_type === 'ssh' ? 'bg-blue-100 text-blue-700' :
                          p.backend_type === 'evonet' ? 'bg-purple-100 text-purple-700' :
                          'bg-green-100 text-green-700';
        const statusDot = p.status === 'connected' ? 'bg-green-500' : 'bg-gray-300';
        const statusText = p.status || 'disconnected';
        const connectBtn = p.status !== 'connected'
            ? `<button onclick="event.stopPropagation(); testPortalConnection('${p.id}')" class="text-xs px-3 py-1 rounded bg-green-50 text-green-600 hover:bg-green-100">Connect</button>`
            : `<button onclick="event.stopPropagation(); disconnectPortal('${p.id}')" class="text-xs px-3 py-1 rounded bg-red-50 text-red-600 hover:bg-red-100">Disconnect</button>`;
        return `
        <div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700/50 rounded-md border border-gray-100 dark:border-gray-600">
            <div>
                <div class="flex items-center gap-2 mb-1">
                    <span class="text-sm font-medium text-gray-800 dark:text-gray-100">${p.name || 'Unnamed'}</span>
                    <span class="inline-block w-2 h-2 rounded-full ${statusDot}"></span>
                    <span class="text-xs text-gray-400">${statusText}</span>
                </div>
                <div class="flex items-center gap-1.5 mb-1">
                    <span class="text-xs px-2 py-0.5 rounded font-medium ${typeBadge}">${p.backend_type}</span>
                    <code class="text-xs text-gray-500 dark:text-gray-400">/_portal/${p.virtual_path}/ → ${p.real_path}</code>
                </div>
                <div class="text-xs text-gray-400 dark:text-gray-500 truncate max-w-sm">${cfgDisplay.substring(0, 80)}</div>
            </div>
            <div class="flex items-center gap-2">
                ${connectBtn}
                <button onclick="event.stopPropagation(); showEditPortalModal('${p.id}')" class="text-xs text-indigo-500 hover:text-indigo-700">Edit</button>
                <button onclick="event.stopPropagation(); deletePortal('${p.id}')" class="text-xs text-red-500 hover:text-red-700">Delete</button>
            </div>
        </div>`;
    }).join('');
}

function showAddPortalModal() {
    _portalEditingId = null;
    document.getElementById('portal-modal-title').textContent = 'Add Portal';
    document.getElementById('portal-name').value = '';
    document.getElementById('portal-virtual-path').value = '';
    document.getElementById('portal-backend-type').value = 'local';
    document.getElementById('portal-real-path').value = '';
    document.getElementById('portal-workplace-select').value = '';
    // Reset checkbox and SSH fields
    const cb = document.getElementById('portal-use-workplace');
    if (cb) { cb.checked = false; cb.disabled = false; }
    document.getElementById('portal-ssh-host').value = '';
    document.getElementById('portal-ssh-port').value = '22';
    document.getElementById('portal-ssh-username').value = '';
    document.getElementById('portal-ssh-auth-type').value = 'key';
    document.getElementById('portal-ssh-key-path').value = '';
    document.getElementById('portal-ssh-passphrase').value = '';
    document.getElementById('portal-ssh-password').value = '';
    document.getElementById('portal-modal').classList.remove('hidden');
    updatePortalConfigFields();
}

async function showEditPortalModal(portalId) {
    _portalEditingId = portalId;
    // Fetch single portal
    const res = await fetch(`/api/agents/${AGENT_ID}/portals`);
    const data = await res.json();
    const portal = (data.portals || []).find(p => p.id === portalId);
    if (!portal) return alert('Portal not found.');
    document.getElementById('portal-modal-title').textContent = 'Edit Portal';
    document.getElementById('portal-name').value = portal.name || '';
    document.getElementById('portal-virtual-path').value = portal.virtual_path || '';
    document.getElementById('portal-backend-type').value = portal.backend_type || 'local';
    document.getElementById('portal-real-path').value = portal.real_path || '';
    const cfg = typeof portal.backend_config === 'string' ? JSON.parse(portal.backend_config || '{}') : (portal.backend_config || {});
    document.getElementById('portal-workplace-select').value = cfg.workplace_id || '';
    if (cfg.workplace_id) {
        const cb = document.getElementById('portal-use-workplace');
        if (cb) cb.checked = true;
        const sel = document.getElementById('portal-workplace-select');
        if (sel) sel.classList.remove('hidden');
    }
    // Pre-populate SSH standalone fields if editing
    if (portal.backend_type === 'ssh' && !cfg.workplace_id) {
        document.getElementById('portal-ssh-host').value = cfg.host || '';
        document.getElementById('portal-ssh-port').value = cfg.port || '22';
        document.getElementById('portal-ssh-username').value = cfg.username || '';
        document.getElementById('portal-ssh-auth-type').value = cfg.auth_type || 'key';
        document.getElementById('portal-ssh-key-path').value = cfg.key_path || '';
        document.getElementById('portal-ssh-passphrase').value = cfg.passphrase || '';
        if (cfg.auth_type === 'password') {
            document.getElementById('portal-ssh-password').value = cfg.password || '';
        }
    }
    document.getElementById('portal-modal').classList.remove('hidden');
    updatePortalConfigFields();
}

function closePortalModal() {
    document.getElementById('portal-modal').classList.add('hidden');
}

function updatePortalConfigFields() {
    const type = document.getElementById('portal-backend-type').value;
    const sshNewConfig = document.getElementById('portal-ssh-config');
    const workplaceRow = document.getElementById('portal-workplace-picker');
    const useWorkplaceCb = document.getElementById('portal-use-workplace');
    const workplaceSelect = document.getElementById('portal-workplace-select');

    if (type === 'local') {
        if (sshNewConfig) sshNewConfig.classList.add('hidden');
        if (workplaceRow) workplaceRow.classList.add('hidden');
        if (useWorkplaceCb) useWorkplaceCb.checked = false;
    } else if (type === 'ssh') {
        if (workplaceRow) workplaceRow.classList.remove('hidden');
        loadPortalWorkplaces();
        _applyWorkplaceToggle();
    } else if (type === 'evonet') {
        if (sshNewConfig) sshNewConfig.classList.add('hidden');
        if (workplaceRow) workplaceRow.classList.remove('hidden');
        if (useWorkplaceCb) { useWorkplaceCb.checked = true; useWorkplaceCb.disabled = true; }
        if (workplaceSelect) workplaceSelect.classList.remove('hidden');
        loadPortalWorkplaces();
    }
}

function _applyWorkplaceToggle() {
    const sshNewConfig = document.getElementById('portal-ssh-config');
    const useWorkplaceCb = document.getElementById('portal-use-workplace');
    const workplaceSelect = document.getElementById('portal-workplace-select');
    if (!useWorkplaceCb) return;
    const useExisting = useWorkplaceCb.checked;
    if (sshNewConfig) {
        if (useExisting) sshNewConfig.classList.add('hidden');
        else sshNewConfig.classList.remove('hidden');
    }
    if (workplaceSelect) {
        if (useExisting) workplaceSelect.classList.remove('hidden');
        else {
            workplaceSelect.classList.add('hidden');
            workplaceSelect.value = '';
        }
    }
}

async function savePortal() {
    const name = document.getElementById('portal-name').value.trim();
    const virtualPath = document.getElementById('portal-virtual-path').value.trim().replace(/^\/+|\/+$/g, '');
    const backendType = document.getElementById('portal-backend-type').value;
    const realPath = document.getElementById('portal-real-path').value.trim();
    const workplaceId = document.getElementById('portal-workplace-select').value.trim();

    if (!name) return alert('Name is required.');
    if (!virtualPath) return alert('Virtual path is required.');
    if (!realPath) return alert('Real path is required.');

    let backendConfig = {};
    if (backendType === 'ssh' || backendType === 'evonet') {
        if (workplaceId) {
            backendConfig = { workplace_id: workplaceId };
        } else if (backendType === 'evonet') {
            return alert('Evonet portals require a workplace_id.');
        } else {
            // Standalone SSH config
            backendConfig = {
                host: document.getElementById('portal-ssh-host')?.value?.trim() || '',
                port: parseInt(document.getElementById('portal-ssh-port')?.value || '22'),
                username: document.getElementById('portal-ssh-username')?.value?.trim() || '',
                auth_type: document.getElementById('portal-ssh-auth-type')?.value || 'key',
                key_path: document.getElementById('portal-ssh-key-path')?.value?.trim() || '',
                passphrase: document.getElementById('portal-ssh-passphrase')?.value?.trim() || '',
            };
            if (backendConfig.auth_type === 'password') {
                backendConfig.password = document.getElementById('portal-ssh-password')?.value?.trim() || '';
            }
            if (!backendConfig.host || !backendConfig.username) {
                return alert('Host and username are required for SSH.');
            }
        }
    }

    const body = {
        name,
        virtual_path: virtualPath,
        backend_type: backendType,
        backend_config: backendConfig,
        real_path: realPath,
    };

    const url = _portalEditingId
        ? `/api/portals/${_portalEditingId}`
        : `/api/agents/${AGENT_ID}/portals`;
    const method = _portalEditingId ? 'PUT' : 'POST';

    const res = await fetch(url, {
        method,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
    });
    const data = await res.json();
    if (data.error) return alert(data.error);
    closePortalModal();
    loadPortals();
}

async function deletePortal(portalId) {
    if (!await showConfirm({ title: 'Delete Portal', message: 'Delete this portal? This only removes the mapping — files on the target are not affected.', confirmText: 'Delete' })) return;
    const res = await fetch(`/api/portals/${portalId}`, { method: 'DELETE' });
    const data = await res.json();
    if (data.error) return alert(data.error);
    loadPortals();
}

async function testPortalConnection(portalId) {
    const res = await fetch(`/api/portals/${portalId}/connect`, { method: 'POST' });
    const data = await res.json();
    if (data.ok) {
        showToast('Portal connected successfully!');
    } else {
        alert('Connection failed: ' + (data.error || 'unknown error'));
    }
    loadPortals();
}

async function disconnectPortal(portalId) {
    const res = await fetch(`/api/portals/${portalId}/disconnect`, { method: 'POST' });
    const data = await res.json();
    if (data.ok) {
        showToast('Portal disconnected.');
    }
    loadPortals();
}

// Load workplaces for dropdown in portal modal
async function loadPortalWorkplaces() {
    try {
        const res = await fetch('/api/workplaces');
        const data = await res.json();
        const workplaces = data || [];
        const sel = document.getElementById('portal-workplace-select');
        if (!sel) return;
        const prevVal = sel.value;
        sel.innerHTML = '<option value="">-- Configure new connection --</option>';
        workplaces.forEach(w => {
            const label = `${w.name} (${w.type})`;
            sel.innerHTML += `<option value="${w.id}">${label}</option>`;
        });
        // Restore previously selected value if it still exists in the new options
        if (prevVal) {
            const exists = Array.from(sel.options).some(o => o.value === prevVal);
            if (exists) sel.value = prevVal;
        }
    } catch (e) {
        console.error('Failed to load workplaces for portal picker:', e);
    }
}
</script>

<!-- ==================== Chat UI v2 init ==================== -->
<script>
$(function () {
    window.chatUI = new window.ChatUI($('#chat-messages'), {
        perspective: 'A',
        showTimestamps: false,
        userBubbleClass: 'bg-indigo-500 text-white dark:bg-indigo-800',
        assistantBubbleClass: 'bg-gray-200 text-gray-800 dark:bg-[#152A3A] border dark:border-[#1B394F] dark:text-gray-100',
        agentAvatarUrl: '/api/agents/{{ agent.id }}/avatar',
        agentId: '{{ agent.id }}',
    });
    showTab(window._tabInitHash || 'chat');
});
</script>
{% endblock %}
