1 line
211 KiB
JSON
1 line
211 KiB
JSON
{"repo": "chrysochos/society-agent", "files": {"src/public/index.html": {"6fd07c39aa571aced7a28f63742727642596579f": {"t": 1785596036.602758, "n": 208068, "h": "2483281c8caa5e1f", "c": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Society Agent</title>\n<script src=\"https://cdn.socket.io/4.7.5/socket.io.min.js\"></script>\n<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css\">\n<script src=\"https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js\"></script>\n<style>\n :root {\n --bg: #0d1117; --surface: #161b22; --border: #30363d;\n --text: #e6edf3; --muted: #8b949e; --accent: #58a6ff;\n --green: #3fb950; --red: #f85149; --yellow: #d29922;\n }\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;\n background: var(--bg); color: var(--text); height: 100vh; display: flex; flex-direction: column; }\n\n /* Header */\n header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 12px 20px;\n display: flex; align-items: center; gap: 12px; }\n header h1 { font-size: 16px; font-weight: 600; }\n header .status { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted); }\n header .dot { width: 8px; height: 8px; border-radius: 50%; }\n header .dot.online { background: var(--green); }\n header .dot.offline { background: var(--red); }\n header .dot.paused { background: var(--yellow); }\n \n /* System pause banner */\n .pause-banner { \n background: rgba(210,153,34,0.15); \n border-bottom: 1px solid var(--yellow);\n padding: 8px 20px;\n display: none;\n align-items: center;\n gap: 10px;\n font-size: 13px;\n }\n .pause-banner.visible { display: flex; }\n .pause-banner .icon { font-size: 16px; }\n .pause-banner .text { color: var(--yellow); }\n .pause-banner button { \n margin-left: auto;\n background: var(--yellow);\n color: var(--bg);\n border: none;\n padding: 4px 12px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 12px;\n font-weight: 500;\n }\n .pause-banner button:hover { opacity: 0.9; }\n \n /* Pause/Resume button in header */\n .pause-btn { \n background: none;\n border: 1px solid var(--border);\n color: var(--muted);\n cursor: pointer;\n font-size: 11px;\n padding: 4px 10px;\n border-radius: 4px;\n display: flex;\n align-items: center;\n gap: 4px;\n margin-right: 8px;\n }\n .pause-btn:hover { border-color: var(--yellow); color: var(--yellow); }\n .pause-btn.paused { border-color: var(--green); color: var(--green); }\n .pause-btn.paused:hover { border-color: var(--green); }\n\n /* Layout */\n .main { display: flex; flex: 1; overflow: hidden; }\n .sidebar { width: 260px; background: var(--surface); border-right: 1px solid var(--border);\n display: flex; flex-direction: column; overflow-y: auto; }\n .chat-area { flex: 1; display: flex; flex-direction: column; }\n\n /* Sidebar */\n .sidebar h2 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;\n color: var(--muted); padding: 12px 16px 8px; }\n .stat-row { display: flex; justify-content: space-between; padding: 6px 16px; font-size: 13px; }\n .stat-row .label { color: var(--muted); }\n .stat-row .value { max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: default; }\n .stat-row .value.expandable { cursor: pointer; }\n .stat-row .value.expanded { white-space: normal; word-break: break-all; max-width: none; }\n .agent-card { padding: 10px 16px; border-bottom: 1px solid var(--border); cursor: pointer; }\n .agent-card:hover { background: rgba(88,166,255,0.08); }\n .agent-card .name { font-size: 13px; font-weight: 500; }\n .agent-card .role { font-size: 11px; color: var(--muted); margin-top: 2px; }\n .agent-card .status-badge { display: inline-block; font-size: 10px; padding: 1px 6px;\n border-radius: 10px; margin-top: 4px; }\n .agent-card .status-badge.idle { background: rgba(139,148,158,0.2); color: var(--muted); }\n .agent-card .status-badge.working { background: rgba(88,166,255,0.2); color: var(--accent); }\n .agent-card .status-badge.completed { background: rgba(63,185,80,0.2); color: var(--green); }\n .agent-card .status-badge.error { background: rgba(248,81,73,0.2); color: var(--red); }\n\n /* Sidebar resizable */\n .sidebar { min-width: 180px; max-width: 600px; position: relative; }\n .sidebar-resize { position: absolute; top: 0; right: -3px; width: 6px; height: 100%;\n cursor: col-resize; z-index: 10; }\n .sidebar-resize:hover, .sidebar-resize.active { background: var(--accent); opacity: 0.5; }\n\n /* File tree */\n .file-tree { font-size: 12px; user-select: none; overflow-y: auto; flex: 1; }\n .tree-node { }\n .tree-row { display: flex; align-items: center; gap: 4px; padding: 3px 8px; cursor: pointer;\n white-space: nowrap; border: 2px solid transparent; transition: background 0.1s; }\n .tree-row:hover { background: rgba(88,166,255,0.08); }\n .tree-row.selected { background: rgba(88,166,255,0.15); }\n .tree-row.drag-over { border-color: var(--accent); background: rgba(88,166,255,0.12); }\n .tree-row .chevron { width: 16px; text-align: center; font-size: 10px; color: var(--muted);\n transition: transform 0.15s; flex-shrink: 0; }\n .tree-row .chevron.open { transform: rotate(90deg); }\n .tree-row .chevron.empty { visibility: hidden; }\n .tree-row .icon { width: 16px; text-align: center; flex-shrink: 0; font-size: 13px; }\n .tree-row .name { flex: 1; overflow: hidden; text-overflow: ellipsis; font-size: 12px; }\n .tree-row .size { color: var(--muted); font-size: 10px; margin-left: auto; flex-shrink: 0; }\n .tree-row .actions { display: flex; gap: 2px; opacity: 0; transition: opacity 0.15s; margin-left: 4px; }\n .tree-row:hover .actions { opacity: 1; }\n .tree-row .act-btn { background: none; border: none; color: var(--muted); cursor: pointer;\n font-size: 11px; padding: 0 2px; line-height: 1; }\n .tree-row .act-btn:hover { color: var(--text); }\n .tree-children { }\n .tree-children.collapsed { display: none; }\n .tree-row.dir-row .name { color: var(--accent); font-weight: 500; }\n /* Drag ghost */\n .drag-ghost { position: fixed; pointer-events: none; z-index: 1000; background: var(--surface);\n border: 1px solid var(--accent); border-radius: 4px; padding: 3px 10px; font-size: 12px;\n color: var(--text); opacity: 0.9; }\n\n /* Context menu */\n .ctx-menu { position: fixed; z-index: 200; background: var(--surface); border: 1px solid var(--border);\n border-radius: 6px; padding: 4px 0; min-width: 160px; box-shadow: 0 8px 24px rgba(0,0,0,0.4); }\n .ctx-menu .ctx-item { padding: 6px 14px; font-size: 12px; cursor: pointer; display: flex;\n align-items: center; gap: 8px; }\n .ctx-menu .ctx-item:hover { background: rgba(88,166,255,0.12); }\n .ctx-menu .ctx-sep { height: 1px; background: var(--border); margin: 4px 0; }\n .ctx-menu .ctx-item .shortcut { margin-left: auto; color: var(--muted); font-size: 10px; }\n .ctx-menu .ctx-item.danger { color: var(--red); }\n .ctx-menu .ctx-item.danger:hover { background: rgba(248,81,73,0.12); }\n\n /* Inline rename input */\n .rename-input { background: var(--bg); border: 1px solid var(--accent); color: var(--text);\n font-size: 12px; padding: 1px 4px; border-radius: 3px; outline: none; width: 100%; font-family: inherit; }\n\n /* Upload area */\n .upload-area { border: 2px dashed var(--border); border-radius: 8px; padding: 16px; margin: 8px 12px;\n text-align: center; cursor: pointer; transition: border-color 0.2s, background 0.2s; font-size: 12px; color: var(--muted); }\n .upload-area:hover { border-color: var(--accent); }\n .upload-area.dragover { border-color: var(--green); background: rgba(63,185,80,0.08); }\n .upload-area input[type=file] { display: none; }\n .upload-row { display: flex; gap: 6px; margin: 4px 12px; }\n .upload-row .btn { flex: 1; font-size: 11px; padding: 5px 0; }\n\n /* API Key Modal */\n .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex;\n align-items: center; justify-content: center; z-index: 100; }\n .modal { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;\n padding: 24px; width: 440px; max-width: 90vw; }\n .modal h2 { font-size: 18px; margin-bottom: 8px; }\n .modal p { font-size: 13px; color: var(--muted); margin-bottom: 16px; }\n .modal input { width: 100%; padding: 8px 12px; background: var(--bg); border: 1px solid var(--border);\n border-radius: 6px; color: var(--text); font-size: 14px; outline: none; }\n .modal input:focus { border-color: var(--accent); }\n .modal .actions { display: flex; gap: 8px; margin-top: 16px; justify-content: flex-end; }\n\n /* Chat messages */\n .messages { flex: 1; overflow-y: auto; padding: 16px 20px; display: flex; flex-direction: column; gap: 12px; }\n .message { max-width: 80%; padding: 10px 14px; border-radius: 12px; font-size: 14px;\n line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; }\n .message.user { align-self: flex-end; background: #1f6feb; border-bottom-right-radius: 4px; }\n .message.assistant { align-self: flex-start; background: var(--surface); border: 1px solid var(--border);\n border-bottom-left-radius: 4px; }\n .message.system { align-self: center; color: var(--muted); font-size: 12px; font-style: italic; }\n .message.assistant .sender { font-size: 11px; color: var(--accent); margin-bottom: 4px; font-weight: 500; }\n .message code { background: rgba(110,118,129,0.2); padding: 1px 5px; border-radius: 3px; font-size: 13px; }\n .message pre { background: var(--bg); padding: 10px; border-radius: 6px; overflow-x: auto; margin: 8px 0; }\n .message pre code { background: none; padding: 0; }\n .typing { align-self: flex-start; color: var(--muted); font-size: 13px; }\n .typing::after { content: '...'; animation: dots 1.5s steps(3) infinite; }\n @keyframes dots { 0% { content: '.'; } 33% { content: '..'; } 66% { content: '...'; } }\n\n /* Agent Turn Container - like Copilot chat */\n .agent-turn {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 8px;\n margin: 8px 0;\n overflow: hidden;\n }\n .agent-turn.completed { border-color: var(--green); border-left: 3px solid var(--green); }\n .task-budget { font-size: 11px; color: #8b949e; margin-top: 6px; padding-top: 6px; border-top: 1px solid #30363d; display: flex; gap: 12px; align-items: center; }\n .task-budget .bud-tokens { color: #58a6ff; font-variant-numeric: tabular-nums; }\n .task-budget .bud-cost { color: #3fb950; font-variant-numeric: tabular-nums; }\n .task-budget .bud-calls { color: #8b949e; }\n\n /* ── Three-level information architecture ── */\n /* Outcome badge in turn header */\n .outcome-badge { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 10px; margin-left: auto; }\n .outcome-success { background: rgba(63,185,80,0.2); color: var(--green); border: 1px solid rgba(63,185,80,0.4); }\n .outcome-partial { background: rgba(210,153,34,0.2); color: var(--yellow); border: 1px solid rgba(210,153,34,0.4); }\n .outcome-failed { background: rgba(248,81,73,0.2); color: var(--red); border: 1px solid rgba(248,81,73,0.4); }\n .outcome-running { background: rgba(88,166,255,0.15); color: var(--accent); border: 1px solid rgba(88,166,255,0.3); }\n\n /* Level 3: Trace drawer (collapsed by default) */\n .trace-drawer { border-top: 1px solid var(--border); }\n .trace-drawer > summary { list-style: none; padding: 5px 12px; cursor: pointer;\n font-size: 11px; color: var(--muted); display: flex; align-items: center; gap: 6px;\n background: rgba(0,0,0,0.12); user-select: none; }\n .trace-drawer > summary:hover { color: var(--text); background: rgba(0,0,0,0.2); }\n .trace-drawer > summary::-webkit-details-marker { display: none; }\n .trace-list { padding: 6px 10px; display: flex; flex-direction: column; gap: 3px; max-height: 400px; overflow-y: auto; }\n .trace-item { background: var(--bg); border: 1px solid var(--border); border-radius: 4px;\n padding: 5px 8px; font-size: 11px; cursor: default; }\n .trace-item.trace-error { border-left: 3px solid var(--red); }\n .trace-item.trace-success { border-left: 2px solid #2d333b; }\n .trace-item.trace-running { border-left: 3px solid var(--yellow); }\n .trace-item-header { display: flex; align-items: center; gap: 5px; }\n .trace-item-icon { font-size: 11px; flex-shrink: 0; }\n .trace-item-name { font-weight: 600; color: var(--text); white-space: nowrap; }\n .trace-item-target { font-family: monospace; color: var(--accent); flex: 1;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .trace-item-duration { color: var(--muted); margin-left: auto; white-space: nowrap; flex-shrink: 0; }\n .trace-item-why { color: #6e7681; font-style: italic; font-size: 10px; padding-left: 16px; margin-top: 1px; }\n .trace-item-error-out { color: var(--red); font-family: monospace; font-size: 10px;\n max-height: 50px; overflow: hidden; padding: 3px 5px;\n background: rgba(248,81,73,0.05); border-radius: 2px; margin-top: 3px; cursor: pointer; }\n .trace-item-error-out.expanded { max-height: none; }\n\n /* Level 1: turn response text (inside turn, always visible) */\n .turn-response { padding: 6px 0; font-size: 13px; line-height: 1.6; }\n .turn-response p { margin-bottom: 6px; }\n .turn-response ul, .turn-response ol { padding-left: 20px; margin-bottom: 6px; }\n .turn-response li { margin-bottom: 2px; }\n .turn-response code { background: rgba(110,118,129,0.15); padding: 1px 5px; border-radius: 3px; font-size: 12px; }\n .turn-response pre { background: var(--bg); padding: 8px 10px; border-radius: 5px; overflow-x: auto; font-size: 11px; margin-bottom: 6px; }\n .turn-response strong { color: var(--text); }\n /* Inline error shown in turn */\n .turn-error-inline { color: var(--red); font-size: 11px; padding: 3px 0; font-family: monospace; }\n /* Live progress indicator inside turn */\n .turn-progress { color: var(--muted); font-size: 11px; padding: 3px 0; font-style: italic; }\n\n /* Artifacts panel (Level 1 — always visible when files changed) */\n .artifacts-panel { margin-top: 8px; background: rgba(63,185,80,0.05);\n border: 1px solid rgba(63,185,80,0.2); border-radius: 6px; overflow: hidden; }\n .artifacts-header { display: flex; align-items: center; gap: 6px; padding: 5px 10px;\n font-size: 11px; color: var(--green); font-weight: 600; cursor: pointer;\n background: rgba(63,185,80,0.08); user-select: none; }\n .artifacts-list { padding: 5px 10px 6px; display: flex; flex-direction: column; gap: 2px; }\n .artifact-item { font-family: monospace; font-size: 11px; color: var(--muted); }\n .artifact-op { color: var(--green); margin-right: 6px; font-weight: 600; }\n\n .turn-header {\n display: flex;\n justify-content: space-between;\n padding: 8px 12px;\n background: rgba(88,166,255,0.1);\n border-bottom: 1px solid var(--border);\n font-size: 12px;\n }\n .turn-agent { color: var(--accent); font-weight: 600; }\n .turn-time { color: var(--muted); }\n .turn-content { padding: 12px; }\n .turn-tools { padding: 0 12px 12px; }\n .turn-summary {\n padding: 12px;\n background: rgba(63,185,80,0.1);\n border-top: 1px solid var(--green);\n }\n .summary-label { font-size: 11px; color: var(--green); font-weight: 600; margin-bottom: 4px; }\n .summary-text { font-size: 13px; color: var(--text); }\n\n /* Agent Thinking — hidden by default, shown only in debug mode */\n .agent-thinking {\n display: none;\n background: rgba(210,153,34,0.1);\n border-radius: 6px;\n padding: 10px;\n margin-bottom: 8px;\n }\n .messages.debug-on .agent-thinking { display: block; }\n .thinking-label { font-size: 11px; color: var(--yellow); font-weight: 600; margin-bottom: 4px; }\n .thinking-text { font-size: 13px; color: var(--text); line-height: 1.5; }\n\n /* Debug toggle button */\n .btn-debug { background: transparent; border: 1px solid var(--border); color: var(--muted);\n padding: 3px 9px; border-radius: 4px; cursor: pointer; font-size: 11px; line-height: 1.6;\n white-space: nowrap; }\n .btn-debug.active { border-color: var(--yellow); color: var(--yellow); background: rgba(210,153,34,0.1); }\n\n /* Message cap banner */\n .msg-cap-banner { text-align: center; padding: 5px 12px; font-size: 11px; color: var(--muted);\n background: var(--surface); border-radius: 6px; margin: 4px 8px;\n border: 1px dashed var(--border); cursor: pointer; }\n .msg-cap-banner:hover { color: var(--text); border-color: var(--accent); }\n\n /* Consecutive system message grouping */\n .message.system + .message.system { margin-top: 2px; }\n\n /* Context stats bar (visible in debug mode only) */\n .ctx-stats-bar { display: none; font-size: 10px; color: var(--muted); padding: 3px 12px;\n background: var(--surface); border-top: 1px solid var(--border); text-align: right; }\n .ctx-stats-bar.visible { display: block; }\n\n /* Tool Cards */\n .tool-card {\n background: var(--bg);\n border: 1px solid var(--border);\n border-radius: 6px;\n padding: 8px 10px;\n margin: 6px 0;\n font-size: 12px;\n }\n .tool-card.tool-running { border-left: 3px solid var(--yellow); }\n .tool-card.tool-success { border-left: 3px solid var(--green); }\n .tool-card.tool-error { border-left: 3px solid var(--red); }\n .tool-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }\n .tool-icon { font-size: 14px; }\n .tool-name { font-weight: 600; color: var(--text); text-transform: capitalize; }\n .tool-duration { color: var(--muted); margin-left: auto; font-size: 11px; }\n .tool-input { color: var(--muted); font-family: monospace; font-size: 11px; padding: 4px 0; }\n .tool-result {\n color: var(--text);\n font-size: 11px;\n padding: 6px;\n background: rgba(0,0,0,0.2);\n border-radius: 4px;\n margin-top: 4px;\n max-height: 80px;\n overflow: hidden;\n font-family: monospace;\n }\n .tool-result.result-error { color: var(--red); }\n\n /* Scroll to bottom button */\n .scroll-bottom-btn {\n position: absolute; bottom: 120px; left: 50%; transform: translateX(-50%);\n padding: 8px 16px; background: var(--accent); color: white; border: none;\n border-radius: 20px; font-size: 13px; font-weight: 500; cursor: pointer;\n box-shadow: 0 2px 8px rgba(0,0,0,0.3); z-index: 100;\n display: none; /* Hidden by default, shown via JS */\n transition: opacity 0.2s;\n }\n .scroll-bottom-btn:hover { opacity: 0.9; }\n\n /* Input area */\n .input-area { padding: 12px 20px; border-top: 1px solid var(--border); background: var(--surface); }\n .input-row { display: flex; gap: 8px; }\n .input-row textarea { flex: 1; padding: 10px 14px; background: var(--bg); border: 1px solid var(--border);\n border-radius: 8px; color: var(--text); font-size: 14px; font-family: inherit;\n resize: none; outline: none; min-height: 44px; max-height: 200px; }\n .input-row textarea:focus { border-color: var(--accent); }\n\n /* Buttons */\n btn, button { cursor: pointer; }\n .btn { padding: 8px 16px; border-radius: 6px; border: none; font-size: 13px; font-weight: 500;\n cursor: pointer; transition: opacity 0.15s; }\n .btn:hover { opacity: 0.85; }\n .btn:disabled { opacity: 0.4; cursor: not-allowed; }\n .btn-primary { background: var(--accent); color: #fff; }\n .btn-secondary { background: var(--border); color: var(--text); }\n .btn-send { padding: 10px 20px; background: var(--accent); color: #fff; border: none;\n border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; }\n .btn-send:disabled { opacity: 0.4; cursor: not-allowed; }\n\n /* Attachments */\n .attach-btn { background: none; border: 1px solid var(--border); color: var(--muted); border-radius: 6px;\n padding: 8px 10px; cursor: pointer; font-size: 16px; line-height: 1; }\n .attach-btn:hover { color: var(--accent); border-color: var(--accent); }\n .attach-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 0 8px 0; }\n .attach-chip { display: flex; align-items: center; gap: 4px; background: var(--surface); border: 1px solid var(--border);\n border-radius: 6px; padding: 3px 8px; font-size: 12px; color: var(--text); }\n .attach-chip img { width: 20px; height: 20px; border-radius: 3px; object-fit: cover; }\n .attach-chip .remove { cursor: pointer; color: var(--muted); margin-left: 2px; }\n .attach-chip .remove:hover { color: var(--red); }\n .msg-attachments { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }\n .msg-attachments img { max-width: 200px; max-height: 150px; border-radius: 6px; cursor: pointer; }\n .msg-attachments .file-badge { font-size: 11px; color: var(--muted); background: rgba(110,118,129,0.15);\n padding: 2px 8px; border-radius: 4px; }\n\n /* Log panel */\n .log-panel { height: 180px; border-top: 1px solid var(--border); background: var(--bg);\n overflow-y: auto; padding: 8px 16px; font-family: 'SF Mono', 'Cascadia Code', monospace; font-size: 12px; }\n .log-entry { color: var(--muted); padding: 1px 0; }\n .log-entry.info { color: var(--accent); }\n .log-entry.warn { color: var(--yellow); }\n .log-entry.error { color: var(--red); }\n .log-entry .ts { color: #484f58; margin-right: 8px; }\n\n /* Welcome */\n .welcome { display: flex; flex-direction: column; align-items: center; justify-content: center;\n flex: 1; color: var(--muted); text-align: center; padding: 40px; }\n .welcome h2 { font-size: 20px; color: var(--text); margin-bottom: 8px; }\n .welcome p { font-size: 14px; max-width: 500px; line-height: 1.6; }\n .welcome .features { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 20px;\n text-align: left; }\n .welcome .feature { padding: 12px; background: var(--surface); border: 1px solid var(--border);\n border-radius: 8px; }\n .welcome .feature .title { font-size: 13px; color: var(--text); font-weight: 500; }\n .welcome .feature .desc { font-size: 12px; color: var(--muted); margin-top: 4px; }\n\n /* Tab bar */\n .tab-bar { display: flex; border-bottom: 1px solid var(--border); background: var(--surface); }\n .tab-bar .tab { padding: 8px 20px; font-size: 13px; cursor: pointer; color: var(--muted);\n border-bottom: 2px solid transparent; transition: color 0.15s, border-color 0.15s; }\n .tab-bar .tab:hover { color: var(--text); }\n .tab-bar .tab.active { color: var(--accent); border-bottom-color: var(--accent); }\n .tab-content { display: none; flex: 1; flex-direction: column; overflow: hidden; }\n .tab-content.active { display: flex; }\n\n /* Terminal pane */\n .terminal-container { flex: 1; background: #000; padding: 4px; overflow: hidden; }\n .terminal-container .xterm { height: 100%; }\n .terminal-toolbar { display: flex; gap: 8px; padding: 8px 12px; background: var(--surface);\n border-bottom: 1px solid var(--border); align-items: center; font-size: 12px; }\n .terminal-toolbar select { background: var(--bg); border: 1px solid var(--border); color: var(--text);\n padding: 4px 8px; border-radius: 4px; font-size: 12px; }\n\n /* File viewer overlay */\n .file-viewer { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex;\n align-items: center; justify-content: center; z-index: 90; }\n .file-viewer-panel { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;\n width: 80vw; max-width: 900px; max-height: 80vh; display: flex; flex-direction: column; overflow: hidden; }\n .file-viewer-panel.pdf-viewer { max-width: 1100px; height: 90vh; max-height: 90vh; }\n .file-viewer-header { display: flex; align-items: center; padding: 12px 16px;\n border-bottom: 1px solid var(--border); gap: 8px; }\n .file-viewer-header .title { flex: 1; font-size: 14px; font-weight: 500; }\n .file-viewer-header .meta { font-size: 12px; color: var(--muted); }\n .file-viewer-header .view-toggle { display: flex; gap: 4px; }\n .file-viewer-header .view-toggle button { background: var(--bg); border: 1px solid var(--border);\n color: var(--muted); padding: 4px 8px; border-radius: 4px; font-size: 11px; cursor: pointer; }\n .file-viewer-header .view-toggle button.active { background: var(--accent); color: white; border-color: var(--accent); }\n .file-viewer-header .close-btn { background: none; border: none; color: var(--muted); font-size: 20px;\n cursor: pointer; padding: 0 4px; line-height: 1; }\n .file-viewer-header .close-btn:hover { color: var(--text); }\n .file-viewer-body { flex: 1; overflow: auto; padding: 16px; }\n .file-viewer-body pre { margin: 0; white-space: pre-wrap; word-break: break-all;\n font-family: 'SF Mono', 'Cascadia Code', monospace; font-size: 13px; line-height: 1.5; }\n .file-viewer-body iframe { width: 100%; height: 100%; border: none; }\n .file-viewer-body.pdf-body { padding: 0; }\n /* Markdown rendered view */\n .file-viewer-body .markdown-body { font-size: 14px; line-height: 1.6; padding: 8px; }\n .file-viewer-body .markdown-body h1 { font-size: 1.8em; margin: 0.5em 0 0.3em; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; }\n .file-viewer-body .markdown-body h2 { font-size: 1.5em; margin: 0.4em 0 0.2em; border-bottom: 1px solid var(--border); padding-bottom: 0.2em; }\n .file-viewer-body .markdown-body h3 { font-size: 1.25em; margin: 0.3em 0 0.15em; }\n .file-viewer-body .markdown-body p { margin: 0.5em 0; }\n .file-viewer-body .markdown-body ul, .file-viewer-body .markdown-body ol { margin: 0.5em 0; padding-left: 1.5em; }\n .file-viewer-body .markdown-body li { margin: 0.2em 0; }\n .file-viewer-body .markdown-body code { background: var(--bg); padding: 2px 6px; border-radius: 4px; font-size: 0.9em; }\n .file-viewer-body .markdown-body pre { background: var(--bg); padding: 12px; border-radius: 8px;\n overflow-x: auto; margin: 0.5em 0; }\n .file-viewer-body .markdown-body pre code { background: none; padding: 0; }\n .file-viewer-body .markdown-body blockquote { border-left: 4px solid var(--accent); margin: 0.5em 0;\n padding-left: 1em; color: var(--muted); }\n .file-viewer-body .markdown-body table { border-collapse: collapse; width: 100%; margin: 0.5em 0; }\n .file-viewer-body .markdown-body th, .file-viewer-body .markdown-body td { border: 1px solid var(--border);\n padding: 8px 12px; text-align: left; }\n .file-viewer-body .markdown-body th { background: var(--bg); font-weight: 600; }\n .file-viewer-body .markdown-body a { color: var(--accent); text-decoration: none; }\n .file-viewer-body .markdown-body a:hover { text-decoration: underline; }\n .file-viewer-body .markdown-body img { max-width: 100%; height: auto; border-radius: 8px; }\n\n /* Agent selector (above chat input) */\n .agent-selector { display: flex; align-items: center; gap: 8px; padding: 6px 20px;\n border-bottom: 1px solid var(--border); background: var(--surface); font-size: 12px; }\n .agent-selector label { color: var(--muted); }\n .agent-selector select { background: var(--bg); border: 1px solid var(--border); color: var(--text);\n padding: 4px 8px; border-radius: 4px; font-size: 12px; min-width: 140px; }\n .agent-selector .agent-info { color: var(--muted); font-size: 11px; margin-left: 4px; }\n .agent-selector .project-badge { display:inline-block; background:rgba(88,166,255,0.15); color:var(--accent); border:1px solid rgba(88,166,255,0.4); font-size:11px; font-weight:600; padding:2px 8px; border-radius:10px; margin-left:6px; letter-spacing:0.02em; max-width:220px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; vertical-align:middle; }\n .agent-selector .agent-indicator { display: inline-block; width: 6px; height: 6px; border-radius: 50%;\n margin-right: 4px; }\n .agent-selector .agent-indicator.active { background: var(--green); }\n .agent-selector .agent-indicator.idle { background: var(--yellow); }\n .agent-selector .agent-indicator.disabled { background: var(--red); }\n\n /* Project management panel (tab) */\n .projects-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));\n gap: 12px; padding: 16px 20px; overflow-y: auto; flex: 1; }\n .project-card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px;\n padding: 16px; display: flex; flex-direction: column; gap: 10px; transition: border-color 0.2s; }\n .project-card:hover { border-color: var(--accent); }\n .project-card .project-header { display: flex; align-items: center; gap: 10px; }\n .project-card .project-icon { width: 40px; height: 40px; border-radius: 10px;\n display: flex; align-items: center; justify-content: center; font-size: 20px;\n background: rgba(88,166,255,0.15); }\n .project-card .project-meta { flex: 1; }\n .project-card .project-name { font-size: 15px; font-weight: 600; }\n .project-card .project-desc { font-size: 11px; color: var(--muted); margin-top: 2px; }\n .project-card .project-folder { font-size: 11px; color: var(--accent); font-family: monospace; }\n .project-card .project-agents { display: flex; flex-wrap: wrap; gap: 6px; }\n .project-card .project-agent-chip { font-size: 11px; padding: 2px 8px; border-radius: 10px;\n background: rgba(88,166,255,0.08); color: var(--text); border: 1px solid var(--border); display: flex; align-items: center; gap: 4px; }\n .project-card .project-agent-chip .chip-dot { width: 6px; height: 6px; border-radius: 50%; }\n .project-card .project-agent-chip .chip-dot.supervisor { background: var(--yellow); }\n .project-card .project-agent-chip .chip-dot.worker { background: var(--green); }\n .project-card .project-stats { display: flex; gap: 12px; font-size: 11px; color: var(--muted); }\n .project-card .project-actions { display: flex; gap: 6px; margin-top: 4px; }\n .project-card .project-actions .btn { font-size: 11px; padding: 4px 10px; }\n /* Society Agent start - Git badge styling */\n .project-card .git-badge { font-size: 10px; padding: 2px 6px; border-radius: 8px;\n background: rgba(63,185,80,0.15); color: var(--green); cursor: pointer; margin-left: 6px; }\n .project-card .git-badge:hover { background: rgba(63,185,80,0.25); }\n /* Society Agent end */\n .project-add-card { background: var(--surface); border: 2px dashed var(--border); border-radius: 10px;\n padding: 30px 16px; display: flex; align-items: center; justify-content: center;\n font-size: 14px; color: var(--muted); cursor: pointer; transition: border-color 0.2s, color 0.2s; }\n .project-add-card:hover { border-color: var(--accent); color: var(--accent); }\n .agent-add-card { background: transparent; border: 2px dashed var(--border); border-radius: 10px;\n display: flex; align-items: center; justify-content: center; min-height: 140px;\n cursor: pointer; color: var(--muted); font-size: 14px; transition: border-color 0.2s; }\n .agent-add-card:hover { border-color: var(--accent); color: var(--accent); }\n\n /* Agent edit modal */\n .modal.wide { width: 560px; }\n .modal .form-group { margin-bottom: 12px; }\n .modal .form-group label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 4px; }\n .modal .form-group input, .modal .form-group textarea, .modal .form-group select {\n width: 100%; padding: 8px 12px; background: var(--bg); border: 1px solid var(--border);\n border-radius: 6px; color: var(--text); font-size: 13px; outline: none; font-family: inherit; }\n .modal .form-group input:focus, .modal .form-group textarea:focus { border-color: var(--accent); }\n .modal .form-group textarea { min-height: 80px; resize: vertical; }\n .modal .form-row { display: flex; gap: 12px; }\n .modal .form-row .form-group { flex: 1; }\n .modal .form-group .checkbox-row { display: flex; align-items: center; gap: 8px; }\n .modal .form-group .checkbox-row input { width: auto; }\n</style>\n</head>\n<body>\n\n<header>\n <h1>◆ Society Agent</h1>\n <div class=\"status\">\n <span class=\"dot\" id=\"statusDot\"></span>\n <span id=\"statusText\">Connecting...</span>\n <!-- Society Agent - manual reconnect button -->\n <button id=\"reconnectBtn\" onclick=\"manualReconnect()\" style=\"margin-left:8px;background:none;border:1px solid var(--border);color:var(--muted);cursor:pointer;font-size:11px;padding:2px 8px;border-radius:4px;display:none\" title=\"Click to reconnect\">↻ Reconnect</button>\n </div>\n <div style=\"flex:1\"></div>\n <!-- Society Agent - System pause/resume for external oversight -->\n <button class=\"pause-btn\" id=\"pauseBtn\" onclick=\"toggleSystemPause()\" title=\"Pause system for maintenance\">\n <span id=\"pauseBtnIcon\">⏸</span>\n <span id=\"pauseBtnText\">Maintenance</span>\n </button>\n <!-- Society Agent - Clear & Reset button for troubleshooting -->\n <button class=\"btn btn-secondary\" onclick=\"clearAndReset()\" style=\"font-size:11px;margin-right:8px;color:var(--yellow)\" title=\"Clear all state and reload\">⚠ Reset</button>\n <button class=\"btn btn-secondary\" onclick=\"showApiKeyModal()\" style=\"font-size:12px\">⚙ Settings</button>\n</header>\n\n<!-- Society Agent - System pause banner -->\n<div class=\"pause-banner\" id=\"pauseBanner\">\n <span class=\"icon\">⚠️</span>\n <span class=\"text\"><strong>System Paused</strong> — Maintenance mode active. New requests are blocked.</span>\n <span id=\"pauseReason\" style=\"color:var(--muted);font-size:12px\"></span>\n <button onclick=\"resumeSystem()\">▶️ Resume System</button>\n</div>\n\n<div class=\"main\">\n <div class=\"sidebar\" id=\"sidebar\" style=\"width:280px\">\n <div class=\"sidebar-resize\" id=\"sidebarResize\"></div>\n <!-- System Info -->\n <h2>System</h2>\n <div class=\"stat-row\"><span class=\"label\">Status</span><span class=\"value\" id=\"sysStatus\">-</span></div>\n <div class=\"stat-row\"><span class=\"label\">Messages</span><span class=\"value\" id=\"msgCount\">0</span></div>\n <div class=\"stat-row\"><span class=\"label\">API Key</span><span class=\"value\" id=\"apiKeyStatus\">-</span></div>\n <div class=\"stat-row\"><span class=\"label\">Output</span><span class=\"value expandable\" id=\"outputDir\" title=\"\" onclick=\"this.classList.toggle('expanded')\">projects/</span></div>\n <div class=\"stat-row\"><span class=\"label\">Files</span><span class=\"value\" id=\"fileCount\">0</span></div>\n <!-- Society Agent start - usage tracking display -->\n <div class=\"stat-row\"><span class=\"label\">💰 Tokens</span>\n <span class=\"value\" id=\"tokenCount\" title=\"Click to see details\" style=\"cursor:pointer\" onclick=\"showUsageDetails()\">0</span>\n </div>\n <div class=\"stat-row\"><span class=\"label\">💵 Est. Cost</span><span class=\"value\" id=\"costDisplay\" style=\"color:var(--green)\">$0.00</span></div>\n <div class=\"stat-row\"><span class=\"label\">📊 API Calls</span><span class=\"value\" id=\"callCount\">0</span></div>\n <div class=\"stat-row\" title=\"Click to configure limits\" style=\"cursor:pointer\" onclick=\"showLimitsDialog()\">\n <span class=\"label\">⚠️ Limit</span>\n <span class=\"value\" id=\"limitDisplay\" style=\"color:var(--green)\">$100</span>\n </div>\n <!-- Society Agent end -->\n \n <!-- Society Agent start - Port allocation display -->\n <div class=\"stat-row\" title=\"Click to see port allocations\" style=\"cursor:pointer\" onclick=\"showPortsDialog()\">\n <span class=\"label\">🔌 Ports</span>\n <span class=\"value\" id=\"portsDisplay\">0</span>\n </div>\n <!-- Society Agent end -->\n\n <!-- File Explorer -->\n <h2 style=\"margin-top:12px;display:flex;align-items:center;gap:6px\">\n Explorer\n <span style=\"flex:1\"></span>\n <button class=\"act-btn\" onclick=\"promptNewFileInTree()\" title=\"New File\" style=\"font-size:14px;background:none;border:none;color:var(--muted);cursor:pointer\">📄+</button>\n <button class=\"act-btn\" onclick=\"promptNewFolderInTree()\" title=\"New Folder\" style=\"font-size:14px;background:none;border:none;color:var(--muted);cursor:pointer\">📁+</button>\n <button class=\"act-btn\" onclick=\"refreshFiles()\" title=\"Refresh\" style=\"font-size:13px;background:none;border:none;color:var(--muted);cursor:pointer\">↻</button>\n </h2>\n <div class=\"upload-area\" id=\"uploadArea\" onclick=\"document.getElementById('fileInput').click()\" style=\"margin:4px 8px;padding:8px\">\n 📤 Drop files or click\n <input type=\"file\" id=\"fileInput\" multiple onchange=\"handleFileSelect(event)\">\n </div>\n <div id=\"fileTree\" class=\"file-tree\">\n <div style=\"padding:16px;color:var(--muted);font-size:12px\">No files yet</div>\n </div>\n\n <!-- Projects -->\n <h2 style=\"margin-top:8px\">Projects</h2>\n <div id=\"projectList\">\n <div style=\"padding:8px 16px;color:var(--muted);font-size:12px\">No projects</div>\n </div>\n\n <!-- Society Agent start - Global Skills section -->\n <h2 style=\"margin-top:8px;display:flex;align-items:center;gap:6px\">\n 🎯 Global Skills\n <span style=\"flex:1\"></span>\n <button class=\"act-btn\" onclick=\"refreshSkills()\" title=\"Refresh\" style=\"font-size:13px;background:none;border:none;color:var(--muted);cursor:pointer\">↻</button>\n </h2>\n <div id=\"skillsList\" style=\"font-size:12px\">\n <div style=\"padding:8px 16px;color:var(--muted)\">Loading...</div>\n </div>\n <!-- Society Agent end -->\n\n <!-- Society Agent start - Global MCPs section -->\n <h2 style=\"margin-top:8px;display:flex;align-items:center;gap:6px\">\n 🔌 Global MCPs\n <span style=\"flex:1\"></span>\n <button class=\"act-btn\" onclick=\"refreshMcps()\" title=\"Refresh\" style=\"font-size:13px;background:none;border:none;color:var(--muted);cursor:pointer\">↻</button>\n </h2>\n <div id=\"mcpsList\" style=\"font-size:12px\">\n <div style=\"padding:8px 16px;color:var(--muted)\">Loading...</div>\n </div>\n <!-- Society Agent end -->\n </div>\n\n <div class=\"chat-area\">\n <div class=\"tab-bar\">\n <div class=\"tab active\" onclick=\"switchTab('chat')\" id=\"tabChat\">💬 Chat</div>\n <div class=\"tab\" onclick=\"switchTab('projects')\" id=\"tabProjects\">📁 Projects</div>\n <div class=\"tab\" onclick=\"switchTab('terminal')\" id=\"tabTerminal\">⌨ Terminal</div>\n </div>\n\n <div class=\"tab-content active\" id=\"paneChat\">\n <!-- Agent selector bar (organized by project) -->\n <div class=\"agent-selector\" id=\"agentSelector\">\n <label>Agent:</label>\n <select id=\"agentSelect\" onchange=\"onAgentChange()\">\n <option value=\"\">Default (Society Agent)</option>\n </select>\n <span class=\"agent-info\" id=\"agentSelectInfo\"></span>\n </div>\n <div class=\"messages\" id=\"messages\">\n <div class=\"welcome\" id=\"welcome\">\n <h2>Society Agent Server</h2>\n <p>Multi-agent collaboration system. Chat naturally, or ask it to build something — files appear in the sidebar.</p>\n <div class=\"features\">\n <div class=\"feature\">\n <div class=\"title\">💬 Chat</div>\n <div class=\"desc\">Conversational AI with persistent memory</div>\n </div>\n <div class=\"feature\">\n <div class=\"title\">📄 Create Files</div>\n <div class=\"desc\">\"Build a calculator\" → files in projects/</div>\n </div>\n <div class=\"feature\">\n <div class=\"title\">🤖 Multi-Agent</div>\n <div class=\"desc\">Complex tasks get supervisor + worker teams</div>\n </div>\n <div class=\"feature\">\n <div class=\"title\">👁 Workspace View</div>\n <div class=\"desc\">See created files, click to view contents</div>\n </div>\n </div>\n </div>\n </div>\n <!-- Scroll to bottom button (appears when scrolled up) -->\n <button id=\"scrollBottomBtn\" class=\"scroll-bottom-btn\" onclick=\"scrollToBottom(true)\" title=\"Scroll to bottom\">\n ↓ New messages\n </button>\n <!-- 16.8 Context stats bar (visible when debug mode is on) -->\n <div class=\"ctx-stats-bar\" id=\"ctxStatsBar\"></div>\n\n <div class=\"input-area\">\n <div id=\"attachChips\" class=\"attach-chips\" style=\"display:none\"></div>\n <div class=\"input-row\">\n <button class=\"attach-btn\" onclick=\"document.getElementById('chatFileInput').click()\" title=\"Attach files\">📎</button>\n <input type=\"file\" id=\"chatFileInput\" multiple style=\"display:none\" onchange=\"handleChatAttach(event)\" accept=\"image/*,.txt,.md,.json,.js,.ts,.py,.html,.css,.csv,.xml,.yaml,.yml,.log,.sh,.sql\">\n <textarea id=\"input\" placeholder=\"Type a message... (Enter to send, Shift+Enter for newline)\"\n rows=\"1\" autofocus></textarea>\n <button class=\"btn-send\" id=\"sendBtn\" onclick=\"sendMessage()\">Send</button>\n <button class=\"btn-stop\" id=\"stopBtn\" onclick=\"stopAgent()\" title=\"Stop agent\">■ Stop</button>\n <button class=\"btn-debug\" id=\"debugBtn\" onclick=\"toggleDebug()\" title=\"Show thinking blocks & debug info\">🔍</button>\n </div>\n </div>\n\n <div class=\"log-panel\" id=\"logPanel\"></div>\n </div><!-- /paneChat -->\n\n <!-- Projects management pane -->\n <div class=\"tab-content\" id=\"paneProjects\">\n <div style=\"padding:12px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px\">\n <h3 style=\"font-size:14px;font-weight:600;flex:1\">Projects</h3>\n <span style=\"font-size:12px;color:var(--muted)\" id=\"projectCountInfo\">-</span>\n </div>\n <div class=\"projects-grid\" id=\"projectsGrid\">\n <div style=\"padding:20px;color:var(--muted);font-size:13px\">Loading projects...</div>\n </div>\n </div><!-- /paneProjects -->\n\n <div class=\"tab-content\" id=\"paneTerminal\">\n <div class=\"terminal-toolbar\">\n <span style=\"color:var(--muted)\">Shell:</span>\n <select id=\"termShell\">\n <option value=\"/bin/bash\">bash</option>\n <option value=\"/bin/sh\">sh</option>\n </select>\n <span style=\"color:var(--muted);margin-left:8px\">CWD:</span>\n <span id=\"termCwd\" style=\"color:var(--text)\">/workspace</span>\n <div style=\"flex:1\"></div>\n <button class=\"btn btn-secondary\" onclick=\"reconnectTerminal()\" style=\"font-size:11px;padding:3px 10px\">↻ Reconnect</button>\n </div>\n <div class=\"terminal-container\" id=\"terminalContainer\"></div>\n </div><!-- /paneTerminal -->\n </div>\n</div>\n\n<!-- Settings Modal (replaces old apiKeyModal) -->\n<div class=\"modal-overlay\" id=\"apiKeyModal\" style=\"display:none\">\n <div class=\"modal wide\">\n <h2>⚙️ Server Settings</h2>\n <p style=\"font-size:12px;color:var(--muted);margin-bottom:16px\">Configure LLM provider and model. These are the defaults - individual agents can override.</p>\n \n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"settingsProvider\" onchange=\"onProviderChange()\">\n <option value=\"anthropic\">Anthropic (Claude direct)</option>\n <option value=\"openrouter\">OpenRouter (multi-model)</option>\n <option value=\"openai\">OpenAI</option>\n <option value=\"minimax\">MiniMax</option>\n <option value=\"ollama\">Ollama (Local)</option>\n <option value=\"gemini\">Google Gemini</option>\n <option value=\"deepseek\">DeepSeek</option>\n <option value=\"groq\">Groq</option>\n <option value=\"mistral\">Mistral</option>\n </select>\n </div>\n \n <div class=\"form-group\">\n <label>Model</label>\n <input type=\"text\" id=\"settingsModelSearch\" placeholder=\"Filter models (e.g. qwen, free, nemotron)\" style=\"margin-bottom:6px\">\n <select id=\"settingsModel\"></select>\n <span style=\"font-size:11px;color:var(--muted)\" id=\"settingsModelCost\"></span>\n </div>\n \n <div class=\"form-group\">\n <label id=\"settingsApiKeyLabel\">API Key</label>\n <input type=\"password\" id=\"settingsApiKey\" placeholder=\"sk-...\">\n <span style=\"font-size:11px;color:var(--muted)\" id=\"settingsApiKeyStatus\"></span>\n </div>\n \n <details style=\"margin-top:16px\">\n <summary style=\"cursor:pointer;font-size:12px;color:var(--muted)\">Advanced Settings</summary>\n <div style=\"margin-top:12px\">\n <div class=\"form-group\">\n <label>Max Tokens</label>\n <input type=\"number\" id=\"settingsMaxTokens\" value=\"16384\" min=\"1000\" max=\"200000\">\n </div>\n <div class=\"form-group\">\n <label>Temperature</label>\n <input type=\"number\" id=\"settingsTemperature\" value=\"0.7\" min=\"0\" max=\"2\" step=\"0.1\">\n </div>\n <div class=\"form-group\">\n <label>Operating Profile</label>\n <select id=\"settingsOperatingProfile\">\n <option value=\"guided\">Guided (small steps, pause often)</option>\n <option value=\"balanced\">Balanced (default)</option>\n <option value=\"autonomous\">Autonomous (complete broader slices)</option>\n <option value=\"turbo\">Turbo (maximum throughput)</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Delegation Style</label>\n <select id=\"settingsDelegationStyle\">\n <option value=\"single_agent\">Single Agent (no delegation unless asked)</option>\n <option value=\"team_mode\">Team Mode (delegate when useful)</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Verification Strictness</label>\n <select id=\"settingsVerificationStrictness\">\n <option value=\"light\">Light (fast checks)</option>\n <option value=\"standard\">Standard</option>\n <option value=\"strict\">Strict (typecheck/tests/lint)</option>\n </select>\n </div>\n </div>\n </details>\n \n <div class=\"actions\">\n <button class=\"btn btn-secondary\" onclick=\"hideApiKeyModal()\">Cancel</button>\n <button class=\"btn btn-primary\" onclick=\"saveSettings()\">Save Settings</button>\n </div>\n </div>\n</div>\n\n<!-- Agent Edit/Create Modal -->\n<div class=\"modal-overlay\" id=\"projectModal\" style=\"display:none\">\n <div class=\"modal wide\">\n <h2 id=\"projectModalTitle\">Create Project</h2>\n <p style=\"font-size:12px;color:var(--muted);margin-bottom:12px\" id=\"projectModalDesc\">Create a project to organize agents around a shared folder and knowledge base.</p>\n <!-- Society Agent start - simplified: name becomes folder/id -->\n <div class=\"form-group\">\n <label>Project Name (also used as folder name)</label>\n <input type=\"text\" id=\"projectFormName\" placeholder=\"my-project\">\n <span style=\"font-size:11px;color:var(--muted)\">Will create folder: <code id=\"projectFolderPreview\">projects/</code></span>\n </div>\n <!-- Society Agent end -->\n <div class=\"form-group\">\n <label>Description</label>\n <input type=\"text\" id=\"projectFormDesc\" placeholder=\"What is this project about?\">\n </div>\n <div class=\"actions\">\n <button class=\"btn btn-secondary\" onclick=\"hideProjectModal()\">Cancel</button>\n <button class=\"btn btn-primary\" id=\"projectFormSubmit\" onclick=\"submitProjectForm()\">Create</button>\n </div>\n </div>\n</div>\n\n<!-- Society Agent start - Git Integration Modals -->\n<!-- Load from Git Modal -->\n<div class=\"modal-overlay\" id=\"gitModal\" style=\"display:none\">\n <div class=\"modal wide\">\n <h2>🔌 Load Project from Git</h2>\n <p style=\"font-size:12px;color:var(--muted);margin-bottom:12px\">Clone a repository from GitLab or GitHub. Agents will work in the cloned workspace.</p>\n \n <div class=\"form-group\">\n <label>Project Name</label>\n <input type=\"text\" id=\"gitFormName\" placeholder=\"my-project\" oninput=\"updateGitFolderPreview()\">\n <span style=\"font-size:11px;color:var(--muted)\">Will clone to: <code id=\"gitFolderPreview\">projects/</code></span>\n </div>\n \n <div class=\"form-group\">\n <label>Repository URL</label>\n <input type=\"text\" id=\"gitFormUrl\" placeholder=\"git@gitlab.com:user/repo.git or https://...\">\n </div>\n \n <div class=\"form-group\">\n <label>Branch / Ref</label>\n <input type=\"text\" id=\"gitFormRef\" placeholder=\"main\" value=\"main\">\n </div>\n \n <div class=\"form-group\">\n <label>Credential (for private repos)</label>\n <select id=\"gitFormCredential\" style=\"width:100%;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--bg);color:var(--text)\">\n <option value=\"\">None (public repo)</option>\n </select>\n <button onclick=\"showCredentialsModal()\" style=\"margin-top:8px;font-size:11px;background:none;border:1px solid var(--border);color:var(--muted);padding:4px 8px;border-radius:4px;cursor:pointer\">\n 🔑 Manage Credentials\n </button>\n </div>\n \n <div class=\"form-group\">\n <label>Description (optional)</label>\n <input type=\"text\" id=\"gitFormDesc\" placeholder=\"What is this project about?\">\n </div>\n \n <div id=\"gitFormStatus\" style=\"display:none;margin-bottom:12px;padding:8px;border-radius:4px;font-size:12px\"></div>\n \n <div class=\"actions\">\n <button class=\"btn btn-secondary\" onclick=\"hideGitModal()\">Cancel</button>\n <button class=\"btn btn-primary\" id=\"gitFormSubmit\" onclick=\"submitGitForm()\">🔌 Clone & Create</button>\n </div>\n </div>\n</div>\n\n<!-- Credentials Management Modal -->\n<div class=\"modal-overlay\" id=\"credentialsModal\" style=\"display:none\">\n <div class=\"modal wide\">\n <h2>🔑 Git Credentials</h2>\n <p style=\"font-size:12px;color:var(--muted);margin-bottom:12px\">Manage SSH keys and access tokens for private repositories.</p>\n \n <div id=\"credentialsList\" style=\"margin-bottom:16px;max-height:200px;overflow-y:auto\">\n <div style=\"color:var(--muted);font-size:12px;padding:8px\">Loading...</div>\n </div>\n \n <hr style=\"border:none;border-top:1px solid var(--border);margin:16px 0\">\n <h3 style=\"font-size:14px;margin-bottom:12px\">Add New Credential</h3>\n \n <div class=\"form-group\">\n <label>Name</label>\n <input type=\"text\" id=\"credFormName\" placeholder=\"GitLab Deploy Key - myproject\">\n </div>\n \n <div class=\"form-group\">\n <label>Type</label>\n <select id=\"credFormType\" style=\"width:100%;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--bg);color:var(--text)\" onchange=\"updateCredFormPlaceholder()\">\n <option value=\"ssh-key\">SSH Key (private key)</option>\n <option value=\"token\">Personal Access Token</option>\n </select>\n </div>\n \n <div class=\"form-group\">\n <label>Host (e.g., gitlab.com, github.com, or * for any)</label>\n <input type=\"text\" id=\"credFormHost\" placeholder=\"gitlab.com\">\n </div>\n \n <div class=\"form-group\">\n <label id=\"credFormSecretLabel\">Private Key</label>\n <textarea id=\"credFormSecret\" rows=\"4\" placeholder=\"-----BEGIN OPENSSH PRIVATE KEY----- ...\" style=\"width:100%;font-family:monospace;font-size:11px;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--bg);color:var(--text);resize:vertical\"></textarea>\n </div>\n \n <div class=\"actions\">\n <button class=\"btn btn-secondary\" onclick=\"hideCredentialsModal()\">Close</button>\n <button class=\"btn btn-primary\" onclick=\"addCredential()\">✓ Add Credential</button>\n </div>\n </div>\n</div>\n\n<!-- Git Project Actions Modal (pull, push, branches) -->\n<div class=\"modal-overlay\" id=\"gitActionsModal\" style=\"display:none\">\n <div class=\"modal wide\">\n <h2 id=\"gitActionsTitle\">🔌 Git Operations</h2>\n <div id=\"gitActionsContent\">Loading...</div>\n <div class=\"actions\" style=\"margin-top:16px\">\n <button class=\"btn btn-secondary\" onclick=\"hideGitActionsModal()\">Close</button>\n </div>\n </div>\n</div>\n\n<!-- Push to GitLab Modal -->\n<div class=\"modal-overlay\" id=\"pushToGitLabModal\" style=\"display:none\">\n <div class=\"modal\">\n <h2>🔌 Push to GitLab</h2>\n <p style=\"color:var(--text-muted);margin-bottom:16px\">Create a new GitLab repository and push this local project</p>\n \n <input type=\"hidden\" id=\"pushGitLabProjectId\" />\n \n <div class=\"form-group\">\n <label>Credential (GitLab Token)</label>\n <select id=\"pushGitLabCredential\" style=\"width:100%;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--bg);color:var(--text)\">\n <option value=\"\">Loading...</option>\n </select>\n <small style=\"color:var(--text-muted);display:block;margin-top:4px\">Must be a Personal Access Token with api scope</small>\n </div>\n \n <div class=\"form-group\">\n <label>Repository Name</label>\n <input type=\"text\" id=\"pushGitLabRepoName\" placeholder=\"my-project\" style=\"width:100%;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--bg);color:var(--text)\" />\n </div>\n \n <div class=\"form-group\">\n <label>Namespace (optional)</label>\n <select id=\"pushGitLabNamespace\" style=\"width:100%;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--bg);color:var(--text)\">\n <option value=\"\">Select credential first...</option>\n </select>\n <small style=\"color:var(--text-muted);display:block;margin-top:4px\">Leave empty for personal namespace</small>\n </div>\n \n <div class=\"form-group\">\n <label>Visibility</label>\n <select id=\"pushGitLabVisibility\" style=\"width:100%;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--bg);color:var(--text)\">\n <option value=\"private\" selected>Private</option>\n <option value=\"internal\">Internal</option>\n <option value=\"public\">Public</option>\n </select>\n </div>\n \n <div id=\"pushGitLabError\" style=\"display:none;padding:8px;background:var(--red);color:white;border-radius:4px;margin-bottom:12px\"></div>\n <div id=\"pushGitLabSuccess\" style=\"display:none;padding:8px;background:var(--green);color:white;border-radius:4px;margin-bottom:12px\"></div>\n \n <div class=\"actions\">\n <button class=\"btn btn-secondary\" onclick=\"hidePushToGitLabModal()\">Cancel</button>\n <button class=\"btn btn-primary\" id=\"pushGitLabBtn\" onclick=\"pushToGitLab()\">🔌 Create & Push</button>\n </div>\n </div>\n</div>\n<!-- Society Agent end - Git Integration Modals -->\n\n<script>\nconst API = window.location.origin;\n// Society Agent start - Enhanced Socket.IO with auto-reconnect and status\n\n// Global error handler - prevent page from breaking\nwindow.onerror = function(msg, url, line, col, error) {\n console.error('Global error:', msg, error);\n try { addLog('error', `JS Error: ${msg}`); } catch {}\n return false; // Don't suppress error in console\n};\n\n// Handle unhandled promise rejections\nwindow.addEventListener('unhandledrejection', function(event) {\n console.error('Unhandled rejection:', event.reason);\n try { addLog('error', `Promise Error: ${event.reason}`); } catch {}\n});\n\nconst socket = io(API, {\n reconnection: true,\n reconnectionAttempts: Infinity,\n reconnectionDelay: 1000,\n reconnectionDelayMax: 5000,\n timeout: 20000,\n forceNew: false, // Reuse connection\n});\nlet messageCount = 0;\nlet isStreaming = false;\nlet currentStreamEl = null;\nlet streamText = '';\nlet chatAttachments = []; // { name, type, dataUrl, base64, mediaType }\nlet reconnectAttempt = 0;\n\n// ============================================================================\n// Socket.IO Events\n// ============================================================================\n\nsocket.on('connect', () => {\n try {\n reconnectAttempt = 0;\n setStatus(true);\n addLog('info', 'Connected to Society Agent server');\n checkApiKey();\n checkServerStatus();\n checkSystemPauseState(); // Society Agent - check if system is paused\n // Reload data after reconnect\n loadProjects();\n } catch (e) {\n console.error('Error in connect handler:', e);\n }\n});\n\nsocket.on('disconnect', (reason) => {\n setStatus(false);\n addLog('warn', `Disconnected: ${reason}`);\n if (reason === 'io server disconnect') {\n // Server disconnected us, try to reconnect\n socket.connect();\n }\n});\n\nsocket.on('reconnect_attempt', (attempt) => {\n reconnectAttempt = attempt;\n setStatus(false, `Reconnecting... (${attempt})`);\n addLog('info', `Reconnection attempt ${attempt}`);\n});\n\nsocket.on('reconnect', (attempt) => {\n addLog('info', `Reconnected after ${attempt} attempts`);\n});\n\nsocket.on('reconnect_error', (error) => {\n addLog('error', `Reconnection error: ${error.message}`);\n});\n\nsocket.on('reconnect_failed', () => {\n addLog('error', 'Reconnection failed - please refresh page');\n setStatus(false, 'Disconnected - refresh page');\n});\n// Society Agent end\n\n// Society Agent start - Usage tracking events\nsocket.on('usage', (data) => {\n // Individual usage entry - update totals\n console.log('[Usage]', data);\n fetchUsageSummary();\n});\n\nsocket.on('usage-summary', (summary) => {\n updateUsageDisplay(summary);\n});\n\nsocket.on('activity', (data) => {\n console.log('[Activity]', data.type, data.summary);\n // Could show a toast or update activity feed\n});\n\nfunction updateUsageDisplay(summary) {\n const tokenEl = document.getElementById('tokenCount');\n const costEl = document.getElementById('costDisplay');\n const callEl = document.getElementById('callCount');\n \n if (tokenEl) tokenEl.textContent = formatNumber(summary.totalTokens);\n if (costEl) {\n const cost = summary.totalCostUsd;\n costEl.textContent = cost < 0.01 ? `$${(cost * 100).toFixed(2)}¢` : `$${cost.toFixed(4)}`;\n // Color code by cost\n if (cost > 1) costEl.style.color = 'var(--red)';\n else if (cost > 0.1) costEl.style.color = 'var(--yellow)';\n else costEl.style.color = 'var(--green)';\n }\n if (callEl) callEl.textContent = summary.callCount;\n}\n\nfunction formatNumber(n) {\n if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';\n if (n >= 1000) return (n / 1000).toFixed(1) + 'K';\n return n.toString();\n}\n\nasync function fetchUsageSummary() {\n try {\n const res = await fetch('/api/usage');\n if (res.ok) {\n const summary = await res.json();\n updateUsageDisplay(summary);\n }\n } catch (e) {\n console.warn('Failed to fetch usage:', e);\n }\n}\n\nfunction showUsageDetails() {\n fetch('/api/usage').then(r => r.json()).then(summary => {\n let html = `<h3>Token Usage Summary</h3>\n <p><strong>Total:</strong> ${formatNumber(summary.totalTokens)} tokens</p>\n <p><strong>Input:</strong> ${formatNumber(summary.totalInputTokens)} | <strong>Output:</strong> ${formatNumber(summary.totalOutputTokens)}</p>\n <p><strong>Cost:</strong> $${summary.totalCostUsd.toFixed(4)}</p>\n <p><strong>API Calls:</strong> ${summary.callCount}</p>\n <h4 style=\"margin-top:12px\">By Agent:</h4>\n <ul style=\"font-size:12px;margin:0;padding-left:20px\">`;\n for (const [agentId, data] of Object.entries(summary.byAgent || {})) {\n html += `<li>${agentId}: ${formatNumber(data.inputTokens + data.outputTokens)} tokens ($${data.costUsd.toFixed(4)}, ${data.callCount} calls)</li>`;\n }\n html += '</ul>';\n \n // Simple alert-style modal\n const overlay = document.createElement('div');\n overlay.className = 'modal-overlay';\n overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };\n overlay.innerHTML = `<div class=\"modal\" style=\"max-width:500px\">\n ${html}\n <div class=\"actions\"><button class=\"btn btn-primary\" onclick=\"this.closest('.modal-overlay').remove()\">Close</button></div>\n </div>`;\n document.body.appendChild(overlay);\n }).catch(e => alert('Failed to load usage: ' + e.message));\n}\n\n// Fetch initial usage on connect\nsetTimeout(fetchUsageSummary, 1000);\n\n// Society Agent start - Token limits tracking\nlet currentLimits = { maxCostGlobal: 100 };\n\nsocket.on('token-limits', (limits) => {\n currentLimits = limits;\n updateLimitDisplay();\n});\n\nsocket.on('token-usage', (data) => {\n currentLimits = data.limits;\n updateLimitDisplay(data.globalUsage?.cost);\n});\n\nsocket.on('token-limit-exceeded', (data) => {\n addLog('warn', `⚠️ Token limit exceeded: ${data.message}`);\n // Show alert\n const limitEl = document.getElementById('limitDisplay');\n if (limitEl) {\n limitEl.style.color = 'var(--red)';\n limitEl.textContent = '⛔ LIMIT';\n }\n});\n\nsocket.on('token-usage-reset', (data) => {\n addLog('info', `Token usage reset (${data.scope})`);\n fetchLimits();\n});\n\nfunction updateLimitDisplay(currentCost) {\n const limitEl = document.getElementById('limitDisplay');\n if (!limitEl) return;\n \n const limit = currentLimits.maxCostGlobal || 100;\n const cost = currentCost || 0;\n const percent = (cost / limit) * 100;\n \n if (percent >= 100) {\n limitEl.style.color = 'var(--red)';\n limitEl.textContent = '⛔ LIMIT';\n } else if (percent >= 75) {\n limitEl.style.color = 'var(--yellow)';\n limitEl.textContent = `$${limit} (${percent.toFixed(0)}%)`;\n } else {\n limitEl.style.color = 'var(--muted)';\n limitEl.textContent = `$${limit}`;\n }\n}\n\nasync function fetchLimits() {\n try {\n const res = await fetch('/api/limits');\n if (res.ok) {\n currentLimits = await res.json();\n updateLimitDisplay();\n }\n } catch (e) {\n console.warn('Failed to fetch limits:', e);\n }\n}\n\nasync function showLimitsDialog() {\n try {\n const [limitsRes, usageRes] = await Promise.all([\n fetch('/api/limits'),\n fetch('/api/limits/usage')\n ]);\n const limits = await limitsRes.json();\n const usage = await usageRes.json();\n \n const overlay = document.createElement('div');\n overlay.className = 'modal-overlay';\n overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };\n overlay.innerHTML = `<div class=\"modal\" style=\"max-width:500px\">\n <h3>Token Limits & Usage</h3>\n \n <h4 style=\"margin:12px 0 8px\">Current Usage</h4>\n <div style=\"font-size:13px;margin-bottom:12px\">\n <div>Global: ${formatNumber(usage.global?.tokens || 0)} tokens / $${(usage.global?.cost || 0).toFixed(4)}</div>\n </div>\n \n <h4 style=\"margin:12px 0 8px\">Limits (per session)</h4>\n <div style=\"display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:13px\">\n <label>Max $ per Agent:</label>\n <input type=\"number\" id=\"limitAgentCost\" value=\"${limits.maxCostPerAgent}\" step=\"1\" min=\"0\" style=\"width:80px\">\n \n <label>Max $ per Project:</label>\n <input type=\"number\" id=\"limitProjectCost\" value=\"${limits.maxCostPerProject}\" step=\"5\" min=\"0\" style=\"width:80px\">\n \n <label>Max $ Global:</label>\n <input type=\"number\" id=\"limitGlobalCost\" value=\"${limits.maxCostGlobal}\" step=\"10\" min=\"0\" style=\"width:80px\">\n \n <label>Max Tokens/Agent:</label>\n <input type=\"number\" id=\"limitAgentTokens\" value=\"${limits.maxTokensPerAgent}\" step=\"100000\" min=\"0\" style=\"width:100px\">\n </div>\n \n <div class=\"actions\" style=\"margin-top:16px;display:flex;gap:8px\">\n <button class=\"btn\" onclick=\"resetUsage()\">Reset Usage</button>\n <span style=\"flex:1\"></span>\n <button class=\"btn\" onclick=\"this.closest('.modal-overlay').remove()\">Cancel</button>\n <button class=\"btn btn-primary\" onclick=\"saveLimits()\">Save</button>\n </div>\n </div>`;\n document.body.appendChild(overlay);\n } catch (e) {\n alert('Failed to load limits: ' + e.message);\n }\n}\n\nasync function saveLimits() {\n const newLimits = {\n maxCostPerAgent: parseFloat(document.getElementById('limitAgentCost').value) || 5,\n maxCostPerProject: parseFloat(document.getElementById('limitProjectCost').value) || 25,\n maxCostGlobal: parseFloat(document.getElementById('limitGlobalCost').value) || 100,\n maxTokensPerAgent: parseInt(document.getElementById('limitAgentTokens').value) || 1000000,\n };\n \n try {\n const res = await fetch('/api/limits', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(newLimits)\n });\n if (res.ok) {\n currentLimits = (await res.json()).limits;\n updateLimitDisplay();\n document.querySelector('.modal-overlay')?.remove();\n addLog('info', 'Token limits updated');\n } else {\n throw new Error('Failed to save');\n }\n } catch (e) {\n alert('Failed to save limits: ' + e.message);\n }\n}\n\nasync function resetUsage() {\n if (!confirm('Reset all token usage counters? This will allow agents to run again if they hit limits.')) return;\n \n try {\n const res = await fetch('/api/limits/reset', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ scope: 'all' })\n });\n if (res.ok) {\n document.querySelector('.modal-overlay')?.remove();\n addLog('info', 'Token usage reset');\n fetchLimits();\n }\n } catch (e) {\n alert('Failed to reset: ' + e.message);\n }\n}\n\nsetTimeout(fetchLimits, 1500);\n// Society Agent end\n\n// Society Agent start - Port allocation dialog\nlet cachedPorts = { total: 0, allocations: [], protectedSystemPorts: [] };\n\nasync function fetchPorts() {\n try {\n const res = await fetch('/api/ports');\n cachedPorts = await res.json();\n updatePortsDisplay();\n } catch (e) {\n console.error('Failed to fetch ports:', e);\n }\n}\n\nfunction updatePortsDisplay() {\n const el = document.getElementById('portsDisplay');\n if (el) {\n const count = cachedPorts.total;\n el.textContent = count > 0 ? count : '0';\n el.style.color = count > 0 ? 'var(--blue)' : 'var(--muted)';\n }\n}\n\nasync function showPortsDialog() {\n await fetchPorts();\n \n // Also fetch projects for the dropdown\n let projects = [];\n try {\n const res = await fetch('/api/projects');\n projects = await res.json();\n } catch (e) {\n console.error('Failed to fetch projects:', e);\n }\n \n const systemPorts = cachedPorts.protectedSystemPorts || [];\n const allocations = cachedPorts.allocations || [];\n \n let allocationsHtml = '';\n if (allocations.length === 0) {\n allocationsHtml = '<div style=\"color:var(--muted);padding:8px\">No ports allocated yet</div>';\n } else {\n allocationsHtml = allocations.map(a => `\n <div style=\"display:flex;gap:8px;padding:6px 0;border-bottom:1px solid var(--border);align-items:center\">\n <span style=\"font-weight:600;font-family:monospace;min-width:50px\">${a.port}</span>\n <span style=\"flex:1\">${escapeHtml(a.serviceName)} ${a.description ? '<span style=\"color:var(--muted)\">— ' + escapeHtml(a.description) + '</span>' : ''}</span>\n <span style=\"color:var(--muted);font-size:11px\">${escapeHtml(a.projectId)}</span>\n <button class=\"btn btn-sm\" onclick=\"releasePort(${a.port}, '${escapeHtml(a.projectId)}')\" title=\"Release this port\" style=\"padding:2px 6px;font-size:10px\">✕</button>\n </div>\n `).join('');\n }\n \n const projectOptions = projects.length > 0 \n ? projects.map(p => `<option value=\"${escapeHtml(p.id)}\">${escapeHtml(p.name || p.id)}</option>`).join('')\n : '<option value=\"\">No projects</option>';\n \n const overlay = document.createElement('div');\n overlay.className = 'modal-overlay';\n overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };\n overlay.innerHTML = `<div class=\"modal\" style=\"max-width:550px\">\n <h3>🔌 Port Allocations</h3>\n \n <div style=\"font-size:12px;color:var(--muted);margin-bottom:12px\">\n Agents request ports using <code>request_port({ service_name: \"api\", port: 3000 })</code>. \n Allocated ports are protected from other projects.\n </div>\n \n <h4 style=\"margin:12px 0 8px\">Allocate New Port</h4>\n <div style=\"display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:13px;margin-bottom:12px\">\n <div>\n <label style=\"font-size:11px;color:var(--muted)\">Project</label>\n <select id=\"allocPort-project\" style=\"width:100%;padding:4px;border:1px solid var(--border);border-radius:4px;background:var(--surface)\">\n ${projectOptions}\n </select>\n </div>\n <div>\n <label style=\"font-size:11px;color:var(--muted)\">Service Name *</label>\n <input type=\"text\" id=\"allocPort-service\" placeholder=\"e.g. api, frontend\" style=\"width:100%;padding:4px;border:1px solid var(--border);border-radius:4px;box-sizing:border-box\">\n </div>\n <div>\n <label style=\"font-size:11px;color:var(--muted)\">Port (optional)</label>\n <input type=\"number\" id=\"allocPort-port\" placeholder=\"Auto\" min=\"1024\" max=\"65535\" style=\"width:100%;padding:4px;border:1px solid var(--border);border-radius:4px;box-sizing:border-box\">\n </div>\n <div>\n <label style=\"font-size:11px;color:var(--muted)\">Description</label>\n <input type=\"text\" id=\"allocPort-desc\" placeholder=\"Optional\" style=\"width:100%;padding:4px;border:1px solid var(--border);border-radius:4px;box-sizing:border-box\">\n </div>\n </div>\n <button class=\"btn btn-primary\" onclick=\"allocatePortManual()\" style=\"margin-bottom:12px\">+ Allocate Port</button>\n \n <h4 style=\"margin:12px 0 8px\">Allocated Ports (${allocations.length})</h4>\n <div style=\"max-height:200px;overflow-y:auto\">\n ${allocationsHtml}\n </div>\n \n <h4 style=\"margin:16px 0 8px\">System Protected Ports</h4>\n <div style=\"font-size:13px;color:var(--muted)\">\n ${systemPorts.length > 0 ? systemPorts.map(p => `<span style=\"font-family:monospace;background:var(--surface);padding:2px 6px;border-radius:4px;margin:2px\">${p}</span>`).join(' ') : 'No additional protected ports (4000 always protected)'}\n </div>\n \n <div class=\"actions\" style=\"margin-top:16px;display:flex;gap:8px\">\n <button class=\"btn\" onclick=\"fetchPorts().then(() => showPortsDialog())\" style=\"margin-right:auto\">↻ Refresh</button>\n <button class=\"btn\" onclick=\"this.closest('.modal-overlay').remove()\">Close</button>\n </div>\n </div>`;\n document.body.appendChild(overlay);\n}\n\nasync function allocatePortManual() {\n const projectId = document.getElementById('allocPort-project')?.value;\n const serviceName = document.getElementById('allocPort-service')?.value?.trim();\n const port = document.getElementById('allocPort-port')?.value;\n const description = document.getElementById('allocPort-desc')?.value?.trim();\n \n if (!projectId) {\n alert('Please select a project');\n return;\n }\n if (!serviceName) {\n alert('Service name is required');\n return;\n }\n \n try {\n const res = await fetch(`/api/projects/${encodeURIComponent(projectId)}/ports/allocate`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ serviceName, port: port || undefined, description: description || undefined })\n });\n const data = await res.json();\n if (data.success) {\n addLog('info', `Allocated port ${data.port} for ${serviceName}`);\n document.querySelector('.modal-overlay')?.remove();\n await fetchPorts();\n showPortsDialog();\n } else {\n alert('Failed to allocate: ' + (data.error || 'Unknown error'));\n }\n } catch (e) {\n alert('Failed to allocate port: ' + e.message);\n }\n}\n\nasync function releasePort(port, projectId) {\n if (!confirm(`Release port ${port}? This will allow other projects to use it.`)) return;\n \n try {\n const res = await fetch(`/api/projects/${encodeURIComponent(projectId)}/ports/${port}/release`, {\n method: 'POST'\n });\n const data = await res.json();\n if (data.success) {\n addLog('info', `Released port ${port}`);\n document.querySelector('.modal-overlay')?.remove();\n await fetchPorts();\n showPortsDialog();\n } else {\n alert('Failed to release: ' + (data.error || 'Unknown error'));\n }\n } catch (e) {\n alert('Failed to release port: ' + e.message);\n }\n}\n\n// Fetch ports on load\nsetTimeout(fetchPorts, 2000);\n// Society Agent end\n\n// Society Agent - Three-level turn management\nlet currentTurnEl = null; // container for current agent turn\nlet currentTextEl = null; // compat ref\nlet turnToolsContainer = null; // .turn-content inside current turn\nlet traceDrawerEl = null; // <details class=\"trace-drawer\"> in current turn\nlet turnChangedFiles = []; // files changed this turn (for artifacts)\nlet lastStreamedText = ''; // agent text captured before each tool call (WHY)\n\nfunction createAgentTurn(agentId, agentName) {\n if (currentTurnEl) finalizeTurnOutcome(currentTurnEl, lastStreamedText);\n const displayName = agentName || agentId || 'Agent';\n const safeId = (agentId || '').replace(/[^a-z0-9_-]/gi, '_');\n const turn = document.createElement('div');\n turn.className = 'agent-turn';\n turn.dataset.turnId = agentId || '';\n turn.innerHTML = `\n <div class=\"turn-header\">\n <span class=\"turn-agent\">${escapeHtml(displayName)}</span>\n <span class=\"turn-time\">${new Date().toLocaleTimeString()}</span>\n <span class=\"outcome-badge outcome-running\" id=\"turn-badge-${safeId}\">⏳ Running</span>\n </div>\n <div class=\"turn-content\"></div>\n <details class=\"trace-drawer\" id=\"trace-${safeId}\">\n <summary>⚙️ <span class=\"trace-count\">0 operations</span> — click to inspect</summary>\n <div class=\"trace-list\"></div>\n </details>\n `;\n document.getElementById('messages').appendChild(turn);\n enforceMessageCap(document.getElementById('messages'));\n currentTurnEl = turn;\n turnToolsContainer = turn.querySelector('.turn-content');\n traceDrawerEl = turn.querySelector('.trace-drawer');\n turnChangedFiles = [];\n lastStreamedText = '';\n scrollToBottom();\n return agentId || safeId;\n}\n\nfunction finalizeTurnOutcome(turnEl, text) {\n if (!turnEl) return;\n turnEl.classList.add('completed');\n const badge = turnEl.querySelector('.outcome-badge');\n if (!badge) return;\n const t = text || '';\n let cls, label;\n if (/\\*\\*done[:\\*]/i.test(t) || /completed successfully/i.test(t)) {\n cls = 'outcome-success'; label = '✓ Done';\n } else if (/\\*\\*next[:\\*]/i.test(t) || /in progress/i.test(t)) {\n cls = 'outcome-partial'; label = '→ In Progress';\n } else if (/❌/.test(t) || /\\bfailed\\b/i.test(t)) {\n cls = 'outcome-failed'; label = '✗ Failed';\n } else if (!t) { badge.style.display = 'none'; return;\n } else { badge.style.display = 'none'; return; }\n badge.className = `outcome-badge ${cls}`;\n badge.textContent = label;\n}\n\nfunction addArtifactsPanel(turnEl, files) {\n const uniq = [...new Set(files)];\n if (!uniq.length || !turnEl) return;\n const content = turnEl.querySelector('.turn-content');\n if (!content) return;\n const panel = document.createElement('div');\n panel.className = 'artifacts-panel';\n panel.innerHTML = `\n <div class=\"artifacts-header\" onclick=\"this.nextElementSibling.style.display=this.nextElementSibling.style.display==='none'?'':'none'\">\n 📎 ${uniq.length} file${uniq.length!==1?'s':''} changed\n </div>\n <div class=\"artifacts-list\">${uniq.map(f=>`<div class=\"artifact-item\"><span class=\"artifact-op\">±</span>${escapeHtml(f)}</div>`).join('')}</div>\n `;\n content.appendChild(panel);\n}\n\nfunction addThinkingToTurn(text) {\n if (!currentTurnEl) return;\n const content = currentTurnEl.querySelector('.turn-content');\n \n // Find or create thinking element\n let thinkingEl = content.querySelector('.agent-thinking');\n if (!thinkingEl) {\n thinkingEl = document.createElement('div');\n thinkingEl.className = 'agent-thinking';\n thinkingEl.innerHTML = '<div class=\"thinking-label\">💭 Thinking...</div><div class=\"thinking-text\"></div>';\n content.appendChild(thinkingEl);\n }\n \n const textEl = thinkingEl.querySelector('.thinking-text');\n textEl.innerHTML = formatMarkdown(text);\n scrollToBottom();\n}\n\n// Resolve tool target label from input\nfunction toolTarget(name, inp) {\n if (!inp) return '';\n if (inp.path) return inp.path;\n if (inp.relativePath) return inp.relativePath;\n if (inp.command) return inp.command.substring(0, 70);\n if (inp.query) return inp.query.substring(0, 60);\n if (inp.task) return inp.task.substring(0, 50);\n if (inp.agent_id) return `→ ${inp.agent_id}`;\n return JSON.stringify(inp).substring(0, 60);\n}\n\nconst TOOL_ICONS = {\n run_command:'⚡', read_file:'📖', read_project_file:'📖',\n write_file:'✏️', write_project_file:'✏️', patch_file:'✏️',\n edit_file:'✏️', edit_project_file:'✏️',\n list_files:'📁', list_project_files:'📁', list_dir:'📁',\n search_in_files:'🔍', find_files:'🔍', grep_search:'🔍',\n http_request:'🌐', delete_file:'🗑️',\n git_status:'📊', git_diff:'📊',\n delegate_task:'📤', send_message:'💬', ask_agent:'💬',\n spawn_worker:'🤖', create_task:'📋',\n complete_task:'✅', fail_task:'❌', claim_task:'🎯',\n report_to_supervisor:'📨',\n};\n\nfunction addTraceItem(toolName, input, status, result, duration, why) {\n if (!traceDrawerEl) return null;\n const traceList = traceDrawerEl.querySelector('.trace-list');\n if (!traceList) return null;\n const toolKey = `${toolName}-${JSON.stringify(input||{}).substring(0,50)}`;\n const icon = TOOL_ICONS[toolName] || '🔧';\n const target = toolTarget(toolName, input);\n\n // Update existing running item on completion\n if (status === 'completed' || status === 'error') {\n for (const el of traceList.querySelectorAll('.trace-running')) {\n if (el.dataset.toolKey === toolKey) {\n el.classList.remove('trace-running');\n el.classList.add(status === 'error' ? 'trace-error' : 'trace-success');\n const ic = el.querySelector('.trace-item-icon');\n if (ic) ic.textContent = status === 'error' ? '✗' : '✓';\n if (duration) {\n let d = el.querySelector('.trace-item-duration');\n if (!d) { d = document.createElement('span'); d.className = 'trace-item-duration'; el.querySelector('.trace-item-header').appendChild(d); }\n d.textContent = `${duration}ms`;\n }\n if (status === 'error' && result) {\n const e2 = document.createElement('div');\n e2.className = 'trace-item-error-out';\n e2.textContent = result.replace(/^❌\\s*/,'').substring(0, 300);\n e2.title = 'Click to expand';\n e2.onclick = () => e2.classList.toggle('expanded');\n el.appendChild(e2);\n }\n return el;\n }\n }\n }\n\n // Create new item\n const item = document.createElement('div');\n item.dataset.toolKey = toolKey;\n item.className = `trace-item trace-${status==='error'?'error':status==='completed'?'success':'running'}`;\n const dspIcon = status==='completed' ? '✓' : status==='error' ? '✗' : icon;\n const whyHtml = why ? `<div class=\"trace-item-why\">→ ${escapeHtml(why.substring(0,100))}</div>` : '';\n const errHtml = (status==='error' && result)\n ? `<div class=\"trace-item-error-out\" title=\"Click to expand\" onclick=\"this.classList.toggle('expanded')\">${escapeHtml(result.replace(/^❌\\s*/,'').substring(0,300))}</div>` : '';\n item.innerHTML = `\n <div class=\"trace-item-header\">\n <span class=\"trace-item-icon\">${dspIcon}</span>\n <span class=\"trace-item-name\">${escapeHtml(toolName.replace(/_/g,' '))}</span>\n <span class=\"trace-item-target\">${escapeHtml(target)}</span>\n </div>${whyHtml}${errHtml}`;\n traceList.appendChild(item);\n const countEl = traceDrawerEl.querySelector('.trace-count');\n if (countEl) { const n = traceList.querySelectorAll('.trace-item').length; countEl.textContent = `${n} operation${n!==1?'s':''}`; }\n return item;\n}\n\n// Legacy shim — delegates to trace system\nfunction addToolCardToTurn(toolName, input, status, result, duration) {\n addTraceItem(toolName, input, status, result, duration, null);\n}\n\nfunction completeTurn(summary) {\n if (!currentTurnEl) return;\n finalizeTurnOutcome(currentTurnEl, lastStreamedText);\n if (turnChangedFiles.length > 0) addArtifactsPanel(currentTurnEl, turnChangedFiles);\n currentTurnEl.classList.add('completed');\n currentTurnEl = null;\n currentTextEl = null;\n currentStreamEl = null;\n turnToolsContainer = null;\n traceDrawerEl = null;\n turnChangedFiles = [];\n lastStreamedText = '';\n scrollToBottom();\n}\n\n// Streaming agent messages\nlet streamDidFire = false;\nlet lastStreamChunk = ''; // Detect repeated chunks\nlet repeatCount = 0;\nsocket.on('agent-message', (data) => {\n // Filter to selected agent only\n if (selectedAgentId && data.agentId) {\n const _sel = getSelectedAgent();\n if (_sel && (data.agentId !== _sel.agentId || (data.projectId && data.projectId !== _sel.projectId))) {\n addLog('info', `[${data.agentName || data.agentId}] ${(data.message || '').substring(0, 50)}...`);\n return;\n }\n }\n\n if (data.isStreaming) {\n // Dedup repeated chunks\n if (data.message === lastStreamChunk) {\n repeatCount++;\n if (repeatCount > 3) return;\n } else { lastStreamChunk = data.message; repeatCount = 0; }\n\n streamDidFire = true;\n\n // Remove tool progress indicator when response starts\n if (currentTurnEl) {\n const prog = currentTurnEl.querySelector('.turn-progress');\n if (prog) prog.remove();\n }\n\n // ── Level 1: stream text INTO the current agent turn ──\n if (!currentStreamEl) {\n removeTyping();\n if (!currentTurnEl) createAgentTurn(data.agentId, data.agentName);\n const respDiv = document.createElement('div');\n respDiv.className = 'turn-response';\n turnToolsContainer.appendChild(respDiv);\n currentStreamEl = respDiv;\n currentTextEl = respDiv; // compat\n }\n\n streamText += data.message;\n lastStreamedText = streamText; // track for WHY + outcome detection\n currentStreamEl.innerHTML = formatMarkdown(streamText);\n scrollToBottom();\n\n } else if (data.isDone) {\n // Finalize rendered text\n if (currentStreamEl && streamText) {\n currentStreamEl.innerHTML = formatMarkdown(streamText);\n }\n // ── 16.6 NO_REPLY / silent turns: discard from UI ────────────────────────────────\n if (!streamText.trim() || /^\\s*NO_REPLY\\s*$/i.test(streamText.trim())) {\n if (currentTurnEl) currentTurnEl.remove();\n currentStreamEl = null; currentTextEl = null; currentTurnEl = null;\n turnToolsContainer = null; traceDrawerEl = null; turnChangedFiles = [];\n streamText = ''; lastStreamedText = ''; isStreaming = false;\n lastStreamChunk = ''; repeatCount = 0;\n document.getElementById('sendBtn').disabled = false;\n document.getElementById('stopBtn').style.display = 'none';\n removeTyping();\n return;\n }\n // Outcome badge + artifacts\n if (currentTurnEl) {\n finalizeTurnOutcome(currentTurnEl, streamText);\n if (turnChangedFiles.length > 0) addArtifactsPanel(currentTurnEl, turnChangedFiles);\n currentTurnEl.classList.add('completed');\n }\n // Clean up\n currentStreamEl = null;\n currentTextEl = null;\n currentTurnEl = null;\n turnToolsContainer = null;\n traceDrawerEl = null;\n turnChangedFiles = [];\n streamText = '';\n lastStreamedText = '';\n isStreaming = false;\n lastStreamChunk = '';\n repeatCount = 0;\n document.getElementById('sendBtn').disabled = false;\n document.getElementById('stopBtn').style.display = 'none';\n removeTyping();\n }\n});\n\n// Purpose events\nsocket.on('purpose-started', (data) => {\n addLog('info', `Purpose started: ${data.description}`);\n // Show task card when a new purpose starts\n addMessage('system', `🎯 <strong>Task:</strong> ${escapeHtml(data.description)}`);\n});\n\n// Society Agent start - Stop agent function\nfunction stopAgent() {\n if (!isStreaming) return;\n \n // Tell server to stop\n const agentId = getSelectedAgent()?.agentId || 'default';\n socket.emit('stop-agent', { agentId });\n \n // Update UI immediately\n if (currentTextEl) {\n const stopMsg = document.createElement('div');\n stopMsg.className = 'agent-stopped';\n stopMsg.innerHTML = '<em style=\"color:var(--red);\">⛔ Stopped by user</em>';\n currentTextEl.appendChild(stopMsg);\n }\n completeTurn();\n currentStreamEl = null;\n currentTextEl = null;\n streamText = '';\n isStreaming = false;\n lastStreamChunk = ''; // Reset repeat detection\n repeatCount = 0;\n document.getElementById('sendBtn').disabled = false;\n document.getElementById('stopBtn').style.display = 'none';\n removeTyping();\n addLog('info', 'Agent stopped by user');\n}\n// Society Agent end\n\n// Society Agent - Tool executions → Level 3 trace drawer\nlet toolCount = 0;\nlet lastToolUpdate = null; // compat ref, unused now\nsocket.on('tool-execution', (data) => {\n if (selectedAgentId && data.agentId) {\n const _sel = getSelectedAgent();\n if (_sel && (data.agentId !== _sel.agentId || (data.projectId && data.projectId !== _sel.projectId))) return;\n }\n\n // Ensure a turn exists\n if (!currentTurnEl && data.status === 'started') {\n createAgentTurn(data.agentId, data.agentName);\n }\n\n // WHY: infer from the last line of streamed text before this tool call\n let why = null;\n if (data.status === 'started' && lastStreamedText) {\n const lines = lastStreamedText.trim().split('\\n').filter(l => l.trim());\n const last = (lines[lines.length - 1] || '').trim();\n // Extract [→ reason] annotation if agent included it\n const annot = last.match(/\\[→\\s*([^\\]]{3,80})\\]/);\n if (annot) why = annot[1];\n else if (last.length > 5 && last.length < 120 && !last.startsWith('#')) why = last.replace(/^[-*]\\s*/, '').substring(0, 80);\n }\n\n if (data.status === 'started') {\n toolCount++;\n addTraceItem(data.toolName, data.toolInput, 'started', null, null, why);\n // Show live progress inside turn (Level 1 — minimal, replaces firehose)\n if (currentTurnEl) {\n let prog = currentTurnEl.querySelector('.turn-progress');\n if (!prog) {\n prog = document.createElement('div');\n prog.className = 'turn-progress';\n turnToolsContainer && turnToolsContainer.appendChild(prog);\n }\n prog.textContent = `⏳ ${data.toolName.replace(/_/g, ' ')}…`;\n }\n scrollToBottom();\n } else {\n addTraceItem(data.toolName, data.toolInput, data.status, data.result, data.duration, null);\n // Remove progress indicator\n if (currentTurnEl) {\n const prog = currentTurnEl.querySelector('.turn-progress');\n if (prog) prog.remove();\n }\n // ── Level 1: show errors inline, hide successes ──\n if (data.status === 'error' && currentTurnEl) {\n const errDiv = document.createElement('div');\n errDiv.className = 'turn-error-inline';\n errDiv.textContent = `✗ ${data.toolName}: ${(data.result || '').replace(/^❌\\s*/, '').substring(0, 120)}`;\n turnToolsContainer && turnToolsContainer.appendChild(errDiv);\n scrollToBottom();\n }\n }\n});\n// Society Agent end\n\n// ── 16.8 Context introspection ─────────────────────────────────────────────────\nsocket.on('context-stats', (data) => {\n const bar = document.getElementById('ctxStatsBar');\n if (bar) {\n bar.textContent = `ctx: ${formatNumber(data.estimated)} tokens · ${data.messageCount} msgs${data.trimCount > 0 ? ` · trimmed ${data.trimCount}` : ''}`;\n if (debugMode) bar.classList.add('visible');\n }\n addLog('info', `Context: ~${formatNumber(data.estimated)} tokens, ${data.messageCount} messages${data.trimCount ? `, trimmed ${data.trimCount}` : ''}`);\n});\n\nsocket.on('team-formed', (data) => {\n addLog('info', `Team formed: ${data.teamSize} agents`);\n loadProjects();\n});\n\nsocket.on('progress-update', (data) => {\n addLog('info', `Progress: ${JSON.stringify(data.progress)}`);\n});\n\nsocket.on('agent-status-change', (data) => {\n // Only log significant status changes\n if (data.status === 'error' || data.status === 'stopped') {\n addLog(data.status === 'error' ? 'error' : 'warn', `Agent ${data.agentId}: ${data.status}`);\n }\n loadProjects();\n});\n\n// Society Agent - Task started event shows what was asked\nsocket.on('task-started', (data) => {\n // No log - the task card shows visually\n // Create a new turn for this task\n const turnId = createAgentTurn(data.agentId, data.agentName);\n // Add task card showing what was asked\n const taskHtml = `<div class=\"task-card\" style=\"background:linear-gradient(145deg,#1a1f26,#21262d);border:2px solid #58a6ff;border-radius:8px;padding:12px;margin-bottom:10px;\">\n <div style=\"display:flex;align-items:center;gap:8px;margin-bottom:8px;\">\n <span style=\"background:#58a6ff;color:#0d1117;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;\">TASK</span>\n <span style=\"color:#8b949e;font-size:12px;\">${new Date(data.timestamp).toLocaleTimeString()}</span>\n </div>\n <div style=\"color:#c9d1d9;line-height:1.5;\">${escapeHtml(data.task)}</div>\n <div class=\"task-budget\" id=\"task-budget-${data.agentId}\">\n <span class=\"bud-tokens\" id=\"task-budget-tok-${data.agentId}\">0 tokens</span>\n <span class=\"bud-cost\" id=\"task-budget-cost-${data.agentId}\">$0.00</span>\n <span class=\"bud-calls\" id=\"task-budget-calls-${data.agentId}\">0 calls</span>\n </div>\n </div>`;\n const currentTurn = document.querySelector(`[data-turn-id=\"${turnId}\"] .turn-content`);\n if (currentTurn) {\n currentTurn.insertAdjacentHTML('afterbegin', taskHtml);\n }\n});\n\n// Society Agent - Live budget tracking per task\nconst taskBudgets = {}; // agentId -> { tokens, cost, calls }\nsocket.on('budget-update', (data) => {\n const id = data.agentId;\n if (!taskBudgets[id]) taskBudgets[id] = { tokens: 0, cost: 0, calls: 0 };\n taskBudgets[id].tokens = data.sessionTokens;\n taskBudgets[id].cost = data.sessionCostUsd;\n taskBudgets[id].calls = (taskBudgets[id].calls || 0) + 1;\n\n const tokEl = document.getElementById(`task-budget-tok-${id}`);\n const costEl = document.getElementById(`task-budget-cost-${id}`);\n const callsEl = document.getElementById(`task-budget-calls-${id}`);\n if (tokEl) tokEl.textContent = formatNumber(data.sessionTokens) + ' tokens';\n if (costEl) {\n const c = data.sessionCostUsd;\n costEl.textContent = c < 0.01 ? `${(c*100).toFixed(2)}¢` : `$${c.toFixed(4)}`;\n costEl.style.color = c > 1 ? 'var(--red)' : c > 0.1 ? 'var(--yellow)' : 'var(--green)';\n }\n if (callsEl) callsEl.textContent = taskBudgets[id].calls + ' call' + (taskBudgets[id].calls !== 1 ? 's' : '');\n});\n\n// Society Agent - Delegation visibility events\nsocket.on('task-delegated', (data) => {\n addLog('info', `📋 Task delegated: ${data.fromAgentName || data.fromAgent} → ${data.toAgentName || data.toAgent}`);\n const priorityBadge = data.priority === 'critical' ? '🔴' : data.priority === 'high' ? '🟠' : data.priority === 'medium' ? '🟡' : '🟢';\n addMessage('system', `${priorityBadge} <strong>Delegation:</strong> ${data.fromAgentName || data.fromAgent} → ${data.toAgentName || data.toAgent}<br><em>${data.task}</em>`);\n});\n\nsocket.on('delegation-message', (data) => {\n addLog('info', `📨 Delegation message: ${data.fromAgentName} → ${data.toAgentName}`);\n // Show detailed delegation in a collapsible format\n let html = `<div class=\"delegation-card\" style=\"background:#1c2128;border:1px solid #30363d;border-radius:6px;padding:10px;margin:5px 0;\">`;\n html += `<div style=\"color:#58a6ff;font-weight:600;\">📋 Task from ${data.fromAgentName}</div>`;\n html += `<div style=\"margin:5px 0;\"><strong>Task:</strong> ${data.task}</div>`;\n if (data.desired_state) html += `<div style=\"margin:5px 0;color:#8b949e;\"><strong>Desired State:</strong> ${data.desired_state.substring(0, 200)}${data.desired_state.length > 200 ? '...' : ''}</div>`;\n if (data.acceptance_criteria?.length) html += `<div style=\"margin:5px 0;color:#3fb950;\"><strong>Acceptance:</strong> ${data.acceptance_criteria.length} criteria</div>`;\n html += `</div>`;\n addMessage('system', html);\n});\n\nsocket.on('agent-report', (data) => {\n const statusIcon = {\n 'in_progress': '🔄',\n 'completed': '✅',\n 'blocked': '🚫',\n 'needs_info': '❓',\n 'failed': '❌'\n }[data.status] || '📝';\n addLog(data.status === 'failed' ? 'error' : data.status === 'blocked' ? 'warn' : 'info', \n `${statusIcon} Report from ${data.fromAgentName}: ${data.summary}`);\n \n let html = `<div class=\"report-card\" style=\"background:#1c2128;border:1px solid ${data.status === 'completed' ? '#3fb950' : data.status === 'blocked' ? '#f85149' : '#30363d'};border-radius:6px;padding:10px;margin:5px 0;\">`;\n html += `<div style=\"font-weight:600;\">${statusIcon} Report from ${data.fromAgentName}</div>`;\n html += `<div><strong>Status:</strong> ${data.status}</div>`;\n html += `<div>${data.summary}</div>`;\n if (data.completion_percentage !== undefined) html += `<div style=\"color:#58a6ff;\">Progress: ${data.completion_percentage}%</div>`;\n if (data.blockers?.length) html += `<div style=\"color:#f85149;\">Blockers: ${data.blockers.join(', ')}</div>`;\n if (data.questions?.length) html += `<div style=\"color:#d29922;\">Questions: ${data.questions.join(', ')}</div>`;\n html += `</div>`;\n addMessage('system', html);\n});\n\n// Society Agent - Progress updates for long-running autonomous tasks\nsocket.on('agent-progress', (data) => {\n addLog('info', `📊 ${data.agentName} progress: iteration ${data.iteration}, ${data.totalFilesCreated} files created`);\n // Don't spam the chat with every progress update, just log it\n});\n\n// File creation events — track for per-turn artifacts panel\nsocket.on('file-created', (data) => {\n addLog('info', `File created: ${data.relativePath} (${formatSize(data.size)})`);\n // Accumulate for artifacts panel instead of noisy addMessage\n if (currentTurnEl && data.relativePath) {\n turnChangedFiles.push(data.relativePath);\n } else {\n addMessage('system', `📄 Created: ${data.relativePath} (${formatSize(data.size)})`);\n }\n refreshFiles();\n});\n\nsocket.on('file-deleted', (data) => {\n addLog('info', `Deleted: ${data.relativePath}${data.wasDir ? ' (directory)' : ''}`);\n refreshFiles();\n});\n\n// System events (multi-agent)\nsocket.on('system-event', (data) => {\n addLog(data.type.includes('error') ? 'error' : 'info', data.message || data.type);\n if (data.type === 'purpose-launching') {\n addMessage('system', data.message);\n }\n // Society Agent - handle system pause/resume events\n if (data.type === 'system-paused') {\n updatePauseUI(true, data.reason);\n addLog('warn', `System PAUSED: ${data.reason || 'Maintenance mode'}`);\n }\n if (data.type === 'system-resumed') {\n updatePauseUI(false);\n addLog('info', 'System RESUMED - accepting new work');\n }\n});\n\n// Society Agent start - System pause/resume management\nlet isSystemPaused = false;\n\nfunction updatePauseUI(paused, reason = '') {\n isSystemPaused = paused;\n const banner = document.getElementById('pauseBanner');\n const btn = document.getElementById('pauseBtn');\n const btnIcon = document.getElementById('pauseBtnIcon');\n const btnText = document.getElementById('pauseBtnText');\n const reasonEl = document.getElementById('pauseReason');\n const statusDot = document.getElementById('statusDot');\n \n if (paused) {\n banner.classList.add('visible');\n btn.classList.add('paused');\n btnIcon.textContent = '▶️';\n btnText.textContent = 'Resume';\n if (reason) reasonEl.textContent = `(${reason})`;\n statusDot.classList.remove('online');\n statusDot.classList.add('paused');\n } else {\n banner.classList.remove('visible');\n btn.classList.remove('paused');\n btnIcon.textContent = '⏸';\n btnText.textContent = 'Maintenance';\n reasonEl.textContent = '';\n statusDot.classList.remove('paused');\n statusDot.classList.add('online');\n }\n}\n\nasync function toggleSystemPause() {\n if (isSystemPaused) {\n await resumeSystem();\n } else {\n await pauseSystem();\n }\n}\n\nasync function pauseSystem() {\n const reason = prompt('Enter reason for maintenance pause (optional):', 'Manual maintenance');\n if (reason === null) return; // cancelled\n \n try {\n const res = await fetch('/api/system/pause', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ reason: reason || 'Manual maintenance' })\n });\n const data = await res.json();\n if (data.success) {\n updatePauseUI(true, data.pausedState?.reason);\n addLog('warn', 'System paused for maintenance');\n } else {\n addLog('error', data.message || 'Failed to pause system');\n }\n } catch (e) {\n addLog('error', 'Failed to pause system: ' + e.message);\n }\n}\n\nasync function resumeSystem() {\n try {\n const res = await fetch('/api/system/resume', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' }\n });\n const data = await res.json();\n if (data.success) {\n updatePauseUI(false);\n addLog('info', 'System resumed');\n } else {\n addLog('error', data.message || 'Failed to resume system');\n }\n } catch (e) {\n addLog('error', 'Failed to resume system: ' + e.message);\n }\n}\n\nasync function checkSystemPauseState() {\n try {\n const res = await fetch('/api/system/state');\n if (res.ok) {\n const data = await res.json();\n updatePauseUI(data.systemPaused, data.pausedState?.reason);\n }\n } catch (e) {\n console.warn('Failed to check system pause state:', e);\n }\n}\n// Society Agent end - System pause/resume\n\n// ============================================================================\n// API Functions\n// ============================================================================\n\nasync function checkServerStatus() {\n try {\n const res = await fetch(`${API}/api/status`);\n const data = await res.json();\n document.getElementById('sysStatus').textContent = data.status === 'ok' ? 'Online' : 'Error';\n if (data.outputDir) {\n const el = document.getElementById('outputDir');\n // Show short path, full path in tooltip\n const short = data.outputDir.replace(/^\\/workspace\\//, '');\n el.textContent = short;\n el.title = data.outputDir;\n }\n addLog('info', `Server: ${data.environment} | Output: ${data.outputDir || 'projects/'}`);\n refreshFiles();\n } catch (e) {\n document.getElementById('sysStatus').textContent = 'Error';\n addLog('error', 'Failed to reach server');\n }\n}\n\nasync function checkApiKey() {\n try {\n const res = await fetch(`${API}/api/config/api-key`);\n const data = await res.json();\n const el = document.getElementById('apiKeyStatus');\n if (data.configured) {\n el.textContent = data.masked || 'Set';\n el.style.color = 'var(--green)';\n } else {\n el.textContent = 'Not set';\n el.style.color = 'var(--red)';\n }\n } catch (e) {\n addLog('error', 'Failed to check API key');\n }\n}\n\nasync function sendMessage() {\n const input = document.getElementById('input');\n const text = input.value.trim();\n if ((!text && chatAttachments.length === 0) || isStreaming) return;\n\n // Hide welcome\n const welcome = document.getElementById('welcome');\n if (welcome) welcome.style.display = 'none';\n\n // Show user message with attachment previews\n let attachHtml = '';\n if (chatAttachments.length > 0) {\n attachHtml = '<div class=\"msg-attachments\">' + chatAttachments.map(a => {\n if (a.type.startsWith('image/')) {\n return `<img src=\"${a.dataUrl}\" title=\"${escapeHtml(a.name)}\">`;\n }\n return `<span class=\"file-badge\">📄 ${escapeHtml(a.name)}</span>`;\n }).join('') + '</div>';\n }\n const msgEl = addMessage('user', text);\n if (attachHtml) msgEl.querySelector('.content').innerHTML += attachHtml;\n\n // Build content array for API\n const contentParts = [];\n for (const a of chatAttachments) {\n if (a.type.startsWith('image/')) {\n contentParts.push({ type: 'image', source: { type: 'base64', media_type: a.mediaType, data: a.base64 } });\n } else {\n // Send text files as text content\n contentParts.push({ type: 'text', text: `[Attached file: ${a.name}]\\n${atob(a.base64)}` });\n }\n }\n if (text) contentParts.push({ type: 'text', text: text });\n\n // Clear attachments\n chatAttachments = [];\n renderAttachChips();\n\n input.value = '';\n input.style.height = 'auto';\n messageCount++;\n document.getElementById('msgCount').textContent = messageCount;\n\n isStreaming = true;\n streamDidFire = false;\n document.getElementById('sendBtn').disabled = true;\n document.getElementById('stopBtn').style.display = 'inline-block'; // Society Agent - show stop\n showTyping();\n\n try {\n const sel = getSelectedAgent();\n const agentId = sel?.agentId;\n const projectId = sel?.projectId;\n const agentName = selectedAgentId ? (allProjectAgents.find(a => (a.projectId + ':' + a.id) === selectedAgentId)?.name || agentId) : undefined;\n\n const res = await fetch(`${API}/api/purpose/start`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n description: text,\n attachments: contentParts.length > 1 || contentParts[0]?.type === 'image' ? contentParts : undefined,\n agentId,\n projectId,\n }),\n });\n const data = await res.json();\n\n if (!res.ok) {\n removeTyping();\n addMessage('system', `Error: ${data.error || 'Unknown error'}`);\n isStreaming = false;\n document.getElementById('sendBtn').disabled = false;\n if (res.status === 401) showApiKeyModal();\n return;\n }\n\n // Only show HTTP response if WebSocket streaming didn't already handle it\n if (!streamDidFire && data.response) {\n removeTyping();\n const displayName = data.agentName || agentName || 'Society Agent';\n const el = addMessage('assistant', '', displayName);\n el.querySelector('.content').innerHTML = formatMarkdown(data.response);\n isStreaming = false;\n document.getElementById('sendBtn').disabled = false;\n }\n\n messageCount++;\n document.getElementById('msgCount').textContent = messageCount;\n addLog('info', `Response from ${data.agentName || agentName || 'Society Agent'} (history: ${data.historyLength} messages)`);\n } catch (e) {\n removeTyping();\n addMessage('system', `Network error: ${e.message}`);\n isStreaming = false;\n document.getElementById('sendBtn').disabled = false;\n }\n}\n\nasync function saveApiKey() {\n const input = document.getElementById('apiKeyInput');\n const key = input.value.trim();\n if (!key) return;\n\n try {\n const res = await fetch(`${API}/api/config/api-key`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ apiKey: key }),\n });\n const data = await res.json();\n if (data.success) {\n hideApiKeyModal();\n checkApiKey();\n addLog('info', 'API key saved successfully');\n } else {\n alert(data.error || 'Failed to save');\n }\n } catch (e) {\n alert('Network error');\n }\n}\n\nasync function refreshAgents() {\n try {\n const res = await fetch(`${API}/api/agents`);\n const data = await res.json();\n const list = document.getElementById('agentList');\n if (!data.agents || data.agents.length === 0) {\n list.innerHTML = '<div style=\"padding:16px;color:var(--muted);font-size:12px\">No active agents</div>';\n return;\n }\n list.innerHTML = data.agents.map(a => `\n <div class=\"agent-card\">\n <div class=\"name\">${a.name || a.id}</div>\n <div class=\"role\">${a.role}</div>\n <span class=\"status-badge ${a.status}\">${a.status}</span>\n </div>\n `).join('');\n } catch (e) {}\n}\n\n// ============================================================================\n// File Explorer - Tree with expand/collapse, drag-drop, context menu\n// ============================================================================\n\nlet fileData = []; // flat list from server\nlet activeProjectFolder = null; // Society Agent - filter file tree to selected project\nlet expandedDirs = new Set(['']); // track open folders ('' = root always open)\nlet selectedPath = null; // currently selected item\nlet dragSrc = null; // path being dragged\nlet treeEventsDelegated = false; // Society Agent - track if delegated events attached\n\n// Build nested tree from flat file list\nfunction buildTree(files) {\n const root = { children: {}, path: '', isDir: true };\n for (const f of files) {\n const parts = f.path.split('/');\n let node = root;\n let pathSoFar = '';\n for (let i = 0; i < parts.length; i++) {\n pathSoFar = pathSoFar ? pathSoFar + '/' + parts[i] : parts[i];\n if (!node.children[parts[i]]) {\n node.children[parts[i]] = {\n name: parts[i], path: pathSoFar,\n isDir: i < parts.length - 1 || f.isDir,\n size: f.isDir ? 0 : f.size,\n children: {},\n };\n }\n node = node.children[parts[i]];\n }\n if (!f.isDir) { node.size = f.size; node.isDir = false; }\n }\n return root;\n}\n\nfunction fileIcon(name, isDir) {\n if (isDir) return '📁';\n const ext = name.split('.').pop().toLowerCase();\n const icons = {\n js:'🔸', ts:'🔵', py:'🐍', html:'🌐', css:'🎨',\n json:'{}', md:'📝', txt:'📄', sh:'⌨', sql:'🗃',\n yaml:'⚙', yml:'⚙', xml:'📜', csv:'📊', log:'📋',\n png:'🎨', jpg:'🎨', jpeg:'🎨', gif:'🎨', svg:'🎨',\n pdf:'📕', zip:'📦', tar:'📦', gz:'📦',\n };\n return icons[ext] || '📄';\n}\n\nfunction renderTree(node, depth = 0) {\n const entries = Object.values(node.children);\n // Sort: dirs first, then alpha\n entries.sort((a, b) => {\n if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;\n return a.name.localeCompare(b.name);\n });\n\n return entries.map(e => {\n const indent = depth * 16;\n const isOpen = expandedDirs.has(e.path);\n const sel = selectedPath === e.path ? ' selected' : '';\n\n if (e.isDir) {\n const hasKids = Object.keys(e.children).length > 0;\n const chevCls = hasKids ? (isOpen ? 'chevron open' : 'chevron') : 'chevron empty';\n let html = `<div class=\"tree-node\" data-path=\"${escapeHtml(e.path)}\" data-dir=\"true\">\n <div class=\"tree-row dir-row${sel}\" style=\"padding-left:${indent + 8}px\"\n draggable=\"true\" data-path=\"${escapeHtml(e.path)}\" data-dir=\"true\">\n <span class=\"${chevCls}\">▶</span>\n <span class=\"icon\">${fileIcon(e.name, true)}</span>\n <span class=\"name\">${escapeHtml(e.name)}</span>\n <span class=\"actions\">\n <button class=\"act-btn\" title=\"New file here\" onclick=\"event.stopPropagation();promptNewFileInDir('${escapeAttr(e.path)}')\">+📄</button>\n <button class=\"act-btn\" title=\"New folder here\" onclick=\"event.stopPropagation();promptNewFolderInDir('${escapeAttr(e.path)}')\">+📁</button>\n </span>\n </div>\n <div class=\"tree-children ${isOpen ? '' : 'collapsed'}\">\n ${hasKids ? renderTree(e, depth + 1) : ''}\n </div>\n </div>`;\n return html;\n } else {\n return `<div class=\"tree-node\" data-path=\"${escapeHtml(e.path)}\">\n <div class=\"tree-row${sel}\" style=\"padding-left:${indent + 24}px\"\n draggable=\"true\" data-path=\"${escapeHtml(e.path)}\" data-dir=\"false\">\n <span class=\"icon\">${fileIcon(e.name, false)}</span>\n <span class=\"name\">${escapeHtml(e.name)}</span>\n <span class=\"size\">${formatSize(e.size)}</span>\n </div>\n </div>`;\n }\n }).join('');\n}\n\nasync function refreshFiles() {\n try {\n const res = await fetch(`${API}/api/workspace/files`);\n const data = await res.json();\n // Society Agent start - Filter to selected project's files when an agent is active\n const allFiles = data.files || [];\n if (activeProjectFolder) {\n const prefix = activeProjectFolder + '/';\n fileData = allFiles.filter(f => f.path === activeProjectFolder || f.path.startsWith(prefix));\n } else {\n fileData = allFiles;\n }\n const visibleFiles = fileData.filter(f => !f.isDir);\n const visibleDirs = fileData.filter(f => f.isDir);\n document.getElementById('fileCount').textContent = `${visibleFiles.length} files, ${visibleDirs.length} dirs`;\n // Society Agent end\n\n const tree = document.getElementById('fileTree');\n if (fileData.length === 0) {\n tree.innerHTML = '<div style=\"padding:16px;color:var(--muted);font-size:12px\">No files yet — ask the agent to create something</div>';\n return;\n }\n\n renderTreeFromCache();\n } catch (e) {\n addLog('warn', 'Failed to refresh files');\n }\n}\n\n// Society Agent start - Render tree from cached data (no API call, instant)\nfunction renderTreeFromCache() {\n const tree = document.getElementById('fileTree');\n if (!tree || fileData.length === 0) return;\n \n const root = buildTree(fileData);\n tree.innerHTML = renderTree(root);\n attachTreeHandlers();\n}\n// Society Agent end\n\n// Society Agent start - Attach delegated event handlers (only once)\nfunction attachDelegatedTreeEvents() {\n if (treeEventsDelegated) return;\n treeEventsDelegated = true;\n \n const tree = document.getElementById('fileTree');\n if (!tree) return;\n\n // Click handler (event delegation)\n tree.addEventListener('click', (e) => {\n const row = e.target.closest('.tree-row');\n if (!row) return;\n\n const nodePath = row.dataset.path;\n const isDir = row.dataset.dir === 'true';\n\n // Toggle folder expand/collapse\n if (isDir) {\n if (expandedDirs.has(nodePath)) {\n expandedDirs.delete(nodePath);\n } else {\n expandedDirs.add(nodePath);\n }\n renderTreeFromCache();\n return;\n }\n\n // Select / view file\n selectedPath = nodePath;\n tree.querySelectorAll('.tree-row').forEach(r => r.classList.remove('selected'));\n row.classList.add('selected');\n\n if (!isDir) {\n viewFile(nodePath);\n }\n });\n\n // Double-click to rename\n tree.addEventListener('dblclick', (e) => {\n const row = e.target.closest('.tree-row');\n if (!row) return;\n e.preventDefault();\n startRename(row.dataset.path, row.dataset.dir === 'true');\n });\n\n // Right-click context menu\n tree.addEventListener('contextmenu', (e) => {\n const row = e.target.closest('.tree-row');\n if (!row) return;\n e.preventDefault();\n showContextMenu(e.clientX, e.clientY, row.dataset.path, row.dataset.dir === 'true');\n });\n}\n// Society Agent end\n\nfunction attachTreeHandlers() {\n const tree = document.getElementById('fileTree');\n \n // Attach delegated events once\n attachDelegatedTreeEvents();\n\n // Drag-and-drop (must re-attach since rows are recreated)\n tree.querySelectorAll('.tree-row[draggable]').forEach(row => {\n row.addEventListener('dragstart', (e) => {\n dragSrc = row.dataset.path;\n e.dataTransfer.effectAllowed = 'move';\n e.dataTransfer.setData('text/plain', row.dataset.path);\n // Custom ghost\n const ghost = document.createElement('div');\n ghost.className = 'drag-ghost';\n ghost.textContent = row.dataset.path.split('/').pop();\n document.body.appendChild(ghost);\n e.dataTransfer.setDragImage(ghost, 0, 0);\n setTimeout(() => ghost.remove(), 0);\n row.style.opacity = '0.4';\n });\n\n row.addEventListener('dragend', () => {\n dragSrc = null;\n row.style.opacity = '';\n tree.querySelectorAll('.tree-row.drag-over').forEach(r => r.classList.remove('drag-over'));\n });\n\n // Only dirs receive drops\n if (row.dataset.dir === 'true') {\n row.addEventListener('dragover', (e) => {\n e.preventDefault();\n e.dataTransfer.dropEffect = 'move';\n row.classList.add('drag-over');\n });\n row.addEventListener('dragleave', () => {\n row.classList.remove('drag-over');\n });\n row.addEventListener('drop', async (e) => {\n e.preventDefault();\n row.classList.remove('drag-over');\n const from = e.dataTransfer.getData('text/plain');\n const toDir = row.dataset.path;\n if (!from || from === toDir) return;\n // Don't drop into self\n if (toDir.startsWith(from + '/')) return;\n const fileName = from.split('/').pop();\n const to = toDir + '/' + fileName;\n await moveFile(from, to);\n });\n }\n });\n\n // Also allow drop on root (file tree container) — drop at root level\n tree.addEventListener('dragover', (e) => {\n if (e.target === tree) {\n e.preventDefault();\n e.dataTransfer.dropEffect = 'move';\n }\n });\n tree.addEventListener('drop', async (e) => {\n if (e.target !== tree) return;\n e.preventDefault();\n const from = e.dataTransfer.getData('text/plain');\n if (!from) return;\n const fileName = from.split('/').pop();\n await moveFile(from, fileName);\n });\n\n // Also support file upload drag-drop onto folder rows\n tree.querySelectorAll('.tree-row[data-dir=\"true\"]').forEach(row => {\n row.addEventListener('dragover', (e) => {\n // Check if it's an external file (not internal drag)\n if (e.dataTransfer.types.includes('Files')) {\n e.preventDefault();\n e.dataTransfer.dropEffect = 'copy';\n row.classList.add('drag-over');\n }\n });\n row.addEventListener('drop', (e) => {\n row.classList.remove('drag-over');\n if (e.dataTransfer.files.length > 0) {\n e.preventDefault();\n e.stopPropagation();\n uploadFiles(e.dataTransfer.files, row.dataset.path);\n }\n });\n });\n}\n\n// ============================================================================\n// Context Menu\n// ============================================================================\n\nfunction showContextMenu(x, y, filePath, isDir) {\n hideContextMenu();\n selectedPath = filePath;\n\n const menu = document.createElement('div');\n menu.className = 'ctx-menu';\n menu.id = 'ctxMenu';\n\n const items = [];\n if (isDir) {\n items.push({ label: '📄+ New File', action: () => promptNewFileInDir(filePath) });\n items.push({ label: '📁+ New Folder', action: () => promptNewFolderInDir(filePath) });\n items.push({ type: 'sep' });\n items.push({ label: '📤 Upload Here', action: () => uploadToDir(filePath) });\n items.push({ type: 'sep' });\n } else {\n items.push({ label: '👁 View', action: () => viewFile(filePath) });\n if (isEditableFile(filePath)) {\n items.push({ label: '✎ Edit', action: () => editFile(filePath) });\n }\n items.push({ type: 'sep' });\n }\n items.push({ label: '✎ Rename', action: () => startRename(filePath, isDir), shortcut: 'F2' });\n items.push({ type: 'sep' });\n items.push({ label: '🗑 Delete', action: () => deleteFile(filePath, isDir), cls: 'danger', shortcut: 'Del' });\n\n menu.innerHTML = items.map(it => {\n if (it.type === 'sep') return '<div class=\"ctx-sep\"></div>';\n return `<div class=\"ctx-item ${it.cls || ''}\" data-idx=\"${items.indexOf(it)}\">${it.label}${it.shortcut ? `<span class=\"shortcut\">${it.shortcut}</span>` : ''}</div>`;\n }).join('');\n\n // Attach click handlers\n menu.querySelectorAll('.ctx-item').forEach(el => {\n const idx = parseInt(el.dataset.idx);\n el.addEventListener('click', () => { hideContextMenu(); items[idx].action(); });\n });\n\n // Position (keep on screen)\n document.body.appendChild(menu);\n const rect = menu.getBoundingClientRect();\n if (x + rect.width > window.innerWidth) x = window.innerWidth - rect.width - 8;\n if (y + rect.height > window.innerHeight) y = window.innerHeight - rect.height - 8;\n menu.style.left = x + 'px';\n menu.style.top = y + 'px';\n\n // Close on click outside\n setTimeout(() => {\n document.addEventListener('click', hideContextMenu, { once: true });\n document.addEventListener('contextmenu', hideContextMenu, { once: true });\n }, 0);\n}\n\nfunction hideContextMenu() {\n const m = document.getElementById('ctxMenu');\n if (m) m.remove();\n}\n\n// ============================================================================\n// Rename (inline editing)\n// ============================================================================\n\nfunction startRename(filePath, isDir) {\n const tree = document.getElementById('fileTree');\n const row = tree.querySelector(`.tree-row[data-path=\"${CSS.escape(filePath)}\"]`);\n if (!row) return;\n\n const nameSpan = row.querySelector('.name');\n const oldName = filePath.split('/').pop();\n const origHTML = nameSpan.innerHTML;\n\n const input = document.createElement('input');\n input.type = 'text';\n input.className = 'rename-input';\n input.value = oldName;\n nameSpan.innerHTML = '';\n nameSpan.appendChild(input);\n input.focus();\n // Select name without extension for files\n if (!isDir) {\n const dotIdx = oldName.lastIndexOf('.');\n input.setSelectionRange(0, dotIdx > 0 ? dotIdx : oldName.length);\n } else {\n input.select();\n }\n\n function finish(commit) {\n const newName = input.value.trim();\n nameSpan.innerHTML = origHTML;\n if (commit && newName && newName !== oldName) {\n const parentPath = filePath.includes('/') ? filePath.substring(0, filePath.lastIndexOf('/')) : '';\n const newPath = parentPath ? parentPath + '/' + newName : newName;\n moveFile(filePath, newPath);\n }\n }\n\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') { e.preventDefault(); finish(true); }\n if (e.key === 'Escape') { e.preventDefault(); finish(false); }\n e.stopPropagation();\n });\n input.addEventListener('blur', () => finish(true));\n input.addEventListener('click', (e) => e.stopPropagation());\n}\n\n// ============================================================================\n// File Operations (move, delete, upload, create)\n// ============================================================================\n\nasync function moveFile(from, to) {\n if (from === to) return;\n try {\n const res = await fetch(`${API}/api/workspace/move`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ from, to }),\n });\n const data = await res.json();\n if (data.success) {\n addLog('info', `Moved: ${from} → ${to}`);\n refreshFiles();\n } else {\n addLog('error', `Move failed: ${data.error}`);\n }\n } catch (e) {\n addLog('error', `Move error: ${e.message}`);\n }\n}\n\nasync function deleteFile(filePath, isDir) {\n const kind = isDir ? 'directory and all its contents' : 'file';\n if (!confirm(`Delete ${kind}?\\n${filePath}`)) return;\n try {\n const res = await fetch(`${API}/api/workspace/file?path=${encodeURIComponent(filePath)}`, { method: 'DELETE' });\n const data = await res.json();\n if (data.success) {\n addLog('info', `Deleted: ${filePath}`);\n refreshFiles();\n } else {\n addLog('error', `Delete failed: ${data.error}`);\n }\n } catch (e) {\n addLog('error', `Delete error: ${e.message}`);\n }\n}\n\nasync function uploadFiles(files, targetDir = '') {\n for (const file of files) {\n const filePath = targetDir ? `${targetDir}/${file.name}` : file.name;\n addLog('info', `Uploading: ${filePath} (${formatSize(file.size)})...`);\n try {\n const content = await readFileAsBase64(file);\n const res = await fetch(`${API}/api/workspace/file`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ path: filePath, content, encoding: 'base64' }),\n });\n const data = await res.json();\n if (data.success) addLog('info', `Uploaded: ${filePath} (${formatSize(data.size)})`);\n else addLog('error', `Upload failed: ${data.error}`);\n } catch (e) {\n addLog('error', `Upload error: ${e.message}`);\n }\n }\n refreshFiles();\n}\n\nfunction readFileAsBase64(file) {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result.split(',')[1]);\n reader.onerror = reject;\n reader.readAsDataURL(file);\n });\n}\n\nfunction uploadToDir(dirPath) {\n const inp = document.createElement('input');\n inp.type = 'file';\n inp.multiple = true;\n inp.onchange = () => { if (inp.files.length) uploadFiles(inp.files, dirPath); };\n inp.click();\n}\n\nfunction handleFileSelect(event) {\n const files = event.target.files;\n if (files.length) uploadFiles(files);\n event.target.value = '';\n}\n\n// Upload area drag-drop\nconst uploadArea = document.getElementById('uploadArea');\n['dragenter', 'dragover'].forEach(ev => {\n uploadArea.addEventListener(ev, (e) => { e.preventDefault(); uploadArea.classList.add('dragover'); });\n});\n['dragleave', 'drop'].forEach(ev => {\n uploadArea.addEventListener(ev, (e) => { e.preventDefault(); uploadArea.classList.remove('dragover'); });\n});\nuploadArea.addEventListener('drop', (e) => {\n if (e.dataTransfer.files.length) uploadFiles(e.dataTransfer.files);\n});\n\n// Prompt helpers\nasync function promptNewFileInTree() { promptNewFileInDir(''); }\nasync function promptNewFolderInTree() { promptNewFolderInDir(''); }\n\nasync function promptNewFileInDir(dirPath) {\n const name = prompt(`New file name${dirPath ? ' in ' + dirPath : ''}:`);\n if (!name) return;\n const fullPath = dirPath ? dirPath + '/' + name : name;\n try {\n const res = await fetch(`${API}/api/workspace/file`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ path: fullPath, content: '' }),\n });\n const data = await res.json();\n if (data.success) {\n addLog('info', `Created: ${fullPath}`);\n if (dirPath) expandedDirs.add(dirPath);\n refreshFiles();\n } else addLog('error', data.error);\n } catch (e) { addLog('error', `Error: ${e.message}`); }\n}\n\nasync function promptNewFolderInDir(dirPath) {\n const name = prompt(`New folder name${dirPath ? ' in ' + dirPath : ''}:`);\n if (!name) return;\n const fullPath = dirPath ? dirPath + '/' + name : name;\n try {\n const res = await fetch(`${API}/api/workspace/dir`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ path: fullPath }),\n });\n const data = await res.json();\n if (data.success) {\n addLog('info', `Folder created: ${fullPath}`);\n if (dirPath) expandedDirs.add(dirPath);\n expandedDirs.add(fullPath);\n refreshFiles();\n } else addLog('error', data.error);\n } catch (e) { addLog('error', `Error: ${e.message}`); }\n}\n\n// ============================================================================\n// File Viewer Overlay\n// ============================================================================\n\n// Society Agent start - Enhanced file viewer with PDF and Markdown support\nlet currentFileContent = ''; // Store for toggle between raw/rendered\nlet currentFilePath = '';\nlet isEditing = false;\n\nconst editableExts = ['js', 'ts', 'jsx', 'tsx', 'py', 'java', 'c', 'cpp', 'h', 'go', 'rs', 'rb', 'php',\n 'swift', 'kt', 'json', 'yaml', 'yml', 'xml', 'toml', 'ini', 'html', 'css', 'scss',\n 'less', 'txt', 'log', 'csv', 'sh', 'bash', 'sql', 'env', 'md', 'markdown', 'tex', 'bib'];\n\nfunction isEditableFile(path) {\n const ext = (path.split('.').pop() || '').toLowerCase();\n return editableExts.includes(ext);\n}\n\nasync function editFile(filePath) {\n console.log('[editFile] Opening for edit:', filePath);\n \n if (!filePath || !isEditableFile(filePath)) {\n addLog('error', 'Cannot edit this file type');\n return;\n }\n \n try {\n const res = await fetch(`${API}/api/workspace/file?path=${encodeURIComponent(filePath)}`);\n const data = await res.json();\n if (!res.ok) { addLog('error', data.error); return; }\n \n currentFileContent = data.content;\n currentFilePath = filePath;\n isEditing = true;\n \n let viewer = document.getElementById('fileViewer');\n if (!viewer) {\n viewer = document.createElement('div');\n viewer.id = 'fileViewer';\n viewer.className = 'file-viewer';\n viewer.onclick = (e) => { if (e.target === viewer) closeFileViewer(); };\n document.body.appendChild(viewer);\n }\n \n const saveBtn = `<button class=\"save-btn\" onclick=\"saveFileEdit()\" id=\"saveBtn\" style=\"margin-right:8px;background:var(--accent);border:none;color:#fff;padding:4px 12px;border-radius:4px;cursor:pointer;font-size:12px;\">💾 Save</button>`;\n const cancelBtn = `<button class=\"cancel-btn\" onclick=\"closeFileViewer()\" id=\"cancelBtn\" style=\"margin-right:8px;background:var(--surface);border:1px solid var(--border);color:var(--fg);padding:4px 12px;border-radius:4px;cursor:pointer;font-size:12px;\">Cancel</button>`;\n \n viewer.innerHTML = `<div class=\"file-viewer-panel\" style=\"max-width:900px;height:80vh;\">\n <div class=\"file-viewer-header\">\n <span class=\"title\">✎ Editing: ${escapeHtml(filePath)}</span>\n <span class=\"meta\">${formatSize(data.size)}</span>\n ${saveBtn}${cancelBtn}\n <button class=\"close-btn\" onclick=\"closeFileViewer()\" title=\"Close\">×</button>\n </div>\n <div class=\"file-viewer-body\" id=\"fileViewerBody\" style=\"padding:0;\">\n <textarea id=\"editTextarea\" style=\"width:100%;height:100%;box-sizing:border-box;resize:none;border:none;font-family:'Monaco','Menlo','Consolas',monospace;font-size:13px;line-height:1.5;background:var(--surface);color:var(--fg);padding:16px;outline:none;\">${escapeHtml(data.content)}</textarea>\n </div>\n </div>`;\n viewer.style.display = 'flex';\n \n const ta = document.getElementById('editTextarea');\n ta.focus();\n ta.setSelectionRange(0, 0);\n } catch (e) {\n addLog('error', 'Failed to open file for edit: ' + e.message);\n }\n}\n\nasync function viewFile(filePath) {\n console.log('[viewFile] Opening:', filePath); // Debug log\n \n // Validation: ensure path is not empty\n if (!filePath) {\n addLog('error', 'Cannot view file: empty path');\n return;\n }\n \n try {\n const ext = filePath.split('.').pop().toLowerCase();\n const isPdf = ext === 'pdf';\n const isMd = ext === 'md' || ext === 'markdown';\n const isImage = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'].includes(ext);\n \n let viewer = document.getElementById('fileViewer');\n if (!viewer) {\n viewer = document.createElement('div');\n viewer.id = 'fileViewer';\n viewer.className = 'file-viewer';\n viewer.onclick = (e) => { if (e.target === viewer) closeFileViewer(); };\n document.body.appendChild(viewer);\n }\n\n // PDF: Use iframe (browsers have built-in PDF viewers)\n if (isPdf) {\n viewer.innerHTML = `<div class=\"file-viewer-panel pdf-viewer\">\n <div class=\"file-viewer-header\">\n <span class=\"title\">📕 ${escapeHtml(filePath)}</span>\n <a href=\"${API}/api/workspace/raw?path=${encodeURIComponent(filePath)}\" \n download=\"${escapeAttr(filePath.split('/').pop())}\" \n style=\"color:var(--accent);font-size:12px;text-decoration:none;margin-right:8px;\">⬇ Download</a>\n <button class=\"close-btn\" onclick=\"closeFileViewer()\" title=\"Close\">×</button>\n </div>\n <div class=\"file-viewer-body pdf-body\">\n <iframe src=\"${API}/api/workspace/raw?path=${encodeURIComponent(filePath)}\"></iframe>\n </div>\n </div>`;\n viewer.style.display = 'flex';\n return;\n }\n\n // Image: Display inline\n if (isImage) {\n viewer.innerHTML = `<div class=\"file-viewer-panel\">\n <div class=\"file-viewer-header\">\n <span class=\"title\">🎨 ${escapeHtml(filePath)}</span>\n <a href=\"${API}/api/workspace/raw?path=${encodeURIComponent(filePath)}\" \n download=\"${escapeAttr(filePath.split('/').pop())}\" \n style=\"color:var(--accent);font-size:12px;text-decoration:none;margin-right:8px;\">⬇ Download</a>\n <button class=\"close-btn\" onclick=\"closeFileViewer()\" title=\"Close\">×</button>\n </div>\n <div class=\"file-viewer-body\" style=\"text-align:center;\">\n <img src=\"${API}/api/workspace/raw?path=${encodeURIComponent(filePath)}\" \n style=\"max-width:100%;max-height:70vh;border-radius:8px;\" alt=\"${escapeAttr(filePath)}\">\n </div>\n </div>`;\n viewer.style.display = 'flex';\n return;\n }\n\n // Text files: Fetch content\n const res = await fetch(`${API}/api/workspace/file?path=${encodeURIComponent(filePath)}`);\n const data = await res.json();\n if (!res.ok) { addLog('error', data.error); return; }\n \n currentFileContent = data.content;\n currentFilePath = filePath;\n\n // Markdown: Show rendered with toggle to raw\n if (isMd) {\n const rendered = renderMarkdown(data.content);\n const editBtn = `<button class=\"edit-btn\" onclick=\"toggleEditMode()\" id=\"editBtn\" style=\"margin-right:8px;background:var(--surface);border:1px solid var(--border);color:var(--fg);padding:4px 12px;border-radius:4px;cursor:pointer;font-size:12px;\">✎ Edit</button>`;\n const saveBtn = `<button class=\"save-btn\" onclick=\"saveFileEdit()\" id=\"saveBtn\" style=\"display:none;margin-right:8px;background:var(--accent);border:none;color:#fff;padding:4px 12px;border-radius:4px;cursor:pointer;font-size:12px;\">💾 Save</button>`;\n const cancelBtn = `<button class=\"cancel-btn\" onclick=\"cancelEdit()\" id=\"cancelBtn\" style=\"display:none;margin-right:8px;background:var(--surface);border:1px solid var(--border);color:var(--fg);padding:4px 12px;border-radius:4px;cursor:pointer;font-size:12px;\">Cancel</button>`;\n \n viewer.innerHTML = `<div class=\"file-viewer-panel\">\n <div class=\"file-viewer-header\">\n <span class=\"title\">📝 ${escapeHtml(filePath)}</span>\n <span class=\"meta\">${formatSize(data.size)}</span>\n <div class=\"view-toggle\" id=\"viewToggle\">\n <button id=\"btnRendered\" class=\"active\" onclick=\"toggleMdView('rendered')\">Rendered</button>\n <button id=\"btnRaw\" onclick=\"toggleMdView('raw')\">Raw</button>\n </div>\n ${editBtn}${saveBtn}${cancelBtn}\n <button class=\"close-btn\" onclick=\"closeFileViewer()\" title=\"Close\">×</button>\n </div>\n <div class=\"file-viewer-body\" id=\"fileViewerBody\">\n <div class=\"markdown-body\">${rendered}</div>\n </div>\n </div>`;\n viewer.style.display = 'flex';\n return;\n }\n\n // Default: Code/text view\n const editBtn = isEditableFile(filePath) ? \n `<button class=\"edit-btn\" onclick=\"toggleEditMode()\" id=\"editBtn\" style=\"margin-right:8px;background:var(--surface);border:1px solid var(--border);color:var(--fg);padding:4px 12px;border-radius:4px;cursor:pointer;font-size:12px;\">✎ Edit</button>` : '';\n const saveBtn = `<button class=\"save-btn\" onclick=\"saveFileEdit()\" id=\"saveBtn\" style=\"display:none;margin-right:8px;background:var(--accent);border:none;color:#fff;padding:4px 12px;border-radius:4px;cursor:pointer;font-size:12px;\">💾 Save</button>`;\n const cancelBtn = `<button class=\"cancel-btn\" onclick=\"cancelEdit()\" id=\"cancelBtn\" style=\"display:none;margin-right:8px;background:var(--surface);border:1px solid var(--border);color:var(--fg);padding:4px 12px;border-radius:4px;cursor:pointer;font-size:12px;\">Cancel</button>`;\n \n viewer.innerHTML = `<div class=\"file-viewer-panel\">\n <div class=\"file-viewer-header\">\n <span class=\"title\">📄 ${escapeHtml(filePath)}</span>\n <span class=\"meta\">${formatSize(data.size)}</span>\n ${editBtn}${saveBtn}${cancelBtn}\n <button class=\"close-btn\" onclick=\"closeFileViewer()\" title=\"Close\">×</button>\n </div>\n <div class=\"file-viewer-body\" id=\"fileViewerBody\"><pre>${escapeHtml(data.content)}</pre></div>\n </div>`;\n viewer.style.display = 'flex';\n } catch (e) { addLog('error', 'Failed to read file: ' + e.message); }\n}\n\nfunction toggleEditMode() {\n isEditing = true;\n const body = document.getElementById('fileViewerBody');\n const editBtn = document.getElementById('editBtn');\n const saveBtn = document.getElementById('saveBtn');\n const cancelBtn = document.getElementById('cancelBtn');\n const viewToggle = document.getElementById('viewToggle');\n \n if (editBtn) editBtn.style.display = 'none';\n if (saveBtn) saveBtn.style.display = 'inline-block';\n if (cancelBtn) cancelBtn.style.display = 'inline-block';\n if (viewToggle) viewToggle.style.display = 'none';\n \n body.innerHTML = `<textarea id=\"editTextarea\" style=\"width:100%;height:100%;box-sizing:border-box;resize:none;border:none;font-family:'Monaco','Menlo','Consolas',monospace;font-size:13px;line-height:1.5;background:var(--surface);color:var(--fg);padding:16px;outline:none;\">${escapeHtml(currentFileContent)}</textarea>`;\n const ta = document.getElementById('editTextarea');\n ta.focus();\n ta.setSelectionRange(0, 0);\n}\n\nfunction cancelEdit() {\n isEditing = false;\n const body = document.getElementById('fileViewerBody');\n const editBtn = document.getElementById('editBtn');\n const saveBtn = document.getElementById('saveBtn');\n const cancelBtn = document.getElementById('cancelBtn');\n const viewToggle = document.getElementById('viewToggle');\n const isMd = currentFilePath && ['md', 'markdown'].includes(currentFilePath.split('.').pop().toLowerCase());\n \n if (editBtn) editBtn.style.display = 'inline-block';\n if (saveBtn) saveBtn.style.display = 'none';\n if (cancelBtn) cancelBtn.style.display = 'none';\n if (viewToggle) viewToggle.style.display = 'flex';\n \n if (isMd) {\n body.innerHTML = `<div class=\"markdown-body\">${renderMarkdown(currentFileContent)}</div>`;\n } else {\n body.innerHTML = `<pre>${escapeHtml(currentFileContent)}</pre>`;\n }\n}\n\nasync function saveFileEdit() {\n const ta = document.getElementById('editTextarea');\n if (!ta || !currentFilePath) return;\n \n const content = ta.value;\n const saveBtn = document.getElementById('saveBtn');\n if (saveBtn) {\n saveBtn.disabled = true;\n saveBtn.innerHTML = 'Saving...';\n }\n \n try {\n const res = await fetch(`${API}/api/workspace/file`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ path: currentFilePath, content })\n });\n \n if (!res.ok) {\n const err = await res.json();\n throw new Error(err.error || 'Failed to save');\n }\n \n currentFileContent = content;\n isEditing = false;\n \n const body = document.getElementById('fileViewerBody');\n const editBtn = document.getElementById('editBtn');\n const cancelBtn = document.getElementById('cancelBtn');\n const viewToggle = document.getElementById('viewToggle');\n const isMd = currentFilePath && ['md', 'markdown'].includes(currentFilePath.split('.').pop().toLowerCase());\n \n if (editBtn) editBtn.style.display = 'inline-block';\n if (saveBtn) saveBtn.style.display = 'none';\n if (cancelBtn) cancelBtn.style.display = 'none';\n if (viewToggle) viewToggle.style.display = 'flex';\n \n if (isMd) {\n body.innerHTML = `<div class=\"markdown-body\">${renderMarkdown(content)}</div>`;\n } else {\n body.innerHTML = `<pre>${escapeHtml(content)}</pre>`;\n }\n \n addLog('info', `Saved: ${currentFilePath}`);\n refreshFiles();\n } catch (e) {\n alert('Error saving file: ' + e.message);\n } finally {\n if (saveBtn) {\n saveBtn.disabled = false;\n saveBtn.innerHTML = '💾 Save';\n }\n }\n}\n\nfunction toggleMdView(mode) {\n const body = document.getElementById('fileViewerBody');\n const btnRendered = document.getElementById('btnRendered');\n const btnRaw = document.getElementById('btnRaw');\n if (!body) return;\n \n if (mode === 'raw') {\n body.innerHTML = `<pre>${escapeHtml(currentFileContent)}</pre>`;\n btnRendered.classList.remove('active');\n btnRaw.classList.add('active');\n } else {\n body.innerHTML = `<div class=\"markdown-body\">${renderMarkdown(currentFileContent)}</div>`;\n btnRendered.classList.add('active');\n btnRaw.classList.remove('active');\n }\n}\n\n// Simple markdown renderer (no external dependencies)\nfunction renderMarkdown(text) {\n if (!text) return '';\n let html = escapeHtml(text);\n \n // Code blocks (``` ... ```)\n html = html.replace(/```(\\w*)\\n([\\s\\S]*?)```/g, '<pre><code class=\"lang-$1\">$2</code></pre>');\n \n // Inline code (`code`)\n html = html.replace(/`([^`]+)`/g, '<code>$1</code>');\n \n // Headers (# to ######)\n html = html.replace(/^###### (.+)$/gm, '<h6>$1</h6>');\n html = html.replace(/^##### (.+)$/gm, '<h5>$1</h5>');\n html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');\n html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');\n html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');\n html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');\n \n // Bold (**text** or __text__)\n html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');\n html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');\n \n // Italic (*text* or _text_)\n html = html.replace(/\\*([^*]+)\\*/g, '<em>$1</em>');\n html = html.replace(/_([^_]+)_/g, '<em>$1</em>');\n \n // Strikethrough (~~text~~)\n html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');\n \n // Blockquotes (> text)\n html = html.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>');\n \n // Horizontal rules (---, ***, ___)\n html = html.replace(/^(---+|\\*\\*\\*+|___+)$/gm, '<hr>');\n \n // Links [text](url)\n html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\" target=\"_blank\">$1</a>');\n \n // Images \n html = html.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, '<img src=\"$2\" alt=\"$1\" style=\"max-width:100%;\">');\n \n // Unordered lists (- item or * item)\n html = html.replace(/^[\\-\\*] (.+)$/gm, '<li>$1</li>');\n html = html.replace(/(<li>.*<\\/li>\\n)+/g, '<ul>$&</ul>');\n \n // Ordered lists (1. item)\n html = html.replace(/^\\d+\\. (.+)$/gm, '<li>$1</li>');\n \n // Tables (basic support)\n html = html.replace(/^\\|(.+)\\|$/gm, (match, content) => {\n const cells = content.split('|').map(c => c.trim());\n if (cells.every(c => /^[-:]+$/.test(c))) return ''; // separator row\n const isHeader = content.includes('---');\n const tag = isHeader ? 'th' : 'td';\n return '<tr>' + cells.map(c => `<${tag}>${c}</${tag}>`).join('') + '</tr>';\n });\n html = html.replace(/(<tr>.*<\\/tr>\\n?)+/g, '<table>$&</table>');\n \n // Paragraphs (double newlines)\n html = html.replace(/\\n\\n+/g, '</p><p>');\n html = '<p>' + html + '</p>';\n \n // Clean up empty paragraphs and fix nesting\n html = html.replace(/<p><\\/p>/g, '');\n html = html.replace(/<p>(<h[1-6]>)/g, '$1');\n html = html.replace(/(<\\/h[1-6]>)<\\/p>/g, '$1');\n html = html.replace(/<p>(<pre>)/g, '$1');\n html = html.replace(/(<\\/pre>)<\\/p>/g, '$1');\n html = html.replace(/<p>(<ul>)/g, '$1');\n html = html.replace(/(<\\/ul>)<\\/p>/g, '$1');\n html = html.replace(/<p>(<table>)/g, '$1');\n html = html.replace(/(<\\/table>)<\\/p>/g, '$1');\n html = html.replace(/<p>(<blockquote>)/g, '$1');\n html = html.replace(/(<\\/blockquote>)<\\/p>/g, '$1');\n html = html.replace(/<p>(<hr>)/g, '$1');\n html = html.replace(/(<hr>)<\\/p>/g, '$1');\n \n return html;\n}\n// Society Agent end\n\nfunction closeFileViewer() {\n const v = document.getElementById('fileViewer');\n if (v) v.style.display = 'none';\n}\n\nfunction formatSize(bytes) {\n if (!bytes || bytes === 0) return '0 B';\n const k = 1024;\n const sizes = ['B', 'KB', 'MB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];\n}\n\nfunction escapeAttr(s) {\n return s.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\").replace(/\"/g, '"');\n}\n\n// ============================================================================\n// Sidebar Resize Handle\n// ============================================================================\n\n(function initSidebarResize() {\n const sidebar = document.getElementById('sidebar');\n const handle = document.getElementById('sidebarResize');\n let startX, startWidth;\n\n handle.addEventListener('mousedown', (e) => {\n e.preventDefault();\n startX = e.clientX;\n startWidth = sidebar.offsetWidth;\n handle.classList.add('active');\n document.addEventListener('mousemove', onDrag);\n document.addEventListener('mouseup', onUp);\n });\n\n function onDrag(e) {\n const w = startWidth + (e.clientX - startX);\n sidebar.style.width = Math.max(180, Math.min(600, w)) + 'px';\n }\n\n function onUp() {\n handle.classList.remove('active');\n document.removeEventListener('mousemove', onDrag);\n document.removeEventListener('mouseup', onUp);\n // Persist sidebar width\n try { localStorage.setItem('sidebar-width', sidebar.style.width); } catch {}\n }\n\n // Restore saved width\n try {\n const saved = localStorage.getItem('sidebar-width');\n if (saved) sidebar.style.width = saved;\n } catch {}\n})();\n\n// Listen for file-moved events from server\nsocket.on('file-moved', () => refreshFiles());\n\n// ============================================================================\n// UI Helpers\n// ============================================================================\n\n// ── 16.4/16.5 Debug toggle ──────────────────────────────────────────────────\nlet debugMode = false;\nfunction toggleDebug() {\n debugMode = !debugMode;\n const msgs = document.getElementById('messages');\n msgs.classList.toggle('debug-on', debugMode);\n const btn = document.getElementById('debugBtn');\n if (btn) { btn.classList.toggle('active', debugMode); btn.title = debugMode ? 'Hide debug info' : 'Show thinking blocks & debug info'; }\n // Open all trace drawers when debug is on, close when off\n msgs.querySelectorAll('.trace-drawer').forEach(d => { d.open = debugMode; });\n // Show/hide context stats bar\n const bar = document.getElementById('ctxStatsBar');\n if (bar) bar.classList.toggle('visible', debugMode);\n}\n\n// ── 16.3 Message render cap ──────────────────────────────────────────────────\nfunction enforceMessageCap(container, max = 200) {\n const items = container.querySelectorAll('.message:not(.msg-cap-banner), .agent-turn');\n if (items.length <= max) return;\n const toHide = items.length - max;\n let hidden = 0;\n for (const item of items) {\n if (hidden >= toHide) break;\n if (!item.classList.contains('capped')) { item.style.display = 'none'; item.classList.add('capped'); hidden++; }\n }\n let banner = container.querySelector('.msg-cap-banner');\n if (!banner) {\n banner = document.createElement('div');\n banner.className = 'msg-cap-banner';\n banner.onclick = () => {\n container.querySelectorAll('.capped').forEach(el => { el.style.display = ''; el.classList.remove('capped'); });\n banner.remove();\n };\n container.prepend(banner);\n }\n banner.textContent = `↑ ${container.querySelectorAll('.capped').length} older messages hidden — click to show`;\n}\n\nfunction addMessage(role, content, sender) {\n const container = document.getElementById('messages');\n\n // ── 16.3 Consecutive same-role system grouping ────────────────────────────\n if (role === 'system') {\n const last = container.lastElementChild;\n if (last && last.classList.contains('message') && last.classList.contains('system')) {\n const c = last.querySelector('.content');\n if (c) {\n const div = document.createElement('div');\n div.className = 'sys-item';\n div.innerHTML = content; // server-controlled HTML\n c.appendChild(div);\n scrollToBottom();\n return last;\n }\n }\n }\n\n const el = document.createElement('div');\n el.className = `message ${role}`;\n if (role === 'assistant') {\n // Escape sender to prevent XSS (CodeQL js/xss-through-dom)\n el.innerHTML = `<div class=\"sender\">${escapeHtml(sender || 'Agent')}</div><div class=\"content\">${formatMarkdown(content)}</div>`;\n } else if (role === 'user') {\n el.innerHTML = `<div class=\"content\">${escapeHtml(content)}</div>`;\n } else {\n // system: server-controlled HTML (callers already escape user data within it)\n el.innerHTML = `<div class=\"content\">${content}</div>`;\n }\n container.appendChild(el);\n enforceMessageCap(container);\n scrollToBottom();\n return el;\n}\n\nfunction showTyping() {\n const el = document.createElement('div');\n el.className = 'typing';\n el.id = 'typingIndicator';\n el.textContent = 'Agent is thinking';\n document.getElementById('messages').appendChild(el);\n scrollToBottom();\n}\n\nfunction removeTyping() {\n const el = document.getElementById('typingIndicator');\n if (el) el.remove();\n}\n\nfunction scrollToBottom(force = false) {\n const m = document.getElementById('messages');\n const isNearBottom = m.scrollHeight - m.scrollTop - m.clientHeight < 150;\n if (force || isNearBottom) {\n m.scrollTop = m.scrollHeight;\n }\n}\n\n// Show/hide scroll-to-bottom button based on scroll position\n(function setupScrollButton() {\n const messages = document.getElementById('messages');\n const scrollBtn = document.getElementById('scrollBottomBtn');\n if (!messages || !scrollBtn) return;\n \n messages.addEventListener('scroll', () => {\n const isNearBottom = messages.scrollHeight - messages.scrollTop - messages.clientHeight < 150;\n scrollBtn.style.display = isNearBottom ? 'none' : 'block';\n });\n})();\n\n// Society Agent start - Enhanced status with reconnection message\nfunction setStatus(online, statusMsg) {\n const dot = document.getElementById('statusDot');\n const text = document.getElementById('statusText');\n const btn = document.getElementById('reconnectBtn');\n dot.className = `dot ${online ? 'online' : 'offline'}`;\n if (statusMsg) {\n text.textContent = statusMsg;\n } else {\n text.textContent = online ? 'Connected' : 'Disconnected';\n }\n // Show reconnect button when disconnected\n btn.style.display = online ? 'none' : 'inline-block';\n}\n\nfunction manualReconnect() {\n addLog('info', 'Manual reconnect requested...');\n setStatus(false, 'Reconnecting...');\n socket.disconnect();\n setTimeout(() => socket.connect(), 500);\n}\n\n// Society Agent - Clear all state and reload page\nfunction clearAndReset() {\n if (!confirm('This will clear all local state and reload the page. Continue?')) return;\n \n // Clear localStorage\n try { localStorage.clear(); } catch {}\n \n // Clear sessionStorage\n try { sessionStorage.clear(); } catch {}\n \n // Disconnect socket cleanly\n try { socket.disconnect(); } catch {}\n \n // Clear any IndexedDB (if used)\n try {\n if (indexedDB && indexedDB.databases) {\n indexedDB.databases().then(dbs => {\n dbs.forEach(db => indexedDB.deleteDatabase(db.name));\n });\n }\n } catch {}\n \n // Force hard reload bypassing cache\n window.location.reload(true);\n}\n// Society Agent end\n\nfunction addLog(level, text) {\n const el = document.createElement('div');\n el.className = `log-entry ${level}`;\n const ts = new Date().toLocaleTimeString();\n el.innerHTML = `<span class=\"ts\">${ts}</span>${escapeHtml(text)}`;\n const panel = document.getElementById('logPanel');\n panel.appendChild(el);\n panel.scrollTop = panel.scrollHeight;\n}\n\n// ============================================================================\n// Settings Modal (Provider/Model Configuration)\n// ============================================================================\n\n// Models available per provider with cost info ($/1M tokens)\nconst MODELS_BY_PROVIDER = {\n anthropic: [\n { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4 (Latest)', input: 3, output: 15 },\n { id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', input: 3, output: 15 },\n { id: 'claude-3-opus-20240229', name: 'Claude 3 Opus', input: 15, output: 75 },\n { id: 'claude-3-haiku-20240307', name: 'Claude 3 Haiku (Fast)', input: 0.25, output: 1.25 },\n ],\n openrouter: [\n { id: 'anthropic/claude-sonnet-4', name: 'Claude Sonnet 4', input: 3, output: 15 },\n { id: 'anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet', input: 3, output: 15 },\n { id: 'anthropic/claude-3-opus', name: 'Claude 3 Opus', input: 15, output: 75 },\n { id: 'anthropic/claude-3-haiku', name: 'Claude 3 Haiku', input: 0.25, output: 1.25 },\n { id: 'openai/gpt-4o', name: 'GPT-4o', input: 2.5, output: 10 },\n { id: 'openai/gpt-4o-mini', name: 'GPT-4o Mini (Fast)', input: 0.15, output: 0.60 },\n { id: 'google/gemini-2.0-flash-001', name: 'Gemini 2.0 Flash', input: 0.10, output: 0.40 },\n { id: 'google/gemini-pro', name: 'Gemini Pro', input: 0.125, output: 0.375 },\n { id: 'meta-llama/llama-3.1-70b-instruct', name: 'Llama 3.1 70B', input: 0.52, output: 0.75 },\n { id: 'meta-llama/llama-3.1-8b-instruct', name: 'Llama 3.1 8B (Fast)', input: 0.055, output: 0.055 },\n { id: 'deepseek/deepseek-chat', name: 'DeepSeek Chat', input: 0.14, output: 0.28 },\n { id: 'mistralai/mistral-large', name: 'Mistral Large', input: 2, output: 6 },\n { id: 'qwen/qwen-2.5-72b-instruct', name: 'Qwen 2.5 72B', input: 0.35, output: 0.40 },\n { id: 'minimax/minimax-m2.5', name: 'MiniMax M2.5', input: 0.30, output: 1.10 },\n { id: 'minimax/minimax-m2.5:free', name: 'MiniMax M2.5 (Free)', input: 0, output: 0 },\n { id: 'minimax/minimax-m2.7', name: 'MiniMax M2.7', input: 0.60, output: 2.40 },\n { id: 'kwaipilot/kat-coder-pro-v2', name: 'KAT-Coder-Pro V2', input: 0.40, output: 1.60 },\n { id: 'qwen/qwen3-coder-480b-a35b', name: 'Qwen3 Coder 480B A35B', input: 0.80, output: 3.20 },\n { id: 'qwen/qwen3-coder-480b-a35b:free', name: 'Qwen3 Coder 480B A35B (Free)', input: 0, output: 0 },\n { id: 'nvidia/nemotron-3-super', name: 'NVIDIA Nemotron 3 Super', input: 0.50, output: 2.00 },\n { id: 'nvidia/nemotron-3-super:free', name: 'NVIDIA Nemotron 3 Super (Free)', input: 0, output: 0 },\n ],\n openai: [\n { id: 'gpt-4o', name: 'GPT-4o', input: 2.5, output: 10 },\n { id: 'gpt-4o-mini', name: 'GPT-4o Mini (Fast)', input: 0.15, output: 0.60 },\n { id: 'gpt-4-turbo', name: 'GPT-4 Turbo', input: 10, output: 30 },\n { id: 'o1-preview', name: 'o1 Preview (Reasoning)', input: 15, output: 60 },\n { id: 'o1-mini', name: 'o1 Mini', input: 3, output: 12 },\n ],\n minimax: [\n { id: 'minimax/minimax-m2.5', name: 'MiniMax M2.5', input: 0.30, output: 1.10 },\n { id: 'minimax/minimax-m2.7', name: 'MiniMax M2.7', input: 0.60, output: 2.40 },\n ],\n ollama: [\n { id: 'qwen2.5-coder:7b', name: 'Qwen 2.5 Coder 7B (Local)', input: 0, output: 0 },\n { id: 'llama3.1:8b', name: 'Llama 3.1 8B (Local)', input: 0, output: 0 },\n { id: 'deepseek-coder-v2:16b', name: 'DeepSeek Coder V2 16B (Local)', input: 0, output: 0 },\n { id: 'gemma3:12b', name: 'Gemma 3 12B (Local)', input: 0, output: 0 },\n { id: 'gemma3:27b', name: 'Gemma 3 27B (Local)', input: 0, output: 0 },\n ],\n gemini: [\n { id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro', input: 1.25, output: 5 },\n { id: 'gemini-1.5-flash', name: 'Gemini 1.5 Flash (Fast)', input: 0.075, output: 0.30 },\n { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', input: 0.10, output: 0.40 },\n ],\n deepseek: [\n { id: 'deepseek-chat', name: 'DeepSeek Chat', input: 0.14, output: 0.28 },\n { id: 'deepseek-coder', name: 'DeepSeek Coder', input: 0.14, output: 0.28 },\n ],\n groq: [\n { id: 'llama-3.1-70b-versatile', name: 'Llama 3.1 70B', input: 0.59, output: 0.79 },\n { id: 'llama-3.1-8b-instant', name: 'Llama 3.1 8B (Fast)', input: 0.05, output: 0.08 },\n { id: 'mixtral-8x7b-32768', name: 'Mixtral 8x7B', input: 0.24, output: 0.24 },\n ],\n mistral: [\n { id: 'mistral-large-latest', name: 'Mistral Large', input: 2, output: 6 },\n { id: 'mistral-medium-latest', name: 'Mistral Medium', input: 2.7, output: 8.1 },\n { id: 'mistral-small-latest', name: 'Mistral Small', input: 0.2, output: 0.6 },\n ],\n};\n\nconst API_KEY_LABELS = {\n anthropic: 'Anthropic API Key',\n openrouter: 'OpenRouter API Key',\n openai: 'OpenAI API Key',\n minimax: 'MiniMax API Key',\n ollama: 'Local Model (no API key required)',\n gemini: 'Google AI API Key',\n deepseek: 'DeepSeek API Key',\n groq: 'Groq API Key',\n mistral: 'Mistral API Key',\n};\n\nconst API_KEY_PLACEHOLDERS = {\n anthropic: 'sk-ant-api03-...',\n openrouter: 'sk-or-v1-...',\n openai: 'sk-...',\n minimax: 'minimax-...',\n ollama: 'Not required for local Ollama',\n gemini: 'AIza...',\n deepseek: 'sk-...',\n groq: 'gsk_...',\n mistral: '...',\n};\n\nlet currentSettings = null;\n\nfunction getModelsForProvider(provider) {\n if (provider === 'ollama' && currentSettings?.ollamaModelsFromApi) {\n const live = Array.isArray(currentSettings?.ollamaModels) ? currentSettings.ollamaModels : [];\n return live.map(id => ({ id, name: id, input: 0, output: 0 }));\n }\n return MODELS_BY_PROVIDER[provider] || [];\n}\n\nasync function showApiKeyModal() {\n // Load current settings from server\n try {\n const res = await fetch(`${API}/api/config/provider`);\n const data = await res.json();\n currentSettings = data;\n \n // Set provider dropdown\n const providerSelect = document.getElementById('settingsProvider');\n providerSelect.value = data.provider || 'anthropic';\n \n // Populate model dropdown\n onProviderChange();\n \n // Set current model\n if (data.model) {\n const modelSelect = document.getElementById('settingsModel');\n modelSelect.value = data.model;\n if (modelSelect.value !== data.model && modelSelect.options.length > 0) {\n modelSelect.selectedIndex = 0;\n }\n updateModelCost();\n }\n \n // Show API key status\n const statusEl = document.getElementById('settingsApiKeyStatus');\n if (data.apiKeyConfigured) {\n statusEl.textContent = '✓ API key configured';\n statusEl.style.color = 'var(--success)';\n document.getElementById('settingsApiKey').placeholder = '••••••••••••••••';\n } else {\n statusEl.textContent = '✗ No API key configured';\n statusEl.style.color = 'var(--error)';\n }\n \n // Load advanced settings\n const fullSettings = await fetch(`${API}/api/settings`).then(r => r.json()).catch(() => ({}));\n if (fullSettings.defaultMaxTokens) {\n document.getElementById('settingsMaxTokens').value = fullSettings.defaultMaxTokens;\n }\n if (fullSettings.defaultTemperature !== undefined) {\n document.getElementById('settingsTemperature').value = fullSettings.defaultTemperature;\n }\n if (fullSettings.operatingProfile) {\n document.getElementById('settingsOperatingProfile').value = fullSettings.operatingProfile;\n }\n if (fullSettings.delegationStyle) {\n document.getElementById('settingsDelegationStyle').value = fullSettings.delegationStyle;\n }\n if (fullSettings.verificationStrictness) {\n document.getElementById('settingsVerificationStrictness').value = fullSettings.verificationStrictness;\n }\n } catch (e) {\n console.error('Failed to load settings:', e);\n }\n \n document.getElementById('apiKeyModal').style.display = 'flex';\n}\n\nfunction hideApiKeyModal() { \n document.getElementById('apiKeyModal').style.display = 'none'; \n}\n\nasync function onProviderChange() {\n const provider = document.getElementById('settingsProvider').value;\n\n // Refresh live Ollama model list on demand so UI doesn't stay on stale fallback data.\n if (provider === 'ollama') {\n try {\n const res = await fetch(`${API}/api/config/provider`);\n const data = await res.json();\n currentSettings = { ...(currentSettings || {}), ...data };\n } catch (e) {\n console.error('Failed to refresh Ollama models:', e);\n }\n }\n\n renderModelOptions(provider, '');\n const searchEl = document.getElementById('settingsModelSearch');\n if (searchEl) searchEl.value = '';\n\n const apiKeyInput = document.getElementById('settingsApiKey');\n const apiKeyStatus = document.getElementById('settingsApiKeyStatus');\n const apiKeyNotRequired = provider === 'ollama';\n \n // Update API key label and placeholder\n document.getElementById('settingsApiKeyLabel').textContent = API_KEY_LABELS[provider] || 'API Key';\n apiKeyInput.placeholder = API_KEY_PLACEHOLDERS[provider] || 'sk-...';\n apiKeyInput.disabled = apiKeyNotRequired;\n apiKeyInput.style.opacity = apiKeyNotRequired ? '0.6' : '1';\n \n // Clear API key field (different provider needs different key)\n apiKeyInput.value = '';\n if (provider === 'ollama' && currentSettings?.ollamaModelsFromApi && Array.isArray(currentSettings?.ollamaModels) && currentSettings.ollamaModels.length === 0) {\n apiKeyStatus.textContent = `Ollama is reachable at ${currentSettings.ollamaBaseUrl || 'configured URL'}, but no models are installed.`;\n apiKeyStatus.style.color = 'var(--muted)';\n } else if (provider === 'ollama' && currentSettings?.ollamaModelsError) {\n apiKeyStatus.textContent = `Could not query Ollama models at ${currentSettings.ollamaBaseUrl || 'configured URL'}: ${currentSettings.ollamaModelsError}`;\n apiKeyStatus.style.color = 'var(--error)';\n } else {\n apiKeyStatus.textContent = apiKeyNotRequired ? 'No API key required for local Ollama' : 'Enter API key for ' + provider;\n apiKeyStatus.style.color = 'var(--muted)';\n }\n \n updateModelCost();\n}\n\nfunction renderModelOptions(provider, query = '') {\n const modelSelect = document.getElementById('settingsModel');\n const models = getModelsForProvider(provider);\n const q = (query || '').trim().toLowerCase();\n\n const filtered = !q\n ? models\n : models.filter(m => m.name.toLowerCase().includes(q) || m.id.toLowerCase().includes(q));\n\n modelSelect.innerHTML = filtered.map(m =>\n `<option value=\"${m.id}\">${m.name}</option>`\n ).join('');\n\n if (filtered.length === 0) {\n modelSelect.innerHTML = '<option value=\"\">No models match filter</option>';\n }\n\n // Keep cost area useful for discovery\n const costEl = document.getElementById('settingsModelCost');\n if (costEl) {\n const total = models.length;\n const shown = filtered.length;\n if (provider === 'ollama') {\n if (currentSettings?.ollamaModelsFromApi && Array.isArray(currentSettings?.ollamaModels) && currentSettings.ollamaModels.length > 0) {\n costEl.textContent = `Live Ollama models (${shown}/${total}) from ${currentSettings.ollamaBaseUrl || 'configured URL'}`;\n } else if (currentSettings?.ollamaModelsFromApi) {\n costEl.textContent = `Live Ollama API reachable at ${currentSettings.ollamaBaseUrl || 'configured URL'}, but no models installed.`;\n } else {\n costEl.textContent = `Fallback model list (${shown}/${total})`;\n }\n } else if (shown !== total) {\n costEl.textContent = `Showing ${shown}/${total} models`;\n }\n }\n}\n\nfunction updateModelCost() {\n const provider = document.getElementById('settingsProvider').value;\n const modelId = document.getElementById('settingsModel').value;\n const models = getModelsForProvider(provider);\n const model = models.find(m => m.id === modelId);\n \n const costEl = document.getElementById('settingsModelCost');\n if (model) {\n costEl.textContent = `Cost: $${model.input}/1M input, $${model.output}/1M output`;\n } else {\n costEl.textContent = '';\n }\n}\n\n// Add change handler for model dropdown\ndocument.addEventListener('DOMContentLoaded', () => {\n const modelSelect = document.getElementById('settingsModel');\n if (modelSelect) {\n modelSelect.addEventListener('change', updateModelCost);\n }\n\n const modelSearch = document.getElementById('settingsModelSearch');\n if (modelSearch) {\n modelSearch.addEventListener('input', () => {\n const provider = document.getElementById('settingsProvider').value;\n renderModelOptions(provider, modelSearch.value || '');\n });\n }\n\n // Society Agent start - Block browser shortcuts when terminal tab is active.\n // window capture fires BEFORE the browser acts on Ctrl+W/T/N etc.\n // NOTE: Ctrl+W cannot be blocked in Chrome/Firefox (browser security restriction).\n // Users should use Ctrl+F instead (which CAN be blocked) for nano search.\n const TERM_BLOCKED = new Set(['f','t','n','k','l','u','h','j']);\n window.addEventListener('keydown', (e) => {\n if (!e.ctrlKey || e.altKey || e.metaKey) return;\n if (!TERM_BLOCKED.has(e.key.toLowerCase())) return;\n const pane = document.getElementById('paneTerminal');\n if (pane && pane.classList.contains('active')) {\n e.preventDefault();\n }\n }, true);\n // Society Agent end\n});\n\nasync function saveSettings() {\n const provider = document.getElementById('settingsProvider').value;\n const model = document.getElementById('settingsModel').value;\n const apiKey = document.getElementById('settingsApiKey').value.trim();\n const maxTokens = parseInt(document.getElementById('settingsMaxTokens').value) || 16384;\n const temperature = parseFloat(document.getElementById('settingsTemperature').value) || 0.7;\n const operatingProfile = document.getElementById('settingsOperatingProfile').value || 'balanced';\n const delegationStyle = document.getElementById('settingsDelegationStyle').value || 'team_mode';\n const verificationStrictness = document.getElementById('settingsVerificationStrictness').value || 'standard';\n \n // Validate\n if (provider !== 'ollama' && !apiKey && !currentSettings?.apiKeyConfigured) {\n alert('Please enter an API key');\n return;\n }\n \n try {\n // Save provider config (with or without new API key)\n const providerPayload = {\n apiProvider: provider,\n apiModelId: model,\n maxTokens,\n temperature,\n };\n if (provider === 'ollama') {\n providerPayload.ollamaModelId = model;\n }\n \n // Add appropriate API key field based on provider\n if (apiKey) {\n if (provider === 'anthropic') providerPayload.anthropicApiKey = apiKey;\n else if (provider === 'openrouter') providerPayload.openRouterApiKey = apiKey;\n else if (provider === 'openai') providerPayload.openAiApiKey = apiKey;\n else if (provider === 'minimax') providerPayload.minimaxApiKey = apiKey;\n else if (provider === 'gemini') providerPayload.geminiApiKey = apiKey;\n else providerPayload.apiKey = apiKey;\n }\n \n const res = await fetch(`${API}/api/config/provider`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(providerPayload),\n });\n \n const data = await res.json();\n \n if (data.success) {\n // Persist standalone server behavior settings\n await fetch(`${API}/api/settings`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n defaultMaxTokens: maxTokens,\n defaultTemperature: temperature,\n operatingProfile,\n delegationStyle,\n verificationStrictness,\n }),\n });\n\n hideApiKeyModal();\n addLog('info', `Settings saved: ${provider} / ${model} (${operatingProfile}, ${delegationStyle}, ${verificationStrictness})`);\n \n // Update status bar to show new provider/model\n updateProviderStatus(provider, model);\n \n // Reload to pick up new settings\n checkApiKey();\n } else {\n alert(data.error || 'Failed to save settings');\n }\n } catch (e) {\n console.error('Save settings error:', e);\n alert('Network error saving settings');\n }\n}\n\n// Update the status bar to show current provider/model\nfunction updateProviderStatus(provider, model) {\n // Add or update provider badge in header\n let badge = document.getElementById('providerBadge');\n if (!badge) {\n badge = document.createElement('span');\n badge.id = 'providerBadge';\n badge.style.cssText = 'font-size:11px;color:var(--muted);margin-left:8px;background:var(--bg-lighter);padding:2px 6px;border-radius:4px;';\n const settingsBtn = document.querySelector('button[onclick=\"showApiKeyModal()\"]');\n if (settingsBtn) settingsBtn.parentNode.insertBefore(badge, settingsBtn);\n }\n \n // Format model name nicely\n const shortModel = model.split('/').pop().replace(/-\\d{8}$/, '');\n badge.textContent = `${provider}: ${shortModel}`;\n}\n\n// Load initial provider status\nasync function loadProviderStatus() {\n try {\n const res = await fetch(`${API}/api/config/provider`);\n const data = await res.json();\n if (data.provider && data.model) {\n updateProviderStatus(data.provider, data.model);\n }\n } catch (e) {\n console.error('Failed to load provider status:', e);\n }\n}\n\n// Call on page load\ndocument.addEventListener('DOMContentLoaded', loadProviderStatus);\n\n// Legacy saveApiKey function (for backward compatibility)\nasync function saveApiKey() {\n return saveSettings();\n}\n\nfunction escapeHtml(text) {\n const d = document.createElement('div');\n d.textContent = text;\n return d.innerHTML;\n}\n\nfunction formatMarkdown(text) {\n if (!text) return '';\n let html = escapeHtml(text);\n // Code blocks\n html = html.replace(/```(\\w*)\\n([\\s\\S]*?)```/g, '<pre><code>$2</code></pre>');\n // Inline code\n html = html.replace(/`([^`]+)`/g, '<code>$1</code>');\n // Bold\n html = html.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');\n // Italic\n html = html.replace(/\\*([^*]+)\\*/g, '<em>$1</em>');\n // Line breaks\n html = html.replace(/\\n/g, '<br>');\n return html;\n}\n\n// ============================================================================\n// Chat Attachments\n// ============================================================================\n\nfunction handleChatAttach(event) {\n const files = event.target.files;\n for (const file of files) {\n if (file.type.startsWith('image/')) {\n // Resize images to avoid token limit (max 1024px on longest side)\n // resizeImage converts to JPEG, so mediaType must be image/jpeg\n resizeImage(file, 1024).then(({ dataUrl, base64 }) => {\n chatAttachments.push({\n name: file.name,\n type: file.type,\n dataUrl,\n base64,\n mediaType: 'image/jpeg',\n });\n renderAttachChips();\n });\n } else {\n const reader = new FileReader();\n reader.onload = () => {\n const dataUrl = reader.result;\n const base64 = dataUrl.split(',')[1];\n chatAttachments.push({\n name: file.name,\n type: file.type || 'application/octet-stream',\n dataUrl,\n base64,\n mediaType: file.type || 'application/octet-stream',\n });\n renderAttachChips();\n };\n reader.readAsDataURL(file);\n }\n }\n event.target.value = '';\n}\n\n// Resize image to fit within maxPx on the longest side, returns JPEG base64\nfunction resizeImage(file, maxPx) {\n return new Promise((resolve) => {\n const img = new Image();\n img.onload = () => {\n let { width, height } = img;\n if (width > maxPx || height > maxPx) {\n const scale = maxPx / Math.max(width, height);\n width = Math.round(width * scale);\n height = Math.round(height * scale);\n }\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n const ctx = canvas.getContext('2d');\n ctx.drawImage(img, 0, 0, width, height);\n // Use JPEG at 85% quality for smaller size\n const dataUrl = canvas.toDataURL('image/jpeg', 0.85);\n const base64 = dataUrl.split(',')[1];\n resolve({ dataUrl, base64 });\n URL.revokeObjectURL(img.src);\n };\n img.src = URL.createObjectURL(file);\n });\n}\n\nfunction renderAttachChips() {\n const container = document.getElementById('attachChips');\n if (chatAttachments.length === 0) {\n container.style.display = 'none';\n container.innerHTML = '';\n return;\n }\n container.style.display = 'flex';\n container.innerHTML = chatAttachments.map((a, i) => {\n const preview = a.type.startsWith('image/')\n ? `<img src=\"${a.dataUrl}\" alt=\"${escapeHtml(a.name)}\">`\n : '📄';\n return `<span class=\"attach-chip\">${preview} ${escapeHtml(a.name)}<span class=\"remove\" onclick=\"removeAttachment(${i})\">✕</span></span>`;\n }).join('');\n}\n\nfunction removeAttachment(index) {\n chatAttachments.splice(index, 1);\n renderAttachChips();\n}\n\n// Also support drag-drop onto the chat input area\ndocument.querySelector('.input-area').addEventListener('dragover', (e) => e.preventDefault());\ndocument.querySelector('.input-area').addEventListener('drop', (e) => {\n e.preventDefault();\n if (e.dataTransfer.files.length) {\n const fakeEvent = { target: { files: e.dataTransfer.files, value: '' } };\n handleChatAttach(fakeEvent);\n }\n});\n\n// Society Agent start - Paste images from clipboard\ndocument.getElementById('input').addEventListener('paste', async (e) => {\n const items = e.clipboardData?.items;\n if (!items) return;\n \n for (const item of items) {\n if (item.type.startsWith('image/')) {\n e.preventDefault();\n const file = item.getAsFile();\n if (file) {\n // Generate a name for pasted images\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);\n const ext = file.type.split('/')[1] || 'png';\n const name = `pasted-image-${timestamp}.${ext}`;\n \n // Create a new file with the generated name\n const namedFile = new File([file], name, { type: file.type });\n \n const { dataUrl, base64 } = await resizeImage(namedFile, 1024);\n // resizeImage converts to JPEG, so mediaType must be image/jpeg\n chatAttachments.push({ \n name: name, \n type: file.type, \n dataUrl, \n base64, \n mediaType: 'image/jpeg' \n });\n renderAttachChips();\n addLog('info', `Pasted image: ${name}`);\n }\n }\n }\n});\n// Society Agent end\n\n// ============================================================================\n// Keyboard\n// ============================================================================\n\ndocument.getElementById('input').addEventListener('keydown', (e) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n sendMessage();\n }\n});\n\n// Close file viewer with Escape; F2 to rename; Delete to delete\ndocument.addEventListener('keydown', (e) => {\n if (e.key === 'Escape') { closeFileViewer(); hideContextMenu(); }\n // F2 to rename selected file\n if (e.key === 'F2' && selectedPath) {\n e.preventDefault();\n const isDir = fileData.some(f => f.path === selectedPath && f.isDir);\n startRename(selectedPath, isDir);\n }\n // Delete key to delete selected file\n if (e.key === 'Delete' && selectedPath && !document.querySelector('.rename-input')) {\n e.preventDefault();\n const isDir = fileData.some(f => f.path === selectedPath && f.isDir);\n deleteFile(selectedPath, isDir);\n }\n});\n\n// Auto-resize textarea\ndocument.getElementById('input').addEventListener('input', function() {\n this.style.height = 'auto';\n this.style.height = Math.min(this.scrollHeight, 200) + 'px';\n});\n\n// ============================================================================\n// Project Management\n// ============================================================================\n\nlet projects = [];\nlet allProjectAgents = []; // flattened list of all agents across projects\nlet selectedAgentId = ''; // '' means default society agent; set to 'projectId:agentId' when a project agent is selected\nlet editingProjectId = null; // null = create, string = editing\n\n// Society Agent start - Parse composite 'projectId:agentId' key\nfunction getSelectedAgent() {\n if (!selectedAgentId) return null;\n const colon = selectedAgentId.indexOf(':');\n if (colon === -1) return null;\n return { projectId: selectedAgentId.slice(0, colon), agentId: selectedAgentId.slice(colon + 1) };\n}\n// Society Agent end\n\nasync function loadProjects() {\n try {\n const res = await fetch(`${API}/api/projects`);\n const data = await res.json();\n projects = data.projects || [];\n // Flatten agents for the selector\n allProjectAgents = [];\n projects.forEach(p => {\n (p.agents || []).forEach(a => {\n allProjectAgents.push({ ...a, projectId: p.id, projectName: p.name });\n });\n });\n renderAgentSelector();\n renderProjectsGrid();\n renderSidebarProjects();\n } catch (e) {\n addLog('warn', 'Failed to load projects');\n }\n}\n\n// ============================================================================\n// Skills & MCPs\n// ============================================================================\n\nasync function refreshSkills() {\n const list = document.getElementById('skillsList');\n if (!list) return;\n list.innerHTML = '<div class=\"skill-item\" style=\"color:#8b949e;\">Loading...</div>';\n try {\n const res = await fetch(`${API}/api/skills`);\n const data = await res.json();\n if (data.skills && data.skills.length > 0) {\n list.innerHTML = data.skills.map(s => `\n <div class=\"skill-item\" style=\"padding:8px 0;border-bottom:1px solid #21262d;\">\n <div style=\"font-weight:500;color:#e6edf3;\">${escapeHtml(s.name)}</div>\n <div style=\"font-size:12px;color:#8b949e;margin-top:2px;\">${escapeHtml(s.description || 'No description')}</div>\n ${s.version ? `<div style=\"font-size:11px;color:#6e7681;margin-top:2px;\">v${escapeHtml(s.version)}</div>` : ''}\n </div>\n `).join('');\n } else {\n list.innerHTML = '<div style=\"color:#8b949e;font-size:13px;\">No global skills installed.<br><span style=\"font-size:12px;\">Add .js files to /skills/ folder</span></div>';\n }\n } catch (e) {\n list.innerHTML = '<div style=\"color:#f85149;\">Error loading skills</div>';\n }\n}\n\nasync function refreshMcps() {\n const list = document.getElementById('mcpsList');\n if (!list) return;\n list.innerHTML = '<div class=\"mcp-item\" style=\"color:#8b949e;\">Loading...</div>';\n try {\n const res = await fetch(`${API}/api/mcps`);\n const data = await res.json();\n if (data.mcps && data.mcps.length > 0) {\n list.innerHTML = data.mcps.map(m => `\n <div class=\"mcp-item\" style=\"padding:8px 0;border-bottom:1px solid #21262d;\">\n <div style=\"display:flex;align-items:center;gap:8px;\">\n <span style=\"color:${m.enabled ? '#3fb950' : '#8b949e'};\">●</span>\n <span style=\"font-weight:500;color:#e6edf3;flex:1;\">${escapeHtml(m.name)}</span>\n <button onclick=\"toggleGlobalMcp('${escapeHtml(m.name)}')\" \n style=\"background:${m.enabled ? 'rgba(248,81,73,0.15)' : 'rgba(63,185,80,0.15)'};\n color:${m.enabled ? '#f85149' : '#3fb950'};\n border:1px solid ${m.enabled ? 'rgba(248,81,73,0.3)' : 'rgba(63,185,80,0.3)'};\n border-radius:4px;padding:2px 8px;font-size:10px;cursor:pointer;\">\n ${m.enabled ? 'Disable' : 'Enable'}\n </button>\n </div>\n <div style=\"font-size:12px;color:#8b949e;margin-top:4px;margin-left:16px;\">${escapeHtml(m.description || 'No description')}</div>\n <div style=\"font-size:11px;color:#6e7681;margin-top:2px;margin-left:16px;font-family:monospace;\">${escapeHtml(m.command)} ${(m.args || []).map(a => escapeHtml(a)).join(' ')}</div>\n </div>\n `).join('');\n } else {\n list.innerHTML = '<div style=\"color:#8b949e;font-size:13px;\">No global MCPs configured.<br><span style=\"font-size:12px;\">Add servers to /mcp-config.json</span></div>';\n }\n } catch (e) {\n list.innerHTML = '<div style=\"color:#f85149;\">Error loading MCPs</div>';\n }\n}\n\nasync function toggleGlobalMcp(name) {\n try {\n await fetch(`${API}/api/mcps/${encodeURIComponent(name)}/toggle`, { method: 'POST' });\n refreshMcps();\n } catch (e) {\n console.error('Failed to toggle MCP:', e);\n }\n}\n\nfunction renderAgentSelector() {\n const sel = document.getElementById('agentSelect');\n const prev = sel.value;\n let html = '<option value=\"\">Default (Society Agent)</option>';\n projects.forEach(p => {\n if (!p.agents || p.agents.length === 0) return;\n html += `<optgroup label=\"${escapeHtml(p.name)}\">`;\n p.agents.forEach(a => {\n const compositeVal = p.id + ':' + a.id;\n html += `<option value=\"${escapeHtml(compositeVal)}\" ${compositeVal === prev ? 'selected' : ''}>[${escapeHtml(p.name)}] ${escapeHtml(a.name)} — ${escapeHtml(a.role)}</option>`;\n });\n html += '</optgroup>';\n });\n sel.innerHTML = html;\n // Restore selection\n if (prev && allProjectAgents.some(a => (a.projectId + ':' + a.id) === prev)) {\n sel.value = prev;\n }\n onAgentChange();\n}\n\nfunction onAgentChange() {\n const sel = document.getElementById('agentSelect');\n selectedAgentId = sel.value;\n const info = document.getElementById('agentSelectInfo');\n \n // Society Agent start - Clear chat when switching agents\n // Reset streaming state\n currentStreamEl = null;\n streamText = '';\n isStreaming = false;\n \n // Clear chat messages area (keep it simple - start fresh)\n const messages = document.getElementById('messages');\n messages.innerHTML = '';\n \n // Show which agent is selected\n if (selectedAgentId) {\n const agent = allProjectAgents.find(a => (a.projectId + ':' + a.id) === selectedAgentId);\n if (agent) {\n addMessage('system', `Now chatting with <b>${escapeHtml(agent.name)}</b> — <span style=\"color:var(--accent)\">${escapeHtml(agent.projectName)}</span>`);\n }\n } else {\n addMessage('system', 'Select an agent from the dropdown above to start chatting');\n }\n // Society Agent end\n \n // Society Agent start - Refresh file explorer scoped to the selected project\n {\n const agentObj = allProjectAgents.find(a => (a.projectId + ':' + a.id) === selectedAgentId);\n const project = agentObj ? projects.find(p => p.id === agentObj.projectId) : null;\n activeProjectFolder = project ? (project.folder || project.id) : null;\n refreshFiles();\n }\n // Society Agent end\n\n if (!selectedAgentId) {\n info.innerHTML = '';\n return;\n }\n const agent = allProjectAgents.find(a => (a.projectId + ':' + a.id) === selectedAgentId);\n if (agent) {\n info.innerHTML = `<span class=\"project-badge\" title=\"${escapeHtml(agent.projectName)}\">📂 ${escapeHtml(agent.projectName)}</span>`;\n }\n}\n\nfunction renderProjectsGrid() {\n const grid = document.getElementById('projectsGrid');\n const countEl = document.getElementById('projectCountInfo');\n countEl.textContent = `${projects.length} project${projects.length !== 1 ? 's' : ''}`;\n\n let html = projects.map(p => {\n const agentChips = (p.agents || []).map(a => {\n const dotCls = !a.ephemeral ? 'supervisor' : 'worker'; // Society Agent - Use !ephemeral\n return `<span class=\"project-agent-chip\"><span class=\"chip-dot ${dotCls}\"></span>${escapeHtml(a.name)}</span>`;\n }).join('');\n\n const totalMsgs = (p.agents || []).reduce((sum, a) => sum + (a.totalMessages || 0), 0);\n const totalTasks = (p.agents || []).reduce((sum, a) => sum + (a.tasksCompleted || 0), 0);\n \n // Society Agent start - Git integration display\n const gitConfig = p.gitConfig;\n const isGitProject = !!gitConfig;\n const gitIcon = isGitProject ? '🔌' : '📁';\n const gitBadge = isGitProject ? `<span class=\"git-badge\" title=\"Branch: ${escapeAttr(gitConfig.currentRef || gitConfig.defaultRef)}\" onclick=\"event.stopPropagation();showGitActions('${escapeAttr(p.id)}')\">🌴 ${escapeHtml(gitConfig.currentRef || gitConfig.defaultRef)}</span>` : '';\n // Society Agent end\n\n return `<div class=\"project-card\">\n <div class=\"project-header\">\n <div class=\"project-icon\">${gitIcon}</div>\n <div class=\"project-meta\">\n <div class=\"project-name\">${escapeHtml(p.name)} ${gitBadge}</div>\n <div class=\"project-desc\">${escapeHtml(p.description || '')}</div>\n </div>\n <span class=\"status-badge ${p.status === 'active' ? 'active' : 'idle'}\">${p.status || 'active'}</span>\n </div>\n <div class=\"project-folder\">📂 ${escapeHtml(p.folder || '/')}</div>\n <div class=\"project-agents\">${agentChips || '<span style=\"color:var(--muted);font-size:11px\">No agents</span>'}</div>\n <div class=\"project-stats\">\n <span>🤖 ${(p.agents || []).length} agent${(p.agents || []).length !== 1 ? 's' : ''}</span>\n <span>💬 ${totalMsgs} msgs</span>\n <span>✅ ${totalTasks} tasks</span>\n </div>\n <div class=\"project-actions\">\n <button class=\"btn btn-primary\" onclick=\"window.open('/project/${escapeAttr(p.id)}','_blank')\" style=\"font-size:12px\">🔗 Open</button>\n ${!isGitProject ? `<button class=\"btn btn-secondary\" onclick=\"showPushToGitLabModal('${escapeAttr(p.id)}', '${escapeAttr(p.name)}')\" style=\"font-size:11px;color:var(--green)\">🔌 Push to GitLab</button>` : ''}\n <button class=\"btn btn-secondary\" onclick=\"editProject('${escapeAttr(p.id)}')\">✎ Edit</button>\n <button class=\"btn btn-secondary\" style=\"color:var(--red)\" onclick=\"deleteProject('${escapeAttr(p.id)}', '${escapeAttr(p.name)}')\">🗑</button>\n </div>\n </div>`;\n }).join('');\n\n // Society Agent start - Add both Create and Git buttons\n html += `<div class=\"project-add-card\" onclick=\"showCreateProjectModal()\">+ Create New Project</div>`;\n html += `<div class=\"project-add-card\" onclick=\"showGitModal()\" style=\"border-color:var(--accent);color:var(--accent)\">🔌 Load from Git</div>`;\n // Society Agent end\n grid.innerHTML = html;\n}\n\nfunction renderSidebarProjects() {\n const list = document.getElementById('projectList');\n if (projects.length === 0) {\n list.innerHTML = '<div style=\"padding:8px 16px;color:var(--muted);font-size:12px\">No projects — go to Projects tab</div>';\n return;\n }\n // Society Agent start - show folder as main identifier (matches disk folder name)\n list.innerHTML = projects.map(p => {\n const agentCount = (p.agents || []).length;\n const folderName = p.folder || p.id; // Folder name = directory on disk\n return `<div class=\"agent-card\" onclick=\"window.open('/project/${escapeAttr(p.id)}','_blank')\" style=\"cursor:pointer\" title=\"${escapeHtml(p.name || folderName)}\">\n <div class=\"name\">${escapeHtml(folderName)}</div>\n <div class=\"role\">${agentCount} agent${agentCount !== 1 ? 's' : ''}</div>\n <span class=\"status-badge ${p.status === 'active' ? 'working' : 'idle'}\">${p.status || 'active'}</span>\n </div>`;\n }).join('');\n // Society Agent end\n}\n\n// Project CRUD\n\n// Society Agent start - simplified project creation\nfunction slugify(text) {\n return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').substring(0, 50);\n}\n\nfunction showCreateProjectModal() {\n editingProjectId = null;\n document.getElementById('projectModalTitle').textContent = 'Create Project';\n document.getElementById('projectModalDesc').textContent = 'Create a project to organize agents around a shared folder.';\n document.getElementById('projectFormName').value = '';\n document.getElementById('projectFormName').disabled = false;\n document.getElementById('projectFormDesc').value = '';\n document.getElementById('projectFormSubmit').textContent = 'Create';\n document.getElementById('projectFolderPreview').textContent = 'projects/';\n document.getElementById('projectModal').style.display = 'flex';\n \n // Update folder preview as user types\n document.getElementById('projectFormName').oninput = function() {\n const slug = slugify(this.value);\n document.getElementById('projectFolderPreview').textContent = slug ? 'projects/' + slug + '/' : 'projects/';\n };\n}\n// Society Agent end\n\n// Society Agent start - simplified edit modal\nfunction editProject(id) {\n const project = projects.find(p => p.id === id);\n if (!project) return;\n editingProjectId = id;\n document.getElementById('projectModalTitle').textContent = `Edit: ${project.name}`;\n document.getElementById('projectModalDesc').textContent = 'Update project settings. Agents are managed from the project page.';\n document.getElementById('projectFormName').value = project.name;\n document.getElementById('projectFormName').disabled = true; // Can't rename existing project\n document.getElementById('projectFormDesc').value = project.description || '';\n document.getElementById('projectFormSubmit').textContent = 'Save';\n document.getElementById('projectFolderPreview').textContent = 'projects/' + (project.folder || project.id) + '/';\n document.getElementById('projectModal').style.display = 'flex';\n}\n// Society Agent end\n\nfunction hideProjectModal() {\n document.getElementById('projectModal').style.display = 'none';\n editingProjectId = null;\n}\n\n// Society Agent start - simplified: name is the id/folder\nasync function submitProjectForm() {\n const name = document.getElementById('projectFormName').value.trim();\n const id = slugify(name); // Auto-generate ID from name\n const description = document.getElementById('projectFormDesc').value.trim();\n const folder = id; // Folder is same as ID\n\n if (!name) {\n alert('Please enter a project name');\n return;\n }\n// Society Agent end\n\n try {\n let res;\n if (editingProjectId) {\n res = await fetch(`${API}/api/projects/${editingProjectId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ name, description, folder }),\n });\n } else {\n res = await fetch(`${API}/api/projects`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ id, name, description, folder }),\n });\n }\n const data = await res.json();\n if (!res.ok) {\n alert(data.error || 'Failed');\n return;\n }\n hideProjectModal();\n addLog('info', editingProjectId ? `Project ${name} updated` : `Project ${name} created`);\n loadProjects();\n } catch (e) {\n alert(`Error: ${e.message}`);\n }\n}\n\nasync function deleteProject(id, name) {\n if (!confirm(`Delete project \"${name}\" and all its agents? This cannot be undone.`)) return;\n try {\n const res = await fetch(`${API}/api/projects/${id}`, { method: 'DELETE' });\n const data = await res.json();\n if (data.success) {\n addLog('info', `Project ${name} deleted`);\n if (selectedAgentId.startsWith(id + ':')) {\n selectedAgentId = '';\n }\n loadProjects();\n } else {\n addLog('error', `Delete failed: ${data.error}`);\n }\n } catch (e) {\n addLog('error', `Delete error: ${e.message}`);\n }\n}\n\n// ===========================================================================\n// Society Agent start - Git Integration Functions\n// ===========================================================================\n\nlet gitCredentials = [];\n\n// Git modal helpers\nfunction updateGitFolderPreview() {\n const name = document.getElementById('gitFormName').value;\n const slug = slugify(name);\n document.getElementById('gitFolderPreview').textContent = slug ? 'projects/' + slug + '/' : 'projects/';\n}\n\nfunction showGitModal() {\n // Reset form\n document.getElementById('gitFormName').value = '';\n document.getElementById('gitFormUrl').value = '';\n document.getElementById('gitFormRef').value = 'main';\n document.getElementById('gitFormDesc').value = '';\n document.getElementById('gitFormStatus').style.display = 'none';\n document.getElementById('gitFormSubmit').disabled = false;\n document.getElementById('gitFormSubmit').textContent = '🔗 Clone & Create';\n document.getElementById('gitFolderPreview').textContent = 'projects/';\n \n // Load credentials for dropdown\n loadCredentials();\n \n document.getElementById('gitModal').style.display = 'flex';\n}\n\nfunction hideGitModal() {\n document.getElementById('gitModal').style.display = 'none';\n}\n\nasync function submitGitForm() {\n const name = document.getElementById('gitFormName').value.trim();\n const id = slugify(name);\n const repoUrl = document.getElementById('gitFormUrl').value.trim();\n const ref = document.getElementById('gitFormRef').value.trim() || 'main';\n const credentialId = document.getElementById('gitFormCredential').value || undefined;\n const description = document.getElementById('gitFormDesc').value.trim();\n \n if (!name || !repoUrl) {\n showGitStatus('error', 'Project name and repository URL are required');\n return;\n }\n \n // Show loading state\n const submitBtn = document.getElementById('gitFormSubmit');\n submitBtn.disabled = true;\n submitBtn.textContent = '⏳ Cloning...';\n showGitStatus('info', 'Cloning repository... This may take a moment.');\n \n try {\n const res = await fetch(`${API}/api/projects/from-git`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ id, name, description, repoUrl, ref, credentialId }),\n });\n const data = await res.json();\n \n if (data.success) {\n showGitStatus('success', `✓ ${data.message}`);\n addLog('info', `Project \"${name}\" loaded from Git`);\n loadProjects();\n setTimeout(() => hideGitModal(), 1500);\n } else {\n showGitStatus('error', `✗ ${data.error || 'Clone failed'}`);\n submitBtn.disabled = false;\n submitBtn.textContent = '🔗 Clone & Create';\n }\n } catch (e) {\n showGitStatus('error', `✗ Error: ${e.message}`);\n submitBtn.disabled = false;\n submitBtn.textContent = '🔗 Clone & Create';\n }\n}\n\nfunction showGitStatus(type, message) {\n const el = document.getElementById('gitFormStatus');\n el.style.display = 'block';\n el.style.background = type === 'error' ? 'rgba(248,81,73,0.15)' : \n type === 'success' ? 'rgba(63,185,80,0.15)' : 'rgba(88,166,255,0.15)';\n el.style.color = type === 'error' ? 'var(--red)' : \n type === 'success' ? 'var(--green)' : 'var(--accent)';\n el.textContent = message;\n}\n\n// Credentials management\nasync function loadCredentials() {\n try {\n const res = await fetch(`${API}/api/credentials`);\n const data = await res.json();\n gitCredentials = data.credentials || [];\n \n // Update dropdown in git modal\n const select = document.getElementById('gitFormCredential');\n select.innerHTML = '<option value=\"\">None (public repo)</option>' +\n gitCredentials.map(c => `<option value=\"${escapeAttr(c.id)}\">${escapeHtml(c.name)} (${c.type} @ ${c.host})</option>`).join('');\n \n // Update credentials list in credentials modal\n renderCredentialsList();\n } catch (e) {\n console.error('Failed to load credentials:', e);\n }\n}\n\nfunction renderCredentialsList() {\n const el = document.getElementById('credentialsList');\n if (gitCredentials.length === 0) {\n el.innerHTML = '<div style=\"color:var(--muted);font-size:12px;padding:8px\">No credentials stored</div>';\n return;\n }\n \n el.innerHTML = gitCredentials.map(c => `\n <div style=\"display:flex;align-items:center;gap:8px;padding:8px;border-bottom:1px solid var(--border)\">\n <span style=\"font-size:16px\">${c.type === 'ssh-key' ? '🔑' : '🎟️'}</span>\n <div style=\"flex:1\">\n <div style=\"font-size:13px\">${escapeHtml(c.name)}</div>\n <div style=\"font-size:11px;color:var(--muted)\">${c.type} • ${c.host} • Added ${new Date(c.createdAt).toLocaleDateString()}</div>\n </div>\n <button onclick=\"deleteCredential('${escapeAttr(c.id)}')\" style=\"background:none;border:none;color:var(--red);cursor:pointer;font-size:14px\">🗑</button>\n </div>\n `).join('');\n}\n\nfunction showCredentialsModal() {\n loadCredentials();\n document.getElementById('credFormName').value = '';\n document.getElementById('credFormType').value = 'ssh-key';\n document.getElementById('credFormHost').value = '';\n document.getElementById('credFormSecret').value = '';\n updateCredFormPlaceholder();\n document.getElementById('credentialsModal').style.display = 'flex';\n}\n\nfunction hideCredentialsModal() {\n document.getElementById('credentialsModal').style.display = 'none';\n}\n\nfunction updateCredFormPlaceholder() {\n const type = document.getElementById('credFormType').value;\n const label = document.getElementById('credFormSecretLabel');\n const textarea = document.getElementById('credFormSecret');\n \n if (type === 'ssh-key') {\n label.textContent = 'Private Key';\n textarea.placeholder = '-----BEGIN OPENSSH PRIVATE KEY-----\\n...';\n textarea.rows = 4;\n } else {\n label.textContent = 'Access Token';\n textarea.placeholder = 'glpat-xxxxxxxxxxxxxxxxxxxx';\n textarea.rows = 1;\n }\n}\n\nasync function addCredential() {\n const name = document.getElementById('credFormName').value.trim();\n const type = document.getElementById('credFormType').value;\n const host = document.getElementById('credFormHost').value.trim();\n const secret = document.getElementById('credFormSecret').value.trim();\n \n if (!name || !host || !secret) {\n alert('Please fill in all fields');\n return;\n }\n \n try {\n const res = await fetch(`${API}/api/credentials`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ name, type, host, secret }),\n });\n \n if (res.ok) {\n addLog('info', `Credential \"${name}\" added`);\n loadCredentials();\n // Clear form\n document.getElementById('credFormName').value = '';\n document.getElementById('credFormSecret').value = '';\n } else {\n const data = await res.json();\n alert(`Error: ${data.error}`);\n }\n } catch (e) {\n alert(`Error: ${e.message}`);\n }\n}\n\nasync function deleteCredential(id) {\n if (!confirm('Delete this credential?')) return;\n \n try {\n const res = await fetch(`${API}/api/credentials/${id}`, { method: 'DELETE' });\n if (res.ok) {\n addLog('info', 'Credential deleted');\n loadCredentials();\n }\n } catch (e) {\n alert(`Error: ${e.message}`);\n }\n}\n\n// Git actions for existing projects\nasync function showGitActions(projectId) {\n const project = projects.find(p => p.id === projectId);\n if (!project || !project.gitConfig) return;\n \n document.getElementById('gitActionsTitle').textContent = `🔗 Git: ${project.name}`;\n document.getElementById('gitActionsContent').innerHTML = '<div style=\"color:var(--muted)\">Loading...</div>';\n document.getElementById('gitActionsModal').style.display = 'flex';\n \n try {\n // Get status and branches\n const [statusRes, branchesRes] = await Promise.all([\n fetch(`${API}/api/projects/${projectId}/git/status`),\n fetch(`${API}/api/projects/${projectId}/git/branches`),\n ]);\n \n const status = await statusRes.json();\n const branches = await branchesRes.json();\n \n const hasChanges = status.hasLocalChanges;\n const changedFiles = status.changedFiles || [];\n \n let html = `\n <div style=\"margin-bottom:16px\">\n <div style=\"font-size:13px;margin-bottom:8px\"><strong>Repository:</strong> ${escapeHtml(project.gitConfig.url)}</div>\n <div style=\"font-size:13px;margin-bottom:8px\"><strong>Current Branch:</strong> 🌿 ${escapeHtml(status.currentRef || 'unknown')}</div>\n ${status.ahead || status.behind ? `<div style=\"font-size:12px;color:var(--muted)\">↑ ${status.ahead || 0} ahead, ↓ ${status.behind || 0} behind</div>` : ''}\n </div>\n \n <div style=\"margin-bottom:16px\">\n <div style=\"font-size:12px;color:var(--muted);margin-bottom:6px\">Local Changes</div>\n ${hasChanges ? `\n <div style=\"background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:8px;font-family:monospace;font-size:11px;max-height:120px;overflow-y:auto\">\n ${changedFiles.map(f => `<div>${escapeHtml(f)}</div>`).join('')}\n </div>\n ` : '<div style=\"color:var(--green);font-size:12px\">✓ No local changes</div>'}\n </div>\n \n <div style=\"margin-bottom:16px\">\n <div style=\"font-size:12px;color:var(--muted);margin-bottom:6px\">Switch Branch</div>\n <select id=\"gitBranchSelect\" style=\"width:100%;padding:6px;border:1px solid var(--border);border-radius:4px;background:var(--bg);color:var(--text);font-size:12px\">\n ${branches.local?.map(b => `<option value=\"${escapeAttr(b)}\" ${b === status.currentRef ? 'selected' : ''}>${escapeHtml(b)}</option>`).join('')}\n <optgroup label=\"Remote\">\n ${branches.remote?.filter(b => !branches.local?.includes(b)).map(b => `<option value=\"${escapeAttr(b)}\">${escapeHtml(b)}</option>`).join('')}\n </optgroup>\n </select>\n <button onclick=\"checkoutBranch('${escapeAttr(projectId)}')\" class=\"btn btn-secondary\" style=\"margin-top:8px;font-size:11px\">Checkout</button>\n </div>\n \n <div style=\"display:flex;gap:8px;flex-wrap:wrap\">\n <button onclick=\"pullProject('${escapeAttr(projectId)}')\" class=\"btn btn-secondary\" style=\"font-size:11px\">⬇️ Pull</button>\n ${hasChanges ? `\n <button onclick=\"showPushForm('${escapeAttr(projectId)}')\" class=\"btn btn-primary\" style=\"font-size:11px\">⬆️ Push Changes</button>\n <button onclick=\"viewDiff('${escapeAttr(projectId)}')\" class=\"btn btn-secondary\" style=\"font-size:11px\">📄 View Diff</button>\n ` : ''}\n </div>\n \n <div id=\"gitPushForm\" style=\"display:none;margin-top:16px;padding:12px;background:var(--bg);border:1px solid var(--border);border-radius:4px\">\n <div class=\"form-group\">\n <label>New Branch Name</label>\n <input type=\"text\" id=\"gitPushBranch\" placeholder=\"feature/my-changes\" style=\"width:100%\">\n </div>\n <div class=\"form-group\">\n <label>Commit Message</label>\n <input type=\"text\" id=\"gitPushMessage\" placeholder=\"Agent changes: ...\" style=\"width:100%\">\n </div>\n <button onclick=\"pushChanges('${escapeAttr(projectId)}')\" class=\"btn btn-primary\" style=\"font-size:12px\">Create Branch & Push</button>\n </div>\n `;\n \n document.getElementById('gitActionsContent').innerHTML = html;\n } catch (e) {\n document.getElementById('gitActionsContent').innerHTML = `<div style=\"color:var(--red)\">Error: ${e.message}</div>`;\n }\n}\n\nfunction hideGitActionsModal() {\n document.getElementById('gitActionsModal').style.display = 'none';\n}\n\nfunction showPushForm(projectId) {\n document.getElementById('gitPushForm').style.display = 'block';\n}\n\nasync function pullProject(projectId) {\n try {\n const res = await fetch(`${API}/api/projects/${projectId}/git/pull`, { method: 'POST' });\n const data = await res.json();\n \n if (data.success) {\n addLog('info', `Pulled latest changes for project`);\n showGitActions(projectId); // Refresh modal\n } else {\n alert(`Pull failed: ${data.message}`);\n }\n } catch (e) {\n alert(`Error: ${e.message}`);\n }\n}\n\nasync function checkoutBranch(projectId) {\n const ref = document.getElementById('gitBranchSelect').value;\n if (!ref) return;\n \n try {\n const res = await fetch(`${API}/api/projects/${projectId}/git/checkout`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ ref }),\n });\n const data = await res.json();\n \n if (data.success) {\n addLog('info', `Checked out ${ref}`);\n loadProjects();\n showGitActions(projectId);\n } else {\n alert(`Checkout failed: ${data.message}`);\n }\n } catch (e) {\n alert(`Error: ${e.message}`);\n }\n}\n\nasync function pushChanges(projectId) {\n const branchName = document.getElementById('gitPushBranch').value.trim();\n const commitMessage = document.getElementById('gitPushMessage').value.trim();\n \n if (!branchName || !commitMessage) {\n alert('Branch name and commit message are required');\n return;\n }\n \n try {\n const res = await fetch(`${API}/api/projects/${projectId}/git/push`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ branchName, commitMessage }),\n });\n const data = await res.json();\n \n if (data.success) {\n addLog('info', `Pushed changes to ${branchName}`);\n loadProjects();\n showGitActions(projectId);\n } else {\n alert(`Push failed: ${data.message}`);\n }\n } catch (e) {\n alert(`Error: ${e.message}`);\n }\n}\n\nasync function viewDiff(projectId) {\n try {\n const res = await fetch(`${API}/api/projects/${projectId}/git/diff`);\n const data = await res.json();\n \n if (data.diff) {\n // Show in a simple alert for now (could be a modal with syntax highlighting)\n const diffWindow = window.open('', '_blank', 'width=800,height=600');\n diffWindow.document.write(`<html><head><title>Git Diff</title><style>body{background:#0d1117;color:#e6edf3;font-family:monospace;padding:20px;white-space:pre-wrap;}</style></head><body>${escapeHtml(data.diff)}</body></html>`);\n } else {\n alert('No changes to show');\n }\n } catch (e) {\n alert(`Error: ${e.message}`);\n }\n}\n\n// Push local project to GitLab\nfunction showPushToGitLabModal(projectId, projectName) {\n document.getElementById('pushGitLabProjectId').value = projectId;\n document.getElementById('pushGitLabRepoName').value = projectName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n document.getElementById('pushGitLabError').style.display = 'none';\n document.getElementById('pushGitLabSuccess').style.display = 'none';\n document.getElementById('pushGitLabBtn').disabled = false;\n document.getElementById('pushGitLabBtn').textContent = '🔗 Create & Push';\n document.getElementById('pushGitLabNamespace').innerHTML = '<option value=\"\">Select credential first...</option>';\n \n // Load credentials (GitLab tokens only)\n loadPushGitLabCredentials();\n \n document.getElementById('pushToGitLabModal').style.display = 'flex';\n}\n\nfunction hidePushToGitLabModal() {\n document.getElementById('pushToGitLabModal').style.display = 'none';\n}\n\nasync function loadPushGitLabCredentials() {\n try {\n const res = await fetch(`${API}/api/credentials`);\n const data = await res.json();\n const tokenCreds = (data.credentials || []).filter(c => c.type === 'token');\n \n const select = document.getElementById('pushGitLabCredential');\n if (tokenCreds.length === 0) {\n select.innerHTML = '<option value=\"\">No GitLab tokens - add one in Credentials</option>';\n } else if (tokenCreds.length === 1) {\n // Auto-select and load namespaces when only one credential\n select.innerHTML = `<option value=\"${escapeAttr(tokenCreds[0].id)}\">${escapeHtml(tokenCreds[0].name)} (${tokenCreds[0].host})</option>`;\n loadPushGitLabNamespaces(tokenCreds[0].id);\n } else {\n select.innerHTML = '<option value=\"\">Select a credential...</option>' +\n tokenCreds.map(c => `<option value=\"${escapeAttr(c.id)}\">${escapeHtml(c.name)} (${c.host})</option>`).join('');\n }\n \n // Add change listener to load namespaces\n select.onchange = () => loadPushGitLabNamespaces(select.value);\n } catch (e) {\n console.error('Failed to load credentials:', e);\n }\n}\n\nasync function loadPushGitLabNamespaces(credentialId) {\n const nsSelect = document.getElementById('pushGitLabNamespace');\n if (!credentialId) {\n nsSelect.innerHTML = '<option value=\"\">Select credential first...</option>';\n return;\n }\n \n nsSelect.innerHTML = '<option value=\"\">Loading...</option>';\n \n try {\n const res = await fetch(`${API}/api/credentials/${credentialId}/namespaces`);\n const data = await res.json();\n \n if (data.namespaces && data.namespaces.length > 0) {\n nsSelect.innerHTML = '<option value=\"\">(Personal namespace)</option>' +\n data.namespaces.map(ns => `<option value=\"${escapeAttr(ns.id)}\">${escapeHtml(ns.full_path)} (${ns.kind})</option>`).join('');\n } else {\n nsSelect.innerHTML = '<option value=\"\">(Personal namespace)</option>';\n }\n } catch (e) {\n nsSelect.innerHTML = '<option value=\"\">(Personal namespace - could not load groups)</option>';\n }\n}\n\nasync function pushToGitLab() {\n const projectId = document.getElementById('pushGitLabProjectId').value;\n const credentialId = document.getElementById('pushGitLabCredential').value;\n const repoName = document.getElementById('pushGitLabRepoName').value.trim();\n const namespaceId = document.getElementById('pushGitLabNamespace').value || undefined;\n const visibility = document.getElementById('pushGitLabVisibility').value;\n \n const errorEl = document.getElementById('pushGitLabError');\n const successEl = document.getElementById('pushGitLabSuccess');\n const btn = document.getElementById('pushGitLabBtn');\n \n errorEl.style.display = 'none';\n successEl.style.display = 'none';\n \n if (!credentialId) {\n errorEl.textContent = 'Please select a GitLab credential';\n errorEl.style.display = 'block';\n return;\n }\n \n if (!repoName) {\n errorEl.textContent = 'Please enter a repository name';\n errorEl.style.display = 'block';\n return;\n }\n \n btn.disabled = true;\n btn.textContent = '⏳ Creating repository...';\n \n try {\n const res = await fetch(`${API}/api/projects/${projectId}/git/push-to-gitlab`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ credentialId, repoName, namespaceId, visibility }),\n });\n const data = await res.json();\n \n if (data.success) {\n successEl.innerHTML = `✓ Repository created and pushed!<br><a href=\"${escapeHtml(data.repoUrl)}\" target=\"_blank\" style=\"color:white\">${escapeHtml(data.repoUrl)}</a>`;\n successEl.style.display = 'block';\n btn.textContent = '✓ Done!';\n addLog('info', `Pushed project \"${projectId}\" to GitLab`);\n loadProjects();\n setTimeout(() => hidePushToGitLabModal(), 3000);\n } else {\n errorEl.textContent = data.error || 'Failed to push to GitLab';\n errorEl.style.display = 'block';\n btn.disabled = false;\n btn.textContent = '🔗 Create & Push';\n }\n } catch (e) {\n errorEl.textContent = `Error: ${e.message}`;\n errorEl.style.display = 'block';\n btn.disabled = false;\n btn.textContent = '🔗 Create & Push';\n }\n}\n\n// Society Agent end - Git Integration Functions\n// ===========================================================================\n\n// Load projects on startup\nsocket.on('connect', () => { \n loadProjects(); \n refreshSkills();\n refreshMcps();\n});\n// Refresh on agent events\nsocket.on('system-event', (data) => {\n if (data.type === 'agent-created' || data.type === 'agent-deleted') loadProjects();\n});\n\n// ============================================================================\n// Tab Switching\n// ============================================================================\n\nfunction switchTab(tab) {\n document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\n document.querySelectorAll('.tab-content').forEach(p => p.classList.remove('active'));\n if (tab === 'terminal') {\n document.getElementById('tabTerminal').classList.add('active');\n document.getElementById('paneTerminal').classList.add('active');\n if (!term) initTerminal();\n else fitAddon.fit();\n } else if (tab === 'projects') {\n document.getElementById('tabProjects').classList.add('active');\n document.getElementById('paneProjects').classList.add('active');\n loadProjects(); // refresh when switching to tab\n } else {\n document.getElementById('tabChat').classList.add('active');\n document.getElementById('paneChat').classList.add('active');\n }\n}\n\n// ============================================================================\n// Terminal (xterm.js + node-pty via WebSocket)\n// ============================================================================\n\nlet term = null;\nlet fitAddon = null;\nlet termConnected = false;\n\nfunction initTerminal() {\n const container = document.getElementById('terminalContainer');\n container.innerHTML = '';\n\n term = new Terminal({\n cursorBlink: true,\n fontSize: 14,\n fontFamily: \"'SF Mono', 'Cascadia Code', 'Consolas', monospace\",\n theme: {\n background: '#0d1117',\n foreground: '#e6edf3',\n cursor: '#58a6ff',\n selectionBackground: 'rgba(88,166,255,0.3)',\n black: '#484f58', red: '#f85149', green: '#3fb950', yellow: '#d29922',\n blue: '#58a6ff', magenta: '#bc8cff', cyan: '#39c5cf', white: '#b1bac4',\n },\n });\n\n fitAddon = new FitAddon.FitAddon();\n term.loadAddon(fitAddon);\n term.open(container);\n\n // Small delay for the container to be laid out before fitting\n setTimeout(() => fitAddon.fit(), 100);\n\n window.addEventListener('resize', () => { if (fitAddon) fitAddon.fit(); });\n\n connectTerminal();\n\n term.onData((data) => {\n if (termConnected) {\n socket.emit('terminal-input', data);\n }\n });\n\n term.onResize(({ cols, rows }) => {\n if (termConnected) {\n socket.emit('terminal-resize', { cols, rows });\n }\n });\n\n // Society Agent start - Block browser shortcuts (Ctrl+W etc) when terminal tab is visible\n // Use capture phase at window level so it fires before the browser acts on the key\n term.attachCustomKeyEventHandler((e) => {\n if (e.ctrlKey && !e.altKey && !e.metaKey) {\n const blockedKeys = ['w','t','n','k','l','u','h','j'];\n if (blockedKeys.includes(e.key.toLowerCase())) {\n e.preventDefault();\n return true;\n }\n }\n return true;\n });\n // Society Agent end\n // Society Agent end\n}\n\nfunction connectTerminal() {\n const shell = document.getElementById('termShell').value;\n socket.emit('terminal-start', { shell, cols: term.cols, rows: term.rows });\n term.clear();\n term.writeln('\\x1b[33mConnecting to terminal...\\x1b[0m');\n}\n\nfunction reconnectTerminal() {\n socket.emit('terminal-stop');\n setTimeout(() => connectTerminal(), 200);\n}\n\nsocket.on('terminal-output', (data) => {\n if (term) term.write(data);\n termConnected = true;\n});\n\nsocket.on('terminal-ready', () => {\n termConnected = true;\n if (term) {\n term.clear();\n addLog('info', 'Terminal connected');\n }\n});\n\nsocket.on('terminal-exit', (data) => {\n termConnected = false;\n if (term) term.writeln(`\\x1b[31m\\r\\nProcess exited (code ${data.code})\\x1b[0m`);\n addLog('warn', `Terminal exited (code ${data.code})`);\n});\n</script>\n</body>\n</html>\n"}}}} |