        const vscode = acquireVsCodeApi();
        const chat = document.getElementById('chat');
        const input = document.getElementById('input');
        
        // [State Persistence - Tier 0] 즉시 복원 (Instant Restore from WebView State)
        const previousState = vscode.getState();
        if (previousState && previousState.history && previousState.history.length > 0) {
            console.log('[Astra] Restoring from Webview State...');
            renderHistory(previousState.history);
        }

        function saveWebviewState(history) {
            const current = vscode.getState() || {};
            vscode.setState({ ...current, history });
        }

        function saveUiState() {
            const current = vscode.getState() || {};
            vscode.setState({ ...current, secondBrainTraceEnabled, secondBrainTraceDebug });
        }

        function renderHistory(history) {
            if (!history || history.length === 0) return;
            chat.innerHTML = '';
            history.forEach(m => {
                if (!m) return;
                // Only skip truly internal system messages, keep assistant thoughts
                if (m.role === 'system' && m.internal) return;
                addMsg(m.content, m.role === 'assistant' ? 'assistant' : 'user', m.rationale);
            });
            chat.scrollTop = chat.scrollHeight;
        }
        const sendBtn = document.getElementById('sendBtn');
        const stopBtn = document.getElementById('stopBtn');
        const cancelBtn = document.getElementById('cancelBtn');
        const toastNotif = document.getElementById('toastNotif');
        const thinkingBar = document.getElementById('thinkingBar');
        const statusLabel = document.getElementById('statusLabel');
        const stepper = document.getElementById('stepper');

        // --- Draft State Management ---
        let isDraftActive = false;
        let _toastTimer = null;

        function showToast(msg, type = 'info') {
            toastNotif.textContent = msg;
            toastNotif.className = 'toast-notif toast-' + type + ' toast-visible';
            if (_toastTimer) clearTimeout(_toastTimer);
            _toastTimer = setTimeout(() => {
                toastNotif.classList.remove('toast-visible');
            }, 2500);
        }

        function setDraftActive(active) {
            isDraftActive = active;
            cancelBtn.style.display = active ? 'inline-flex' : 'none';
        }

        // 생성 중/완료 시 Send ⇔ Stop 전환
        function setGenerating(generating) {
            if (generating) {
                sendBtn.style.display = 'none';
                stopBtn.style.display = 'inline-flex';
                // 생성 중에는 Clear 버튼 숨김
                cancelBtn.style.display = 'none';
            } else {
                stopBtn.style.display = 'none';
                sendBtn.style.display = 'inline-flex';
                sendBtn.disabled = false;
                // Draft 상태에 따라 Clear 버튼 복원
                if (isDraftActive) cancelBtn.style.display = 'inline-flex';
            }
        }

        function clearDraft() {
            // Step 1: 상태 초기화 (Draft State Reset)
            setDraftActive(false);
            // Step 2: UI 반영 (Input + Attachments 초기화)
            input.value = '';
            input.style.height = 'auto';
            pendingFiles = [];
            renderAttachments();
            input.focus();
            // Step 3: Toast 알림으로 즉각적 피드백
            showToast('✕ 작성 내용이 초기화되었습니다.', 'warn');
            Sound.warn();
        }


        // --- Sound Manager ---
        const Sound = {
            ctx: null,
            init() { if (!this.ctx) this.ctx = new (window.AudioContext || window.webkitAudioContext)(); },
            play(freq, type, dur) {
                try {
                    this.init();
                    const osc = this.ctx.createOscillator();
                    const gain = this.ctx.createGain();
                    osc.type = type;
                    osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
                    gain.gain.setValueAtTime(0.05, this.ctx.currentTime);
                    gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + dur);
                    osc.connect(gain);
                    gain.connect(this.ctx.destination);
                    osc.start();
                    osc.stop(this.ctx.currentTime + dur);
                } catch(e) {}
            },
            success() { this.play(880, 'sine', 0.1); setTimeout(() => this.play(1109, 'sine', 0.15), 80); },
            warn() { this.play(440, 'triangle', 0.3); }
        };

        function setStep(stepId, state = 'active') {
            stepper.classList.add('active');
            const step = document.getElementById('step-' + stepId);
            if (step) {
                if (state === 'active') {
                    document.querySelectorAll('.step').forEach(s => s.classList.remove('active'));
                    step.classList.add('active');
                } else if (state === 'complete') {
                    step.classList.remove('active');
                    step.classList.add('complete');
                }
            }
        }

        function resetStepper() {
            stepper.classList.remove('active');
            document.querySelectorAll('.step').forEach(s => {
                s.classList.remove('active');
                s.classList.remove('complete');
            });
        }
        const modelSel = document.getElementById('modelSel');
        const brainSel = document.getElementById('brainSel');
        const historyOverlay = document.getElementById('historyOverlay');
        const historyList = document.getElementById('historyList');
        const statusDot = document.getElementById('statusDot');
        const engineStatusText = document.getElementById('engineStatusText');
        const attachBtn = document.getElementById('attachBtn');
        const fileInput = document.getElementById('fileInput');
        const attachPreview = document.getElementById('attachPreview');
        const agentSel = document.getElementById('agentSel');
        const designerSel = document.getElementById('designerSel');
        const chronicleRecordSel = document.getElementById('chronicleRecordSel');
        const editAgentBtn = document.getElementById('editAgentBtn');
        const addAgentBtn = document.getElementById('addAgentBtn');
        const deleteAgentBtn = document.getElementById('deleteAgentBtn');
        const knowledgeScopeSel = document.getElementById('knowledgeScopeSel');
        const editKnowledgeMapBtn = document.getElementById('editKnowledgeMapBtn');
        const reloadKnowledgeMapBtn = document.getElementById('reloadKnowledgeMapBtn');
        const addBrainBtn = document.getElementById('addBrainBtn');
        const editBrainBtn = document.getElementById('editBrainBtn');
        const deleteBrainBtn = document.getElementById('deleteBrainBtn');
        const saveWikiRawBtn = document.getElementById('saveWikiRawBtn');
        const agentConfigPanel = document.getElementById('agentConfigPanel');
        const agentPrompt = document.getElementById('agentPrompt');
        const negativePrompt = document.getElementById('negativePrompt');
        const updateAgentBtn = document.getElementById('updateAgentBtn');
        const agentMapOverlay = document.getElementById('agentMapOverlay');
        const closeAgentMapBtn = document.getElementById('closeAgentMapBtn');
        const cancelAgentMapBtn = document.getElementById('cancelAgentMapBtn');
        const saveAgentMapBtn = document.getElementById('saveAgentMapBtn');
        const editAgentMapJsonBtn = document.getElementById('editAgentMapJsonBtn');
        const addKnowledgeFolderBtn = document.getElementById('addKnowledgeFolderBtn');
        const addSkillFolderBtn = document.getElementById('addSkillFolderBtn');
        const addSkillFileBtn = document.getElementById('addSkillFileBtn');
        const knowledgeFolderList = document.getElementById('knowledgeFolderList');
        const skillFolderList = document.getElementById('skillFolderList');
        const agentMapAgentName = document.getElementById('agentMapAgentName');
        const agentMapStatus = document.getElementById('agentMapStatus');
        const readyBar = document.getElementById('readyBar');
        const rbDot = document.getElementById('rbDot');
        const rbContent = document.getElementById('rbContent');
        const ctxBadge = document.getElementById('ctxBadge');
        const ctxBrainName = document.getElementById('ctxBrainName');
        const ctxAgentName = document.getElementById('ctxAgentName');
        const ctxProjectName = document.getElementById('ctxProjectName');
        const recordsLatest = document.getElementById('recordsLatest');

        function escAttr(t) { return String(t == null ? '' : t).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }
        function fmtMixHint(w) { return `모델 ${100 - w}% · 두뇌 ${w}%`; }
        /** Compact relative-time formatter for chips (e.g. "2m ago", "just now"). */
        function formatRelativeTime(iso) {
            try {
                const then = new Date(iso).getTime();
                if (!Number.isFinite(then)) return iso;
                const diffSec = Math.max(0, Math.floor((Date.now() - then) / 1000));
                if (diffSec < 45) return 'just now';
                if (diffSec < 90) return '1m ago';
                const m = Math.floor(diffSec / 60);
                if (m < 60) return `${m}m ago`;
                const h = Math.floor(m / 60);
                if (h < 24) return `${h}h ago`;
                const d = Math.floor(h / 24);
                return `${d}d ago`;
            } catch { return iso; }
        }
        function fmtK(n) {
            if (typeof n !== 'number' || !isFinite(n)) return '?';
            if (n >= 1000) return (n / 1000).toFixed(n >= 10000 ? 0 : 1).replace(/\.0$/, '') + 'k';
            return String(n);
        }
        function shortModel(m) { m = String(m || ''); const i = m.lastIndexOf('/'); return i >= 0 ? m.slice(i + 1) : m; }
        function selText(sel) { try { return sel && sel.selectedIndex >= 0 ? (sel.options[sel.selectedIndex].text || '').trim() : ''; } catch { return ''; } }
        function truncMid(s, n) { s = String(s || ''); if (s.length <= n) return s; const h = Math.max(4, Math.floor((n - 1) / 2)); return s.slice(0, h) + '…' + s.slice(-h); }

        // ── Context Bar (Brain / Agent / Project summary) + Records line ──────
        function syncContextBar() {
            if (ctxBrainName) ctxBrainName.textContent = selText(brainSel) || '—';
            if (ctxAgentName) { const t = selText(agentSel); ctxAgentName.textContent = (!t || /no agent/i.test(t)) ? '기본' : t; }
            if (ctxProjectName) ctxProjectName.textContent = selText(designerSel) || '—';
            // welcome 패널이 떠 있으면 두뇌/프로젝트 변경이 즉시 반영되도록 같이 호출.
            try { _renderWelcome(); } catch { /* 초기화 전 호출은 무시 */ }
        }

        // ──────────────────────────────────────────────────────────────────────
        // Welcome panel — 빈 채팅 상태에서 보이는 동적 시작 가이드.
        //
        // 첫 사용자(두뇌 미설정·모델 미선택)에게는 "시작하기 3단계" 체크리스트를,
        // 이미 준비가 끝난 사용자에게는 예시 질문 chip을 보여준다. 같은 슬롯에서
        // 상태에 따라 내용만 바뀌므로 노이즈가 안 쌓인다. 첫 메시지가 가면
        // sender(`if (document.querySelector('.welcome')) ... .remove()`)가
        // 패널 자체를 제거하므로 dismiss 로직은 별도로 둘 필요 없음.
        // ──────────────────────────────────────────────────────────────────────
        const _SAMPLE_PROMPTS = [
            { emoji: '📋', text: '지금 열린 프로젝트의 구조를 분석하고 핵심 모듈을 알려줘' },
            { emoji: '🐞', text: '이 코드에서 잠재적인 버그·엣지 케이스가 있는지 검토해줘' },
            { emoji: '✍️', text: '이 함수에 사용자 입장에서 이해하기 쉬운 주석을 달아줘' },
            { emoji: '🧭', text: '오늘 무엇부터 하면 좋을지 우선순위를 짜줘 (1인 기업 모드 추천)' },
        ];

        function _renderWelcome() {
            const panel = document.getElementById('welcomePanel');
            if (!panel) return;
            // 패널이 한 번 제거된 상태(첫 메시지 이후)면 다시 만들지 않는다.
            if (!panel.isConnected) return;

            const brainName = (ctxBrainName && ctxBrainName.textContent || '').trim();
            const hasBrain = brainName && brainName !== '—';
            const modelVal = (modelSel && modelSel.value || '').trim()
                || (document.getElementById('modelInlineSel') && document.getElementById('modelInlineSel').value || '').trim();
            const hasModel = !!modelVal;
            const ready = hasBrain && hasModel;

            panel.innerHTML = '';
            const logo = document.createElement('div');
            logo.className = 'welcome-logo';
            logo.textContent = '✦';
            const title = document.createElement('div');
            title.className = 'welcome-title';
            title.textContent = ready
                ? '준비 완료. 무엇을 도와드릴까요?'
                : 'Astra에 오신 것을 환영합니다';
            const lead = document.createElement('p');
            lead.className = 'welcome-lead';
            lead.textContent = ready
                ? '아래 예시 중 하나를 눌러 시작하거나, 입력창에 직접 적어보세요.'
                : '로컬에서 동작하는 개인 AI 비서입니다. 시작하기 전에 두 가지만 확인해주세요.';
            panel.appendChild(logo);
            panel.appendChild(title);
            panel.appendChild(lead);

            if (!ready) {
                // ── 시작 체크리스트 (3단계) ──
                const steps = document.createElement('div');
                steps.className = 'welcome-checklist';
                const mkStep = (n, done, label, hint, actionLabel, onAction) => {
                    const row = document.createElement('div');
                    row.className = 'wc-step' + (done ? ' done' : '');
                    const bullet = document.createElement('div');
                    bullet.className = 'wc-bullet';
                    bullet.textContent = done ? '✓' : String(n);
                    const txt = document.createElement('div');
                    txt.className = 'wc-text';
                    const t1 = document.createElement('div');
                    t1.className = 'wc-label';
                    t1.textContent = label;
                    const t2 = document.createElement('div');
                    t2.className = 'wc-hint';
                    t2.textContent = hint;
                    txt.appendChild(t1);
                    txt.appendChild(t2);
                    row.appendChild(bullet);
                    row.appendChild(txt);
                    if (!done && actionLabel && onAction) {
                        const btn = document.createElement('button');
                        btn.className = 'wc-action';
                        btn.textContent = actionLabel;
                        btn.onclick = onAction;
                        row.appendChild(btn);
                    }
                    return row;
                };
                steps.appendChild(mkStep(
                    1, hasBrain,
                    '두뇌(지식 폴더) 연결',
                    '자주 쓰는 노트·문서를 모아둔 로컬 폴더입니다. Astra가 답변할 때 이 폴더의 내용을 참고합니다.',
                    '두뇌 추가',
                    () => {
                        const editBtn = document.getElementById('contextEditBtn');
                        if (editBtn) editBtn.click();
                        const addBrainBtn = document.getElementById('addBrainBtn');
                        if (addBrainBtn) setTimeout(() => addBrainBtn.click(), 120);
                    },
                ));
                steps.appendChild(mkStep(
                    2, hasModel,
                    '사용할 모델 선택',
                    'LM Studio 또는 Ollama에 로드되어 있는 로컬 모델을 고릅니다.',
                    '모델 열기',
                    () => {
                        const editBtn = document.getElementById('contextEditBtn');
                        if (editBtn) editBtn.click();
                        setTimeout(() => { if (modelSel) modelSel.focus(); }, 120);
                    },
                ));
                steps.appendChild(mkStep(
                    3, false,
                    '첫 질문 적어보기',
                    '아래 입력창에 자연어로 무엇이든 적어보세요. 코드·문서·아이디어 모두 가능합니다.',
                    '입력창으로',
                    () => { try { input && input.focus(); } catch {} },
                ));
                panel.appendChild(steps);
                return;
            }

            // ── 준비 완료 상태: 예시 질문 chip ──
            const chips = document.createElement('div');
            chips.className = 'welcome-chips';
            for (const p of _SAMPLE_PROMPTS) {
                const chip = document.createElement('button');
                chip.className = 'welcome-chip';
                chip.innerHTML = `<span class="welcome-chip-emoji">${p.emoji}</span><span class="welcome-chip-text"></span>`;
                chip.querySelector('.welcome-chip-text').textContent = p.text;
                chip.title = '클릭하면 입력창에 채워집니다 (자동 전송 안 함).';
                chip.onclick = () => {
                    if (!input) return;
                    input.value = p.text;
                    input.style.height = 'auto';
                    input.style.height = input.scrollHeight + 'px';
                    input.focus();
                };
                chips.appendChild(chip);
            }
            panel.appendChild(chips);

            // ── 하단 부드러운 안내 (1인 기업 모드, 단축키) ──
            const tips = document.createElement('div');
            tips.className = 'welcome-tips';
            tips.innerHTML = '복잡한 작업은 헤더의 <b>[기업 모드]</b>로 여러 전문 에이전트에게 분배할 수 있습니다. · 입력 후 <b>Cmd/Ctrl + Enter</b>로 전송.';
            panel.appendChild(tips);
        }

        // 모델 selector가 바뀌면 welcome도 즉시 갱신.
        document.addEventListener('change', (e) => {
            if (e.target && (e.target.id === 'modelSel' || e.target.id === 'modelInlineSel')) {
                try { _renderWelcome(); } catch {}
            }
        });
        // v2.2.70 — 자동 기록 on/off 토글 상태. 도구 ▾ 메뉴의 토글 항목과 records-line 라벨
        // (자동 기록 / 자동 기록 (꺼짐)) 모두 이 변수에서 동기화.
        let chronicleAutoEnabled = true;
        function renderChronicleAutoToggle() {
            const btn = document.getElementById('chronicleAutoRecordBtn');
            if (btn) {
                btn.textContent = '자동 기록: ' + (chronicleAutoEnabled ? '켜짐' : '꺼짐');
                btn.classList.toggle('active', chronicleAutoEnabled);
            }
            // v2.2.71 — chronicleAutoStatus 는 이제 도구 메뉴 안에서 "최근 저장 기록" 표시 컨테이너.
            // textContent 를 직접 쓰면 자식(status-dot · recordsLatest)이 지워지므로 opacity / title 만 갱신.
            const statusEl = document.getElementById('chronicleAutoStatus');
            if (statusEl) {
                statusEl.style.opacity = chronicleAutoEnabled ? '' : '0.55';
                statusEl.title = chronicleAutoEnabled
                    ? '자동 기록 켜짐 — 의미 있는 대화가 자동 저장됨'
                    : '자동 기록 꺼짐 — 자동 저장 안 됨 (수동 기록은 가능)';
            }
        }
        function syncRecordsLine() {
            if (!recordsLatest) return;
            const opt = chronicleRecordSel && chronicleRecordSel.value ? selText(chronicleRecordSel) : '';
            recordsLatest.textContent = opt ? '· ' + truncMid(opt, 38) : '';
            // OFF 면 최근 기록 라벨도 dim 처리해서 "지금은 저장 안 됨" 이 한눈에 보이게.
            recordsLatest.style.opacity = chronicleAutoEnabled ? '' : '0.55';
        }

        // ── Ready-status bar (Engine / Model / Brain count / Context / Memory) ──
        let readyState = {};
        function renderReadyBar() {
            if (!readyBar || !rbContent) return;
            const s = readyState;
            const segs = [];
            if (s.engine) {
                const on = s.engine.online;
                const tag = on === true ? 'Online' : on === false ? 'Offline' : '확인 중';
                segs.push(`<span class="rb-seg ${on === false ? 'bad' : on === true ? 'ok' : ''}">${escAttr(tag)}</span>`);
            }
            if (s.model && s.model.name) {
                const loaded = s.model.loaded;
                segs.push(`<span class="rb-seg" title="${escAttr(s.model.name)}${loaded === true ? ' — 메모리에 로드됨' : loaded === false ? ' — 아직 로드 안 됨' : ''}">${escAttr(shortModel(s.model.name))}</span>`);
            }
            if (s.brain && typeof s.brain.files === 'number') {
                segs.push(`<span class="rb-seg" title="${escAttr('Brain: ' + (s.brain.name || ''))}">Brain ${s.brain.files}</span>`);
            }
            if (typeof s.contextLength === 'number') {
                if (s.cappedForSmallModel) {
                    segs.push(`<span class="rb-seg rb-warn" title="${escAttr('작은 모델(≤4B) 감지 — 예산을 ' + fmtK(s.contextLength) + ' tokens 로 축소 (설정 g1nation.contextLength = ' + fmtK(s.nominalContextLength) + '). g1nation.smallModelContextCap 으로 조절.')}">ctx ${fmtK(s.contextLength)} <span class="rb-dim">· 소형모델 제한</span></span>`);
                } else {
                    segs.push(`<span class="rb-seg" title="모델 context window (g1nation.contextLength). 실제 로드된 값과 맞춰주세요.">ctx ${fmtK(s.contextLength)}</span>`);
                }
            }
            segs.push(`<span class="rb-seg ${s.memory ? '' : 'rb-dim'}">메모리 ${s.memory ? 'On' : 'Off'}</span>`);
            if (s.multiAgent) segs.push(`<span class="rb-seg">멀티에이전트</span>`);
            if (s.lmStudioError) {
                segs.push(`<span class="rb-seg bad" title="${escAttr(s.lmStudioError)}">⚠ LM Studio 로드 실패</span>`);
            }
            // 환경 전제 조건 경고 (Bridge·두뇌 볼륨·자격증명·확장 버전) — 전체 목록은 hover.
            if (Array.isArray(s.healthWarnings) && s.healthWarnings.length > 0) {
                segs.push(`<span class="rb-seg bad" title="${escAttr(s.healthWarnings.join('\n'))}">⚠ 환경 ${s.healthWarnings.length}건</span>`);
            }
            rbContent.innerHTML = segs.join('<span class="rb-sep">·</span>');
            if (rbDot) {
                const on = s.engine && s.engine.online;
                const hasEnvWarn = Array.isArray(s.healthWarnings) && s.healthWarnings.length > 0;
                rbDot.className = 'rb-dot ' + (on === false ? 'bad' : hasEnvWarn ? 'warn' : on === true ? 'ok' : 'warn');
            }
        }

        // ── Context-budget badge (직전 요청 기준) ────────────────────────────
        // Last LM Studio prediction stats — merged into the badge after the turn finishes.
        let lastLmStats = null;
        function renderCtxBadge(b) {
            if (!ctxBadge) return;
            if (!b || typeof b.inputTokens !== 'number') { ctxBadge.textContent = ''; ctxBadge.className = 'ctx-badge'; ctxBadge.title = ''; return; }
            const parts = [`≈${fmtK(b.inputTokens)} in / ${fmtK(b.maxOutputTokens)} out`];
            if (typeof b.contextLength === 'number') {
                parts.push(b.cappedForSmallModel ? `ctx ${fmtK(b.contextLength)}↓` : `ctx ${fmtK(b.contextLength)}`);
            }
            if (typeof b.brainFiles === 'number' && b.brainFiles > 0) parts.push(`Brain ${b.brainFiles}`);
            if (b.includesOpenFile) parts.push('📄 열린 파일');
            if (b.imageCount > 0) parts.push(`🖼 ${b.imageCount}`);
            if (b.droppedHistory > 0) parts.push(`기록 −${b.droppedHistory}`);
            if (b.systemTruncated) parts.push('컨텍스트 일부 생략');
            if (b.cappedForSmallModel) parts.push('🔻 작은 모델 모드');
            if (b.windowMismatch && typeof b.actualContextLength === 'number') parts.push('⚠ 실제 창 ' + fmtK(b.actualContextLength) + '↓');
            if (b.tight) parts.push('⚠ 컨텍스트 거의 가득');
            const warn = b.tight || b.systemTruncated || b.windowMismatch;
            ctxBadge.textContent = parts.join(' · ');
            ctxBadge.className = 'ctx-badge' + (warn ? ' warn' : ' ok');
            // New turn starts → drop stale stats from the previous answer.
            lastLmStats = null;
            const mismatchNote = (b.windowMismatch && typeof b.actualContextLength === 'number')
                ? `\n⚠ 모델이 실제로는 ${b.actualContextLength} 토큰 창으로 로드됨 (설정 ${b.nominalContextLength}). 그 한도에 맞춰 예산함.`
                : '';
            ctxBadge.title = `model: ${b.model || ''}${b.paramB != null ? ' (~' + b.paramB + 'B)' : ''}\n입력 ≈ ${b.inputTokens} tokens (시스템 ${b.systemTokens}, 기록 ${b.historyKept}개)\n출력 상한 ${b.maxOutputTokens} tokens / 유효 context window ${b.contextLength} tokens${b.cappedForSmallModel ? ' (작은 모델용 축소; 설정값 ' + b.nominalContextLength + ')' : ''}${mismatchNote}`;
        }
        function renderMapReduceStatus(v) {
            if (!ctxBadge || !v) return;
            if (v.phase === 'start') {
                ctxBadge.textContent = '🧩 큰 입력을 조각으로 나눠 관련 내용 추출 중…';
                ctxBadge.className = 'ctx-badge warn';
                ctxBadge.title = '입력이 컨텍스트 창보다 커서 청크→추출→통합(map-reduce)으로 처리 중입니다.';
            } else if (v.phase === 'done') {
                if (v.allIrrelevant) {
                    ctxBadge.textContent = '🧩 추출 결과: 요청 관련 내용 없음';
                    ctxBadge.className = 'ctx-badge warn';
                    ctxBadge.title = '긴 입력의 모든 조각에서 요청과 직접 관련된 내용을 찾지 못했습니다. 원본을 그대로(예산 내에서 잘라) 전달합니다.';
                } else {
                    ctxBadge.textContent = `🧩 ${v.chunkCount}조각 → ${v.relevantCount}조각 추출·통합`;
                    ctxBadge.className = 'ctx-badge ok';
                    ctxBadge.title = `큰 입력을 ${v.chunkCount}개 조각으로 나눠 그중 ${v.relevantCount}개에서 관련 내용을 추출·통합했습니다.`;
                }
            } else if (v.phase === 'error') {
                ctxBadge.textContent = '🧩 분할 처리 실패 — 단발 처리로 진행';
                ctxBadge.className = 'ctx-badge warn';
            }
        }
        function renderLmStudioStats(s) {
            if (!ctxBadge || !s) return;
            lastLmStats = s;
            const extra = [];
            if (typeof s.tokensPerSecond === 'number') extra.push(`${s.tokensPerSecond.toFixed(1)} tok/s`);
            if (typeof s.timeToFirstTokenSec === 'number') extra.push(`TTFT ${s.timeToFirstTokenSec.toFixed(2)}s`);
            // Speculative-decoding hit rate — shows whether the draft model is paying for itself.
            // A healthy ratio is ~60%+; below ~30% means the draft is mis-predicting and slowing things down.
            if (typeof s.draftTokensCount === 'number' && s.draftTokensCount > 0 && typeof s.acceptedDraftTokensCount === 'number') {
                const pct = Math.round((s.acceptedDraftTokensCount / s.draftTokensCount) * 100);
                extra.push(`spec ${pct}%`);
            }
            if (extra.length === 0) return;
            // Append to badge without clobbering — only if current badge text doesn't already include it.
            const current = ctxBadge.textContent || '';
            const tail = ' · ' + extra.join(' · ');
            if (!current.endsWith(tail)) ctxBadge.textContent = current + tail;
            if (ctxBadge.title) {
                ctxBadge.title += `\n\n[LM Studio]\n${extra.join(' · ')}`;
                if (typeof s.predictedTokensCount === 'number') ctxBadge.title += `\n출력 ${s.predictedTokensCount} tokens`;
                if (typeof s.totalTimeSec === 'number') ctxBadge.title += ` / 총 ${s.totalTimeSec.toFixed(2)}s`;
                if (s.draftModelKey) ctxBadge.title += `\ndraft: ${s.draftModelKey} (${s.acceptedDraftTokensCount || 0}/${s.draftTokensCount || 0} accepted)`;
                if (s.stopReason) ctxBadge.title += `\nstop: ${s.stopReason}`;
            }
        }
        if (readyBar) {
            readyBar.addEventListener('click', e => {
                const t = e.target;
                if (t && t.dataset && t.dataset.act === 'map') vscode.postMessage({ type: 'editKnowledgeMap' });
            });
        }

        // ── "Record a lesson?" prompt (after a rollback / rejected change / repeated complaint) ──
        function renderLessonCandidate(v) {
            const t = v && v.trigger;
            const titleText = t === 'rejected'
                ? '⚠ 방금 변경을 거부하셨네요 — 같은 실수를 반복하지 않게 교훈으로 기록할까요?'
                : t === 'qa-feedback'
                    ? '⚠ 같은 문제가 반복되는 것 같습니다 — 교훈으로 기록해두면 다음 작업 전에 자동으로 체크합니다.'
                    : '⚠ 방금 작업이 롤백됐습니다 — 같은 실수를 반복하지 않게 교훈으로 기록할까요?';
            const reasonLine = v && v.reason ? `<div class="lc-reason">사유: ${escAttr(String(v.reason))}</div>` : '';
            // Reuse the active (or last) assistant bubble as the anchor; fall back to appending to the chat.
            let anchor = streamBody && streamBody._parent;
            if (!anchor) { const all = chat.querySelectorAll('.msg.msg-ai'); anchor = all[all.length - 1]; }
            const old = (anchor || chat).querySelector('.lesson-candidate-box');
            if (old) old.remove();
            const box = document.createElement('div');
            box.className = 'lesson-candidate-box';
            box.innerHTML =
                `<div class="lc-title">${escAttr(titleText)}</div>` +
                reasonLine +
                `<div class="lc-btns"><button class="lc-rec">📝 교훈 기록</button><button class="lc-skip">무시</button></div>`;
            box.querySelector('.lc-rec').onclick = () => { vscode.postMessage({ type: 'createLessonFromConversation' }); box.remove(); };
            box.querySelector('.lc-skip').onclick = () => box.remove();
            if (anchor) {
                const actions = anchor.querySelector('.msg-actions');
                if (actions) anchor.insertBefore(box, actions); else anchor.appendChild(box);
            } else {
                chat.appendChild(box);
            }
            chat.scrollTop = chat.scrollHeight;
        }

        // ── Per-answer "scope used" footer ──────────────────────────────────
        const MEMORY_LAYER_LABELS = {
            'long-term-memory': '장기기억',
            'project-memory': '프로젝트기억',
            'procedural-memory': '절차기억',
            'episodic-memory': '에피소드기억',
            'project-scan': '프로젝트스캔',
            'recent-knowledge': '최근지식',
        };
        function dirOf(rel) {
            const i = Math.max(rel.lastIndexOf('/'), rel.lastIndexOf('\\'));
            return i > 0 ? rel.slice(0, i) : '(루트)';
        }
        function renderScopeFooter(target, v) {
            if (!target) return;
            const old = target.querySelector('.msg-scope-footer');
            if (old) old.remove();
            const footer = document.createElement('div');
            footer.className = 'msg-scope-footer';
            const files = Array.isArray(v.usedBrainFiles) ? v.usedBrainFiles : [];
            const layers = (Array.isArray(v.usedMemoryLayers) ? v.usedMemoryLayers : []).map(s => MEMORY_LAYER_LABELS[s] || s);
            const lessons = Array.isArray(v.lessonFiles) ? v.lessonFiles : [];
            if (files.length === 0 && layers.length === 0 && lessons.length === 0) {
                footer.innerHTML = `<span class="scope-link" data-act="map" title="에이전트↔지식 매핑 편집">🔎 참조 지식 없음</span> <span class="scope-dim">— 모델 자체 지식으로 답변</span>`;
            } else {
                const dirs = Array.from(new Set(files.map(dirOf)));
                let scopeLabel;
                if (v.scoped && Array.isArray(v.configuredFolders) && v.configuredFolders.length) {
                    scopeLabel = v.configuredFolders.join(', ');
                } else if (dirs.length) {
                    scopeLabel = dirs.slice(0, 4).join(', ') + (dirs.length > 4 ? ` 외 ${dirs.length - 4}` : '');
                } else if (files.length === 0) {
                    scopeLabel = '브레인 파일 없음';
                } else {
                    scopeLabel = '전체 브레인';
                }
                const agentTag = v.agentName ? `[${escAttr(v.agentName)}] ` : '';
                const fileTag = files.length ? ` <span class="scope-dim">· 파일 ${files.length}</span>` : '';
                const layerTag = layers.length ? ` <span class="scope-dim">· 메모리 ${escAttr(layers.join('·'))}</span>` : '';
                const lessonTag = lessons.length ? ` <span class="scope-lesson" data-act="lessons" title="${escAttr('적용된 교훈 (클릭 → 교훈 관리):\n' + lessons.join('\n'))}">· ⚠ 교훈 ${lessons.length}</span>` : '';
                const titleAttr = files.length ? `사용된 브레인 파일:\n${files.join('\n')}` : '에이전트↔지식 매핑 편집';
                footer.innerHTML = `<span class="scope-link" data-act="map" title="${escAttr(titleAttr)}">🔎 참조: ${agentTag}${escAttr(scopeLabel)}</span>${fileTag}${lessonTag}${layerTag}`;
            }
            // Knowledge Mix indicator — shows the policy that actually drove this turn so the
            // user can see *why* the answer leaned the way it did.
            if (v.knowledgeMix && typeof v.knowledgeMix.weight === 'number') {
                const w = Math.max(0, Math.min(100, v.knowledgeMix.weight));
                const src = v.knowledgeMix.source;
                const srcLabel = src === 'agent'
                    ? `agent: ${v.knowledgeMix.agent || v.agentName || ''}`
                    : src === 'global' ? 'global' : 'default';
                const mix = document.createElement('div');
                mix.className = 'scope-mix';
                mix.innerHTML = `🎚 Knowledge Mix · Model ${100 - w}% / Brain ${w}% <span class="scope-dim">(${escAttr(srcLabel)})</span>`;
                footer.appendChild(mix);
            }
            // Non-blocking flag: lesson Prevention-Checklist items the answer doesn't visibly address.
            const unaddressed = Array.isArray(v.unaddressedChecklist) ? v.unaddressedChecklist : [];
            if (unaddressed.length) {
                const list = unaddressed.map(it => `· ${escAttr(it)}`).join('<br>');
                const w = document.createElement('div');
                w.className = 'scope-unaddressed';
                w.innerHTML = `⚠ 답변에서 안 보이는 교훈 체크리스트 항목:<br>${list}`;
                footer.appendChild(w);
            }
            footer.addEventListener('click', e => {
                const act = e.target && e.target.dataset && e.target.dataset.act;
                if (act === 'map') vscode.postMessage({ type: 'editKnowledgeMap' });
                else if (act === 'lessons') vscode.postMessage({ type: 'manageLessons' });
            });
            const actions = target.querySelector('.msg-actions');
            if (actions) target.insertBefore(footer, actions); else target.appendChild(footer);
        }

        // `model: ''` means "Use current model" (i.e. no per-agent override).
        // `secondBrainWeight: null` means "Use global setting"; a number 0–100 overrides it.
        let agentMapDraft = { agentPath: '', name: '', knowledgeFolders: [], skillFolders: [], model: '', secondBrainWeight: null };

        /**
         * Sync the per-agent Knowledge Mix UI (checkbox + slider + hint) to whatever
         * is currently in `agentMapDraft.secondBrainWeight`. Called whenever the
         * modal opens or the backend ships fresh data.
         */
        function syncAgentMapMixUi() {
            const cb = document.getElementById('agentMapMixUseGlobal');
            const slider = document.getElementById('agentMapMixSlider');
            const hint = document.getElementById('agentMapMixHint');
            if (!cb || !slider || !hint) return;
            const useGlobal = agentMapDraft.secondBrainWeight === null || agentMapDraft.secondBrainWeight === undefined;
            cb.checked = useGlobal;
            slider.disabled = useGlobal;
            const value = useGlobal
                ? (parseInt((document.getElementById('knowledgeMixSlider') || {}).value, 10) || 50)
                : Math.max(0, Math.min(100, agentMapDraft.secondBrainWeight | 0));
            slider.value = String(value);
            hint.textContent = useGlobal
                ? `전역 설정 사용 · ${fmtMixHint(value)}`
                : fmtMixHint(value);
        }

        /**
         * Rebuild the per-agent model dropdown using whatever model list the top-bar
         * #modelSel currently has. Called whenever the modal opens OR the model list
         * is refreshed by the extension host. Preserves the current draft selection.
         */
        function refreshAgentMapModelOptions() {
            const sel = document.getElementById('agentMapModelSel');
            if (!sel) return;
            const desired = agentMapDraft.model || '';
            sel.innerHTML = '';
            const useDefault = document.createElement('option');
            useDefault.value = '';
            useDefault.innerText = 'Use current model';
            sel.appendChild(useDefault);
            const seen = new Set();
            // Source the available models from the populated top-bar dropdown so we don't
            // need an additional round-trip; if a model is selected for this agent but
            // is no longer in the list, we still surface it so the user sees the value.
            for (const opt of modelSel.options) {
                if (!opt.value || seen.has(opt.value)) continue;
                seen.add(opt.value);
                const o = document.createElement('option');
                o.value = opt.value;
                o.innerText = opt.innerText;
                sel.appendChild(o);
            }
            if (desired && !seen.has(desired)) {
                const o = document.createElement('option');
                o.value = desired;
                o.innerText = `${desired} (saved)`;
                sel.appendChild(o);
            }
            sel.value = desired;
        }

        function renderAgentMapLists() {
            const renderList = (listEl, items, kind) => {
                listEl.innerHTML = '';
                items.forEach((p, idx) => {
                    const li = document.createElement('li');
                    li.className = 'map-item';
                    const icon = document.createElement('span');
                    icon.className = 'map-item-icon';
                    icon.textContent = kind === 'knowledge' ? '📁' : (p.endsWith('.md') || p.endsWith('.markdown') ? '📄' : '📁');
                    const pathEl = document.createElement('span');
                    pathEl.className = 'map-item-path';
                    pathEl.textContent = p;
                    pathEl.title = p;
                    const removeBtn = document.createElement('button');
                    removeBtn.className = 'map-item-remove';
                    removeBtn.textContent = '✕';
                    removeBtn.title = '연결 해제';
                    removeBtn.onclick = () => {
                        items.splice(idx, 1);
                        renderAgentMapLists();
                    };
                    li.appendChild(icon);
                    li.appendChild(pathEl);
                    li.appendChild(removeBtn);
                    listEl.appendChild(li);
                });
            };
            renderList(knowledgeFolderList, agentMapDraft.knowledgeFolders, 'knowledge');
            renderList(skillFolderList, agentMapDraft.skillFolders, 'skill');
        }

        function openAgentMapModal() {
            if (!agentSel || agentSel.value === 'none' || !agentSel.value) {
                showToast('에이전트를 먼저 선택하세요.');
                return;
            }
            agentMapStatus.className = 'map-status';
            agentMapStatus.textContent = '불러오는 중...';
            agentMapDraft = { agentPath: agentSel.value, name: agentSel.options[agentSel.selectedIndex]?.text || '', knowledgeFolders: [], skillFolders: [], model: '', secondBrainWeight: null };
            agentMapAgentName.textContent = agentMapDraft.name;
            knowledgeFolderList.innerHTML = '';
            skillFolderList.innerHTML = '';
            refreshAgentMapModelOptions();
            syncAgentMapMixUi();
            agentMapOverlay.classList.add('visible');
            vscode.postMessage({ type: 'getAgentMap', agentPath: agentSel.value });
        }

        function closeAgentMapModal() {
            agentMapOverlay.classList.remove('visible');
            agentMapStatus.textContent = '';
            agentMapStatus.className = 'map-status';
        }

        let streamBody = null;
        let secondBrainTraceEnabled = true;
        let secondBrainTraceDebug = false;
        let pendingFiles = [];
        let editMode = false;
        if (previousState && typeof previousState.secondBrainTraceEnabled === 'boolean') {
            secondBrainTraceEnabled = previousState.secondBrainTraceEnabled;
        }
        if (previousState && typeof previousState.secondBrainTraceDebug === 'boolean') {
            secondBrainTraceDebug = previousState.secondBrainTraceDebug;
        }
        const initialTraceBtn = document.getElementById('brainTraceBtn');
        initialTraceBtn.classList.toggle('active', secondBrainTraceEnabled);
        initialTraceBtn.setAttribute('data-tooltip', secondBrainTraceEnabled ? 'Second Brain Trace Mode: On' : 'Second Brain Trace Mode: Off');
        const initialTraceDebugBtn = document.getElementById('brainTraceDebugBtn');
        initialTraceDebugBtn.classList.toggle('active', secondBrainTraceDebug);
        initialTraceDebugBtn.setAttribute('data-tooltip', secondBrainTraceDebug ? 'Second Brain Debug JSON: On' : 'Second Brain Debug JSON: Off');

        function fmt(text) { return marked.parse(text || ''); }

        function copyToClipboard(text, btn) {
            const textarea = document.createElement('textarea');
            textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0';
            document.body.appendChild(textarea); textarea.select();
            try {
                if (document.execCommand('copy')) {
                    btn.innerText = '✅ Copied!'; setTimeout(() => { btn.innerText = '📋 Copy'; }, 2000);
                }
            } catch (err) { console.error('Copy failed', err); }
            document.body.removeChild(textarea);
        }

        window.approve = () => {
            const box = document.querySelector('.approval-box');
            if (box) box.remove();
            vscode.postMessage({ type: 'approveAction' });
        };
        window.reject = () => {
            const box = document.querySelector('.approval-box');
            if (box) box.remove();
            vscode.postMessage({ type: 'rejectAction' });
        };

        function exportToMD(text) {
            vscode.postMessage({ type: 'exportResponse', text: text });
        }

        function addMsg(text, role, rationale) {
            const isUser = role === 'user';
            const msgEl = document.createElement('div');
            msgEl.className = 'msg ' + (isUser ? 'msg-user' : 'msg-ai');
            msgEl._raw = text;

            const head = document.createElement('div');
            head.className = 'msg-head';
            head.innerHTML = isUser ? '<div class="av av-user">U</div> You' : '<div class="av av-ai">✦</div> Astra';

            const body = document.createElement('div');
            body.className = 'msg-body markdown-body';
            
            if (isUser) {
                body.innerText = text;
            } else {
                body.innerHTML = fmt(text);
            }

            const actions = document.createElement('div');
            actions.className = 'msg-actions';

            const copyBtn = document.createElement('button');
            copyBtn.className = 'action-btn'; copyBtn.innerText = '📋 Copy';
            copyBtn.onclick = (e) => { e.stopPropagation(); copyToClipboard(msgEl._raw, copyBtn); };

            const exportBtn = document.createElement('button');
            exportBtn.className = 'action-btn'; exportBtn.innerText = '💾 Export';
            exportBtn.onclick = (e) => { e.stopPropagation(); exportToMD(msgEl._raw); };

            actions.appendChild(copyBtn);
            actions.appendChild(exportBtn);
            
            msgEl.appendChild(head); msgEl.appendChild(body);
            msgEl.appendChild(actions);
            chat.appendChild(msgEl); chat.scrollTop = chat.scrollHeight;
            return { body, msgEl };
        }

        window.addEventListener('message', e => {
            const msg = e.data;
            switch(msg.type) {
                case 'slashCommandList':
                    // Extension 측에서 listSlashCommands() 결과 한 번 전달. webview ready 직후.
                    setSlashCommands(msg.commands || []);
                    break;
                case 'addMessage':
                    addMsg(msg.value, msg.role, msg.rationale);
                    // Update state for non-streamed messages
                    const s = vscode.getState() || { history: [] };
                    s.history.push({ role: msg.role === 'assistant' ? 'assistant' : 'user', content: msg.value, rationale: msg.rationale });
                    saveWebviewState(s.history);
                    break;
                case 'streamStart':
                    thinkingBar.classList.remove('active');
                    if (document.querySelector('.welcome')) document.querySelector('.welcome').remove();
                    const res = addMsg('', 'assistant');
                    streamBody = res.body; streamBody._parent = res.msgEl; streamBody._parent._raw = '';
                    streamBody.classList.add('stream-active');
                    break;
                case 'streamChunk':
                    if (streamBody) {
                        streamBody._parent._raw += msg.value;
                        streamBody.innerHTML = fmt(streamBody._parent._raw);
                        chat.scrollTop = chat.scrollHeight;
                    }
                    break;
                case 'streamReplace':
                    // Progressive answering: the backend streamed raw tokens
                    // live (including hidden reasoning, pre-sanitize text);
                    // once everything is finalized it sends the cleaned full
                    // text via streamReplace so the bubble ends up correct
                    // regardless of what slipped through during streaming.
                    if (streamBody) {
                        streamBody._parent._raw = String(msg.value ?? '');
                        streamBody.innerHTML = fmt(streamBody._parent._raw);
                        chat.scrollTop = chat.scrollHeight;
                    }
                    break;
                case 'streamEnd':
                    if (streamBody) {
                        streamBody.classList.remove('stream-active');
                        // Update state after stream finishes
                        const state = vscode.getState() || { history: [] };
                        state.history.push({ role: 'assistant', content: streamBody._parent._raw });
                        saveWebviewState(state.history);
                    }
                    streamBody = null;
                    // 생성 완료 시 Stop 버튼 숨기고 Send 복구
                    setGenerating(false);
                    resetStepper();
                    // 1인 기업 모드는 streamStart를 거치지 않아 thinkingBar가
                    // 그대로 남으므로 streamEnd에서 명시적으로 끄는 게 안전.
                    thinkingBar.classList.remove('active');
                    Sound.success();
                    vscode.postMessage({ type: 'getReadyStatus' });
                    break;
                case 'restoreHistory':
                case 'sessionLoaded':
                    const historyPayload = msg.type === 'sessionLoaded' ? msg.value : msg.value;
                    const history = Array.isArray(historyPayload)
                        ? historyPayload
                        : (Array.isArray(historyPayload?.history) ? historyPayload.history : []);
                    
                    if (history && history.length > 0) {
                        renderHistory(history);
                        saveWebviewState(history);
                    }
                    if (historyPayload?.negativePrompt !== undefined) {
                        negativePrompt.value = historyPayload.negativePrompt;
                    }
                    historyOverlay.classList.remove('visible');
                    break;
                case 'clearChat':
                    // welcomePanel을 빈 div로 다시 만들고 _renderWelcome으로 상태에 맞는
                    // 내용을 채워 넣는다 — 신규 사용자에게는 체크리스트, 준비된 사용자에게는
                    // 예시 질문 chip이 나옴.
                    chat.innerHTML = '<div class="welcome" id="welcomePanel"></div>';
                    try { _renderWelcome(); } catch {}
                    break;
                case 'focusInput':
                    input.focus();
                    break;
                case 'modelsList': {
                    modelSel.innerHTML = '';
                    const inlineModelSel = document.getElementById('modelInlineSel');
                    if (inlineModelSel) inlineModelSel.innerHTML = '';
                    // [State Persistence - Tier 2] LocalStorage에서 마지막 선택 모델 복원 시도
                    const _savedModel = localStorage.getItem('g1nation_last_model');
                    // 서버 추천 모델 vs 로컬 저장 모델 중 우선순위 결정
                    // LocalStorage에 저장된 값이 현재 목록에 있으면 그것을 우선 사용 (Tier 2 우선)
                    const _preferredModel = (_savedModel && msg.value.models.includes(_savedModel))
                        ? _savedModel
                        : msg.value.selected;
                    const _loadedSet = new Set(Array.isArray(msg.value.loadedModels) ? msg.value.loadedModels : []);
                    const _models = Array.isArray(msg.value.models) ? msg.value.models.slice() : [];
                    // Fallback: server returned nothing but we still know the configured model.
                    if (_models.length === 0 && _preferredModel) _models.push(_preferredModel);
                    _models.forEach(m => {
                        const label = _loadedSet.has(m) ? `● ${m}` : m;
                        const o1 = document.createElement('option');
                        o1.value = m; o1.innerText = label;
                        if (m === _preferredModel) o1.selected = true;
                        modelSel.appendChild(o1);
                        if (inlineModelSel) {
                            const o2 = document.createElement('option');
                            o2.value = m; o2.innerText = label;
                            if (m === _preferredModel) o2.selected = true;
                            inlineModelSel.appendChild(o2);
                        }
                    });
                    // LocalStorage에 저장된 모델이 실제로 적용된 경우, 백엔드 설정도 동기화
                    if (_savedModel && _savedModel !== msg.value.selected && msg.value.models.includes(_savedModel)) {
                        vscode.postMessage({ type: 'model', value: _savedModel });
                    }
                    // The model name is now visible inside the footer pill itself,
                    // so statusLabel is reserved for actual status (autoContinue
                    // progress, etc.). Keep it empty in steady state.
                    statusLabel.innerText = '';
                    // Refresh per-agent model dropdown options (if currently visible) so it stays in sync.
                    if (typeof refreshAgentMapModelOptions === 'function') refreshAgentMapModelOptions();
                    // If the company manage overlay is open with cached agent data,
                    // re-render its cards so each per-agent model <select> picks up
                    // any newly-discovered models from the master list.
                    if (typeof _lastCompanyAgentsPayload !== 'undefined' && _lastCompanyAgentsPayload
                        && document.getElementById('companyOverlay')?.classList.contains('visible')) {
                        renderCompanyAgentCards(_lastCompanyAgentsPayload);
                    }
                    // 모델 목록이 채워졌고 default 선택이 정해졌으니 welcome 패널의
                    // "사용할 모델 선택" 체크 표시도 즉시 업데이트.
                    try { _renderWelcome(); } catch {}
                    break;
                }
                case 'brainProfiles': {
                    // 방어: profiles 가 비어/null 로 오면 기존 옵션 보존. 잘못된 상태로 dropdown 을 비워
                    // "+ Add New Brain..." 만 남기는 회귀를 막는다 (v2.2.66 fix).
                    const profilesArr = (msg.value && Array.isArray(msg.value.profiles)) ? msg.value.profiles : [];
                    const activeId = msg.value && msg.value.activeBrainId;
                    if (profilesArr.length === 0) {
                        console.warn('[Astra] brainProfiles message had empty profiles list — preserving existing dropdown.');
                        break;
                    }
                    brainSel.innerHTML = '';
                    profilesArr.forEach(p => {
                        const o = document.createElement('option');
                        o.value = p.id;
                        o.innerText = p.name;
                        brainSel.appendChild(o);
                    });
                    const addOpt = document.createElement('option');
                    addOpt.value = 'new';
                    addOpt.innerText = '+ Add New Brain...';
                    brainSel.appendChild(addOpt);
                    // v2.2.67 — 옵션을 모두 넣은 *후* selection 을 적용한다. appendChild 이전에 `o.selected = true`
                    // 를 거는 방식은 일부 Chromium webview 에서 무시되어 dropdown 이 마지막 옵션('+ Add New Brain...')
                    // 에 머무는 회귀가 발생했다. brainSel.value 로 한 번에 잡으면 이전에 'new' 로 굳어있던 값도 확실히 덮어쓴다.
                    if (activeId && profilesArr.some(p => p.id === activeId)) {
                        brainSel.value = activeId;
                    } else {
                        brainSel.value = profilesArr[0].id;
                    }
                    // 다음에 사용자가 '+ Add New Brain...' 을 클릭하고 취소했을 때 복원할 "이전 유효 선택" 기억.
                    brainSel.dataset.lastSelected = brainSel.value;
                    syncContextBar();
                    break;
                }
                case 'sessionList':
                    historyList.innerHTML = '';
                    msg.value.forEach(s => {
                        const el = document.createElement('div'); el.className = 'history-item';
                        el.setAttribute('role', 'button');
                        el.tabIndex = 0;
                        el.dataset.sessionId = s.id;
                        el.innerHTML = `<div style="font-weight:600; color:var(--text-bright); margin-bottom:2px;">${s.title}</div><div style="font-size:10px; color:var(--text-dim)">${new Date(s.timestamp).toLocaleString()} · ${s.messageCount} msgs</div>`;
                        const load = () => {
                            if (!el.dataset.sessionId) return;
                            vscode.postMessage({ type: 'loadSession', id: el.dataset.sessionId });
                        };
                        el.addEventListener('click', load);
                        el.addEventListener('keydown', event => {
                            if (event.key === 'Enter' || event.key === ' ') {
                                event.preventDefault();
                                load();
                            }
                        });
                        historyList.appendChild(el);
                    });
                    break;
                case 'engineStatus':
                    statusDot.style.background = msg.value.online ? 'var(--success)' : 'var(--error)';
                    engineStatusText.innerText = msg.value.online ? 'Online' : 'Offline';
                    readyState.engine = Object.assign({}, readyState.engine, { online: !!msg.value.online });
                    renderReadyBar();
                    break;
                case 'readyStatus':
                    readyState = Object.assign({}, readyState, msg.value || {});
                    renderReadyBar();
                    break;
                case 'contextBudget':
                    renderCtxBadge(msg.value);
                    break;
                case 'lmStudioStats':
                    renderLmStudioStats(msg.value);
                    break;
                case 'mapReduceStatus':
                    renderMapReduceStatus(msg.value);
                    break;
                case 'usedScope': {
                    let target = streamBody && streamBody._parent;
                    if (!target) {
                        const all = chat.querySelectorAll('.msg.msg-ai');
                        target = all[all.length - 1];
                    }
                    renderScopeFooter(target, msg.value || {});
                    break;
                }
                case 'lessonCandidate':
                    renderLessonCandidate(msg.value || {});
                    break;
                case 'chronicleAutoRecordStatus': {
                    // v2.2.70 — 자동 기록 on/off 상태 푸시. 도구 메뉴 토글과 records-line 라벨 갱신.
                    chronicleAutoEnabled = !!(msg.value && msg.value.enabled);
                    renderChronicleAutoToggle();
                    syncRecordsLine();
                    break;
                }
                case 'autoContinue':
                    statusLabel.innerText = msg.value; thinkingBar.classList.add('active');
                    if (msg.value.includes('Analyzing')) setStep('analyze');
                    if (msg.value.includes('Planning')) setStep('plan');
                    if (msg.value.includes('Executing')) setStep('execute');
                    setTimeout(() => { thinkingBar.classList.remove('active'); }, 3000);
                    break;
                case 'workflowStage': {
                    // [5-stage pipeline] 채팅 본문에 단계 메시지를 흘리는 대신, 사이드바 상단의
                    // 얇은 status strip 에만 한 줄로 표시한다. done=true 면 strip 을 닫는다.
                    const v = msg.value || {};
                    const step = String(v.step || '');
                    const text = String(v.message || '');
                    const done = !!v.done;
                    if (done || (!step && !text)) {
                        statusLabel.innerText = '';
                        thinkingBar.classList.remove('active');
                    } else {
                        const compact = step && text ? step + ' · ' + text : (step || text);
                        statusLabel.innerText = compact;
                        thinkingBar.classList.add('active');
                    }
                    break;
                }
                case 'agentsList':
                    agentSel.innerHTML = '<option value="none">No Agent</option>';
                    msg.value.forEach(a => {
                        const o = document.createElement('option'); o.value = a.path; o.innerText = a.name;
                        if (a.path === msg.selected) o.selected = true;
                        agentSel.appendChild(o);
                    });
                    if (msg.selected && msg.selected !== 'none') {
                        vscode.postMessage({ type: 'getAgentContent', path: msg.selected });
                    }
                    vscode.postMessage({ type: 'getKnowledgeScope', agentPath: msg.selected });
                    syncContextBar();
                    break;
                case 'companyStatus': {
                    const v = msg.value || {};
                    renderCompanyChip(!!v.enabled, v.summary || '');
                    renderScopeSeg(v.activePipelineId || null);
                    break;
                }
                case 'companyIntentDecision': {
                    // 1인 기업 모드에서 의도 분류기가 "이건 새 업무가 아님"으로
                    // 판정했을 때 화면에 작은 라벨 한 줄을 띄워, 사용자가 왜
                    // 파이프라인이 안 돌았는지 알 수 있게 한다. new_task로 갈
                    // 땐 라벨 없이 그냥 파이프라인 카드들이 떠서 명확하므로
                    // 별도 알림 필요 없다.
                    const v = msg.value || {};
                    const chatEl = document.getElementById('chat');
                    if (!chatEl) break;
                    const note = document.createElement('div');
                    note.className = 'company-intent-note';
                    note.innerHTML = `<span class="cin-label">${escAttr(v.label || '💬 대화')}</span>` +
                        (v.reason ? ` <span class="cin-reason">${escAttr(v.reason)}</span>` : '');
                    chatEl.appendChild(note);
                    chatEl.scrollTop = chatEl.scrollHeight;
                    break;
                }
                case 'pixelOfficeUpdate': {
                    // 새 path (officeSnapshot) 가 한 번이라도 도착했다면 옛 message 는 무시.
                    if (window.__officeSnapshotSeen) break;
                    if (typeof window.__pixelOfficeApply === 'function') {
                        window.__pixelOfficeApply(msg.value || {});
                    }
                    break;
                }
                case 'officeSnapshot': {
                    // refactor #E — mini view 도 OfficeSnapshot 수신.
                    // OfficeSnapshot 을 옛 {state, bubbles, config} payload 모양으로 변환 후
                    // 기존 __pixelOfficeApply 재사용. dual-mode 안전 전환.
                    window.__officeSnapshotSeen = true;
                    const snap = msg.value;
                    if (!snap || typeof window.__pixelOfficeApply !== 'function') break;
                    const roster = Array.isArray(snap.roster) ? snap.roster : [];
                    const active = (snap.activeAgentId && roster.find((a) => a.agentId === snap.activeAgentId)) || roster[0];
                    const phaseToStatus = (p) => {
                        if (p === 'awaiting-approval') return 'waiting_approval';
                        if (p === 'reporting') return 'done';
                        if (p === 'intake') return 'analyzing';
                        return p || 'idle';
                    };
                    const synthetic = {
                        agentId: snap.activeAgentId || (active && active.agentId) || 'main',
                        agentName: (active && active.agentName) || 'Agent',
                        status: (active && active.status) || phaseToStatus(snap.phase),
                        currentTask: snap.task && snap.task.goal,
                        currentStep: active && active.currentStep,
                        message: snap.activeAgentId || '',
                        recentLogs: (active && active.lastLog) ? [active.lastLog] : [],
                        progress: snap.pipeline ? (snap.pipeline.index / Math.max(1, snap.pipeline.stages.length)) : 0,
                        pipelineStages: snap.pipeline && snap.pipeline.stages,
                        needUserInput: (snap.awaiting && snap.awaiting.kind === 'clarification') ? snap.awaiting.questions : undefined,
                        awaitingApproval: (snap.awaiting && snap.awaiting.kind === 'approval') ? snap.awaiting.questions[0] : undefined,
                        requirementContract: snap.task,
                        updatedAt: snap.updatedAt,
                    };
                    window.__pixelOfficeApply({
                        state: synthetic,
                        bubbles: Array.isArray(snap.newBubbles) ? snap.newBubbles : [],
                        // config 는 그대로 유지 — snapshot 에는 enabled 만 함의적, 옛 cfg 가 살아있음.
                        config: undefined,
                    });
                    break;
                }
                case 'devilRebuttal': {
                    // Devil Agent (도현) 카드. main assistant 답변 직후 chat 하단에 한 장 추가.
                    // 사용자가 '재반박' 누르면 그 텍스트가 다음 user prompt 로 가서 main turn 한 번 더 돌고,
                    // 재반박-입장 hint 가 prompt 에 prepend 됨.
                    const v = msg.value || {};
                    const chatEl = document.getElementById('chat');
                    if (!chatEl) break;
                    const card = document.createElement('div');
                    card.className = 'devil-rebuttal-card';
                    const persona = String(v.persona || '도현');
                    const text = String(v.text || '');
                    card.innerHTML = `
                        <div class="devil-rebuttal-head">🎭 <strong>${escAttr(persona)}</strong>이(가) 반박합니다</div>
                        <div class="devil-rebuttal-body"></div>
                        <div class="devil-rebuttal-actions">
                            <button class="devil-reply">재반박</button>
                            <button class="devil-dismiss ghost">넘기기</button>
                        </div>`;
                    // body 는 textContent 로 안전하게.
                    card.querySelector('.devil-rebuttal-body').textContent = text;
                    chatEl.appendChild(card);
                    chatEl.scrollTop = chatEl.scrollHeight;
                    card.querySelector('.devil-dismiss').addEventListener('click', () => card.remove());
                    card.querySelector('.devil-reply').addEventListener('click', () => {
                        // 입력창에 prefix 채워서 사용자가 자기 반박 입력하게.
                        const input = document.getElementById('input');
                        if (input) {
                            input.value = `[${persona}의 반박에 답변] `;
                            input.focus();
                            // 커서 끝으로.
                            try { input.setSelectionRange(input.value.length, input.value.length); } catch {}
                        }
                        card.remove();
                    });
                    break;
                }
                case 'companyAlignmentCard': {
                    // Intent Alignment 카드. kind에 따라 4가지 모드:
                    //   - 'auto-proceed' : confidence high → 자동 진행 안내(읽기 전용)
                    //   - 'questions'    : 답해야 할 질문 + 채팅으로 답변 안내
                    //   - 'confirm'      : 질문 없음, ✅진행 / 🛑취소 버튼
                    //   - 'cancelled'    : 사용자가 취소 — 카드 한 줄로 종료 표시
                    const v = msg.value || {};
                    const chatEl = document.getElementById('chat');
                    if (!chatEl) break;
                    const card = document.createElement('div');
                    card.className = 'company-alignment-card';
                    if (v.kind === 'cancelled') {
                        card.classList.add('cancelled');
                        card.innerHTML = '<div class="cph-meta">🛑 의도 정렬 취소됨 — 작업이 시작되지 않았습니다</div>';
                        chatEl.appendChild(card);
                        chatEl.scrollTop = chatEl.scrollHeight;
                        break;
                    }
                    const c = v.contract || {};
                    const kindLabel = v.kind === 'auto-proceed' ? '✅ 의도 분석 완료 — 곧장 진행'
                        : v.kind === 'confirm' ? '🧭 요청 정리 — 확인 후 진행'
                        : '🤔 추가 정보 필요';
                    const head = document.createElement('div');
                    head.className = 'cph-head';
                    head.innerHTML = `<strong>${escAttr(kindLabel)}</strong>`;
                    if (typeof v.roundsAsked === 'number' && typeof v.roundsLimit === 'number') {
                        const meta = document.createElement('span');
                        meta.className = 'cph-meta';
                        meta.style.marginLeft = '8px';
                        meta.textContent = `라운드 ${v.roundsAsked + 1}/${v.roundsLimit}`;
                        head.appendChild(meta);
                    }
                    card.appendChild(head);

                    // ── C-G-C-F summary block ──
                    const summary = document.createElement('div');
                    summary.className = 'cal-summary';
                    const dl = (label, val) => {
                        const row = document.createElement('div');
                        row.className = 'cal-row';
                        row.innerHTML = `<span class="cal-key">${escAttr(label)}</span><span class="cal-val">${val ? fmt(val) : '<em>(미정)</em>'}</span>`;
                        return row;
                    };
                    summary.appendChild(dl('맥락', c.context));
                    summary.appendChild(dl('목표', c.goal));
                    if (Array.isArray(c.criteria) && c.criteria.length > 0) {
                        const ul = c.criteria.map((x) => `- ${x}`).join('\n');
                        summary.appendChild(dl('기준', ul));
                    } else {
                        summary.appendChild(dl('기준', ''));
                    }
                    summary.appendChild(dl('형식', c.format));
                    card.appendChild(summary);

                    // ── 자가 조사로 해결된 항목 — 묻기 전에 두뇌에서 스스로 찾은 답 ──
                    const SELF_RESEARCH_PREFIX = '(자가 조사로 두뇌에서 확인) ';
                    if (Array.isArray(c.answeredQuestions)) {
                        const selfResearched = c.answeredQuestions
                            .filter((qa) => qa && typeof qa.a === 'string' && qa.a.startsWith(SELF_RESEARCH_PREFIX))
                            .slice(0, 3);
                        if (selfResearched.length > 0) {
                            const srBlock = document.createElement('div');
                            srBlock.className = 'cal-self-research';
                            const srHead = document.createElement('div');
                            srHead.className = 'cal-q-head';
                            srHead.textContent = '🔎 스스로 확인한 정보 (두뇌 검색):';
                            srBlock.appendChild(srHead);
                            const srUl = document.createElement('ul');
                            for (const qa of selfResearched) {
                                const li = document.createElement('li');
                                const ans = qa.a.slice(SELF_RESEARCH_PREFIX.length);
                                li.textContent = `${qa.q} → ${ans.length > 150 ? ans.slice(0, 150) + '…' : ans}`;
                                srUl.appendChild(li);
                            }
                            srBlock.appendChild(srUl);
                            card.appendChild(srBlock);
                        }
                    }

                    // ── 미해결 질문 ──
                    if (Array.isArray(c.openQuestions) && c.openQuestions.length > 0 && v.kind === 'questions') {
                        const qBlock = document.createElement('div');
                        qBlock.className = 'cal-questions';
                        const qHead = document.createElement('div');
                        qHead.className = 'cal-q-head';
                        qHead.textContent = '아래 질문에 한 메시지로 답해 주세요 (다 답해도, 일부만 답해도 OK):';
                        qBlock.appendChild(qHead);
                        const ul = document.createElement('ul');
                        for (const q of c.openQuestions) {
                            const li = document.createElement('li');
                            li.textContent = q;
                            ul.appendChild(li);
                        }
                        qBlock.appendChild(ul);
                        const hint = document.createElement('div');
                        hint.className = 'cal-hint';
                        hint.textContent = '답하지 않고 지금 그대로 시작하려면 아래 "그대로 진행" 버튼을 누르세요.';
                        qBlock.appendChild(hint);
                        card.appendChild(qBlock);
                    }

                    // ── 신뢰도 + 액션 버튼 ──
                    const confLabel = c.confidence === 'high' ? '신뢰도: high'
                        : c.confidence === 'medium' ? '신뢰도: medium' : '신뢰도: low';
                    const confEl = document.createElement('div');
                    confEl.className = 'cal-conf cal-conf-' + (c.confidence || 'low');
                    confEl.textContent = confLabel;
                    card.appendChild(confEl);

                    if (v.kind !== 'auto-proceed') {
                        const actions = document.createElement('div');
                        actions.className = 'cal-actions';
                        const proceedBtn = document.createElement('button');
                        proceedBtn.className = 'send-btn';
                        proceedBtn.textContent = '✅ 그대로 진행';
                        proceedBtn.onclick = () => {
                            vscode.postMessage({ type: 'respondCompanyAlignment', decision: 'proceed' });
                            actions.querySelectorAll('button').forEach((b) => b.disabled = true);
                            proceedBtn.textContent = '✅ 진행 중...';
                        };
                        const cancelBtn = document.createElement('button');
                        cancelBtn.className = 'secondary-btn';
                        cancelBtn.textContent = '🛑 취소';
                        cancelBtn.title = '이 작업을 시작하지 않음';
                        cancelBtn.onclick = () => {
                            vscode.postMessage({ type: 'respondCompanyAlignment', decision: 'cancel' });
                            actions.querySelectorAll('button').forEach((b) => b.disabled = true);
                            cancelBtn.textContent = '🛑 취소됨';
                        };
                        actions.appendChild(proceedBtn);
                        actions.appendChild(cancelBtn);
                        card.appendChild(actions);
                    }

                    chatEl.appendChild(card);
                    chatEl.scrollTop = chatEl.scrollHeight;
                    break;
                }
                case 'companyAgents': {
                    renderCompanyAgentCards(msg.value || {});
                    break;
                }
                case 'addCompanyAgentResult': {
                    const v = msg.value || {};
                    const errEl = document.getElementById('addAgentError');
                    const form = document.getElementById('addCompanyAgentForm');
                    if (v.ok) {
                        if (errEl) errEl.textContent = '';
                        // 폼 닫고 필드 초기화 — companyAgents 메시지가 목록을 다시 그려줌
                        if (form) form.setAttribute('data-open', 'false');
                        const fids = ['newAgentId','newAgentName','newAgentRole','newAgentEmoji','newAgentColor','newAgentTagline','newAgentSpecialty','newAgentPersona'];
                        for (const fid of fids) { const el = document.getElementById(fid); if (el) el.value = ''; }
                        showToast(`✅ '${v.agentId}' 에이전트 추가 완료`, 'info');
                    } else {
                        if (errEl) errEl.textContent = v.reason || '에이전트 추가 실패.';
                    }
                    break;
                }
                case 'deleteCompanyAgentResult': {
                    const v = msg.value || {};
                    if (v.ok) {
                        const msg2 = v.kind === 'builtin-hidden'
                            ? `🗑 기본 에이전트 '${v.agentId}' 숨김 처리 (목록 하단에서 복원 가능)`
                            : `🗑 '${v.agentId}' 에이전트 삭제됨`;
                        showToast(msg2, 'warn');
                    } else {
                        showToast(`삭제 실패: ${v.reason || '알 수 없음'}`, 'warn');
                    }
                    break;
                }
                case 'restoreHiddenAgentResult': {
                    const v = msg.value || {};
                    if (v.ok) {
                        showToast(`↩ '${v.agentId}' 에이전트 복원 완료`, 'info');
                    } else {
                        showToast(`복원 실패: ${v.reason || '알 수 없음'}`, 'warn');
                    }
                    break;
                }
                case 'companyPipelines': {
                    if (typeof window.__renderCompanyPipelines === 'function') {
                        window.__renderCompanyPipelines(msg.value || {});
                    }
                    break;
                }
                case 'companyResumable': {
                    if (typeof window.__renderCompanyResumable === 'function') {
                        window.__renderCompanyResumable(msg.value || {});
                    }
                    break;
                }
                case 'companyPipelineTemplateContent': {
                    const tpl = msg.value;
                    if (!tpl) { showToast('템플릿을 찾을 수 없습니다.', 'warn'); break; }
                    // 에디터를 미리 채워서 연다 — 사용자는 ID/name/지시문만 다듬으면 됨.
                    if (typeof window.__openPipelineEditorWithTemplate === 'function') {
                        window.__openPipelineEditorWithTemplate(tpl);
                    }
                    break;
                }
                case 'upsertCompanyPipelineResult': {
                    const v = msg.value || {};
                    const errEl = document.getElementById('pipelineEditError');
                    if (v.ok) {
                        if (errEl) errEl.textContent = '';
                        if (typeof window.__closePipelineEditor === 'function') window.__closePipelineEditor();
                        showToast('작업 흐름 저장 완료', 'info');
                    } else {
                        if (errEl) errEl.textContent = v.reason || '작업 흐름 저장 실패.';
                    }
                    break;
                }
                case 'deleteCompanyPipelineResult': {
                    const v = msg.value || {};
                    if (v.ok) showToast(`작업 흐름 '${v.pipelineId}' 삭제됨`, 'warn');
                    else showToast(`삭제 실패: ${v.reason || '알 수 없음'}`, 'warn');
                    break;
                }
                case 'setActiveCompanyPipelineResult': {
                    const v = msg.value || {};
                    if (v.ok) {
                        showToast(v.pipelineId ? `작업 흐름 '${v.pipelineId}' 사용 중` : '대표가 알아서 분배하도록 변경', 'info');
                    } else {
                        showToast(`활성 설정 실패: ${v.reason || '알 수 없음'}`, 'warn');
                    }
                    break;
                }
                case 'openCompanyManageOverlay': {
                    // Triggered by the Command Palette `Manage 1인 기업 Agents`.
                    document.getElementById('companyOverlay')?.classList.add('visible');
                    vscode.postMessage({ type: 'getCompanyAgents' });
                    vscode.postMessage({ type: 'getCompanyResumable' });
                    break;
                }
                case 'companyTurnUpdate': {
                    if (msg.value) renderCompanyPhase(msg.value);
                    break;
                }
                case 'architectureStatus': {
                    // Three-state chip:
                    //   active   — full info + Open/Refresh/Detach
                    //   inactive — name + [Attach] button (user previously detached, OR doc not yet generated)
                    //   hidden   — no project + no workspace
                    const chip = document.getElementById('archChip');
                    const title = document.getElementById('archChipTitle');
                    const meta = document.getElementById('archChipMeta');
                    if (!chip || !title || !meta) break;
                    const v = msg.value || {};
                    if (v.active) {
                        chip.setAttribute('data-state', 'active');
                        title.textContent = `${v.projectName || 'Project'} architecture`;
                        const updatedLabel = v.lastUpdated ? `updated ${formatRelativeTime(v.lastUpdated)}` : 'just attached';
                        const autoLabel = v.autoUpdate === false ? 'Auto-update Off' : 'Auto-update On';
                        meta.textContent = `${updatedLabel} · ${autoLabel}`;
                    } else if (v.canAttach && v.projectName) {
                        chip.setAttribute('data-state', 'inactive');
                        title.textContent = `${v.projectName} architecture`;
                        meta.textContent = v.detached ? 'detached — click Attach to re-enable' : 'not yet activated';
                    } else {
                        chip.setAttribute('data-state', 'hidden');
                    }
                    break;
                }
                case 'architectureRefreshFailed': {
                    const reason = msg.value && msg.value.reason;
                    if (reason === 'no-active-project') {
                        showToast('활성 프로젝트가 없습니다. 먼저 프로젝트를 선택하세요.', 'warn');
                    } else {
                        showToast('Architecture 갱신 실패', 'warn');
                    }
                    break;
                }
                case 'architectureRefreshResult': {
                    // Trust-building stats card: shows exactly what the
                    // refresh did so users don't have to guess whether the
                    // 0.1s click actually accomplished anything.
                    const v = msg.value || {};
                    const card = document.createElement('div');
                    card.className = 'arch-refresh-card';
                    const noChanges = (v.newlyAnalyzed | 0) === 0 && (v.deleted | 0) === 0;
                    if (noChanges) card.classList.add('no-changes');
                    const head = noChanges
                        ? `📋 ${escAttr(v.projectName || 'Project')} architecture — 변경 사항 없음`
                        : `📋 ${escAttr(v.projectName || 'Project')} architecture refreshed`;
                    const parts = [
                        `${v.newlyAnalyzed | 0} newly analysed`,
                        `${v.cached | 0} cached`,
                    ];
                    if ((v.deleted | 0) > 0) parts.push(`${v.deleted | 0} deleted`);
                    parts.push(`${v.durationMs | 0}ms`);
                    card.innerHTML =
                        `<div class="arc-head">${head}</div>` +
                        `<div class="arc-meta">${parts.join(' · ')}</div>`;
                    const chatEl = document.getElementById('chat');
                    if (chatEl) {
                        chatEl.appendChild(card);
                        chatEl.scrollTop = chatEl.scrollHeight;
                        setTimeout(() => {
                            card.classList.add('fading');
                            card.addEventListener('transitionend', () => card.remove(), { once: true });
                        }, 3000);
                    }
                    break;
                }
                case 'knowledgeMix': {
                    // Initial sync: reflect whatever weight is currently in settings.
                    if (msg.value && typeof msg.value.weight === 'number') {
                        const w = Math.max(0, Math.min(100, msg.value.weight));
                        const slider = document.getElementById('knowledgeMixSlider');
                        if (slider) slider.value = String(w);
                        const hint = document.getElementById('knowledgeMixHint');
                        if (hint) hint.textContent = fmtMixHint(w);
                    }
                    break;
                }
                case 'agentModelOverride': {
                    // The extension chose a different model than what the dropdowns show
                    // (per-agent pinned model). Reflect that in the UI without persisting
                    // it as the new global default — selecting a different agent or
                    // clearing the override should restore the previous selection.
                    const pinned = msg.value && msg.value.model;
                    if (pinned) {
                        const inlineSel = document.getElementById('modelInlineSel');
                        // Add an option if it isn't already known so the value can stick.
                        const ensureOption = (sel) => {
                            if (!sel) return;
                            const has = Array.from(sel.options).some(o => o.value === pinned);
                            if (!has) {
                                const o = document.createElement('option');
                                o.value = pinned;
                                o.innerText = `${pinned} (agent)`;
                                sel.appendChild(o);
                            }
                            sel.value = pinned;
                        };
                        ensureOption(modelSel);
                        ensureOption(inlineSel);
                        // The pill shows the model directly; surface the override as a tooltip
                        // instead of a duplicate status string.
                        if (inlineSel) inlineSel.title = `Model pinned by current agent: ${pinned}`;
                    }
                    break;
                }
                case 'agentMapData':
                    if (msg.value) {
                        agentMapDraft = {
                            agentPath: agentMapDraft.agentPath,
                            name: agentMapDraft.name,
                            knowledgeFolders: Array.isArray(msg.value.knowledgeFolders) ? msg.value.knowledgeFolders.slice() : [],
                            skillFolders: Array.isArray(msg.value.skillFolders) ? msg.value.skillFolders.slice() : [],
                            model: typeof msg.value.model === 'string' ? msg.value.model : '',
                            secondBrainWeight: (typeof msg.value.secondBrainWeight === 'number'
                                && Number.isFinite(msg.value.secondBrainWeight))
                                ? Math.max(0, Math.min(100, Math.round(msg.value.secondBrainWeight)))
                                : null,
                        };
                        renderAgentMapLists();
                        refreshAgentMapModelOptions();
                        syncAgentMapMixUi();
                        agentMapStatus.textContent = msg.value.exists ? '' : '새 매핑입니다. 저장하면 생성됩니다.';
                        agentMapStatus.className = 'map-status';
                    }
                    break;
                case 'pickedPath':
                    if (msg.value && msg.value.path && agentMapOverlay.classList.contains('visible')) {
                        const target = (msg.value.kind === 'knowledgeFolder')
                            ? agentMapDraft.knowledgeFolders
                            : agentMapDraft.skillFolders;
                        if (!target.includes(msg.value.path)) {
                            target.push(msg.value.path);
                            renderAgentMapLists();
                        }
                    }
                    break;
                case 'agentMapSaved':
                    if (msg.value && msg.value.ok) {
                        agentMapStatus.className = 'map-status ok';
                        agentMapStatus.textContent = '저장되었습니다.';
                        vscode.postMessage({ type: 'getKnowledgeScope', agentPath: agentMapDraft.agentPath });
                        setTimeout(closeAgentMapModal, 700);
                    } else {
                        agentMapStatus.className = 'map-status error';
                        agentMapStatus.textContent = '저장 실패: ' + (msg.value?.error || '알 수 없는 오류');
                    }
                    break;
                case 'knowledgeScope':
                    if (knowledgeScopeSel) {
                        knowledgeScopeSel.innerHTML = '';
                        const folders = (msg.value && msg.value.folders) || [];
                        if (folders.length === 0) {
                            const o = document.createElement('option');
                            o.value = '';
                            const label = (msg.value && msg.value.agent)
                                ? `매핑된 폴더 없음 (agent: ${msg.value.agent})`
                                : '매핑 없음 — 전체 브레인 검색';
                            o.innerText = label;
                            knowledgeScopeSel.appendChild(o);
                            knowledgeScopeSel.disabled = true;
                        } else {
                            knowledgeScopeSel.disabled = false;
                            folders.forEach(f => {
                                const o = document.createElement('option');
                                o.value = f.absolute;
                                o.innerText = f.relative || f.absolute;
                                o.title = f.absolute;
                                knowledgeScopeSel.appendChild(o);
                            });
                        }
                    }
                    break;
                case 'chronicleProjects':
                    designerSel.innerHTML = '';
                    msg.value.projects.forEach(p => {
                        const o = document.createElement('option');
                        o.value = p.id;
                        o.innerText = p.name;
                        o.title = p.recordRoot;
                        if (p.id === msg.value.activeProjectId) o.selected = true;
                        designerSel.appendChild(o);
                    });
                    const newDesignerOpt = document.createElement('option');
                    newDesignerOpt.value = 'new';
                    newDesignerOpt.innerText = '+ Add Designer Project...';
                    designerSel.appendChild(newDesignerOpt);
                    syncContextBar();
                    vscode.postMessage({ type: 'getChronicleRecords' });
                    break;
                case 'chronicleRecords':
                    chronicleRecordSel.innerHTML = '';
                    if (!msg.value || msg.value.length === 0) {
                        const emptyRecordOpt = document.createElement('option');
                        emptyRecordOpt.value = '';
                        emptyRecordOpt.innerText = 'No records yet';
                        chronicleRecordSel.appendChild(emptyRecordOpt);
                        syncRecordsLine();
                        break;
                    }
                    msg.value.forEach(record => {
                        const o = document.createElement('option');
                        o.value = record.path;
                        o.innerText = record.relativePath;
                        o.title = record.path;
                        chronicleRecordSel.appendChild(o);
                    });
                    syncRecordsLine();
                    break;
                case 'agentContent':
                    agentPrompt.value = msg.value;
                    negativePrompt.value = msg.negativePrompt || '';
                    break;
                case 'agentDeleted':
                    agentConfigPanel.style.display = 'none';
                    editMode = false;
                    editAgentBtn.classList.remove('active');
                    agentPrompt.value = '';
                    negativePrompt.value = '';
                    break;
                case 'error':
                    thinkingBar.classList.remove('active'); sendBtn.disabled = false;
                    addMsg(msg.value, 'error');
                    break;
                case 'lmStudioError':
                    showToast('LM Studio: ' + msg.value, 'warn');
                    break;
                case 'requiresApproval':
                    const box = document.createElement('div');
                    box.className = 'approval-box';
                    box.innerHTML = '<div class="approval-title"><span>🛡️</span> 작업 승인 대기 중 (Action Approval Required)</div>' +
                                    '<div style="font-size: 11px; color: var(--text-dim); margin-bottom: 8px;">위의 변경 사항을 프로젝트에 반영할까요?</div>' +
                                    '<div class="approval-btns">' +
                                    '    <button class="btn-approve" onclick="approve()">승인 (Approve)</button>' +
                                    '    <button class="btn-reject" onclick="reject()">롤백 (Rollback)</button>' +
                                    '</div>';
                    chat.appendChild(box);
                    chat.scrollTop = chat.scrollHeight;
                    break;
            }
        });

        function renderAttachments() {
            attachPreview.innerHTML = '';
            if (pendingFiles.length === 0) { attachPreview.classList.remove('visible'); return; }
            attachPreview.classList.add('visible');
            pendingFiles.forEach((f, i) => {
                const chip = document.createElement('div'); chip.className = 'file-chip';
                chip.innerHTML = `<span>📎</span> ${f.name} <span class="remove" onclick="removeFile(${i})">✕</span>`;
                attachPreview.appendChild(chip);
            });
        }
        window.removeFile = (i) => {
            pendingFiles.splice(i, 1);
            renderAttachments();
            // 파일 삭제 후 Draft State 재평가
            setDraftActive(input.value.trim().length > 0 || pendingFiles.length > 0);
        };
        function processFiles(files) {
            if (!files || files.length === 0) return;
            
            Array.from(files).forEach(file => {
                const reader = new FileReader();
                reader.onload = () => {
                    const base64 = reader.result.split(',')[1];
                    pendingFiles.push({ name: file.name, type: file.type, data: base64 });
                    renderAttachments();
                    setDraftActive(true);
                };
                reader.readAsDataURL(file);
            });
            showToast(`${files.length}개의 파일이 추가되었습니다.`, 'success');
            Sound.success();
        }

        attachBtn.onclick = () => fileInput.click();
        fileInput.onchange = () => {
            processFiles(fileInput.files);
            fileInput.value = '';
        };

        // --- Drag and Drop Implementation ---
        ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
            document.body.addEventListener(eventName, e => {
                e.preventDefault();
                e.stopPropagation();
            }, false);
        });

        ['dragenter', 'dragover'].forEach(eventName => {
            document.body.addEventListener(eventName, () => {
                document.body.classList.add('drag-over');
            }, false);
        });

        ['dragleave', 'drop'].forEach(eventName => {
            document.body.addEventListener(eventName, () => {
                document.body.classList.remove('drag-over');
            }, false);
        });

        document.body.addEventListener('drop', e => {
            const dt = e.dataTransfer;
            const files = dt.files;

            // ⭐ Kodari PD 가이드 반영: Input 요소의 상태를 드롭된 파일로 강제 동기화
            if (files && files.length > 0) {
                fileInput.files = files; // Input의 files 속성 업데이트
                console.log(`✅ [DnD] Input 상태 동기화 성공: ${files[0].name} 외 ${files.length - 1}개`);
            }
            
            processFiles(files);
        }, false);

        function send() {
            const val = input.value.trim();
            if (!val && pendingFiles.length === 0) return;
            addMsg(val || (pendingFiles.length > 0 ? `[Sent ${pendingFiles.length} files]` : ''), 'user');
            vscode.postMessage({
                type: 'prompt',
                value: val,
                model: modelSel.value,
                files: pendingFiles.length > 0 ? pendingFiles : undefined,
                agentFile: agentSel.value === 'none' ? undefined : agentSel.value,
                brainProfileId: brainSel.value && brainSel.value !== 'new' ? brainSel.value : undefined,
                negativePrompt: negativePrompt.value.trim() || undefined,
                secondBrainTrace: secondBrainTraceEnabled,
                secondBrainTraceDebug
            });
            input.value = ''; input.style.height = 'auto'; pendingFiles = []; renderAttachments();
            // 전송 완료 후 Draft State 리셋 + Stop 버튼 표시
            setDraftActive(false);
            setGenerating(true);
            thinkingBar.classList.add('active');
            
            // Save state after sending
            const currentState = vscode.getState() || { history: [] };
            currentState.history.push({ role: 'user', content: val });
            saveWebviewState(currentState.history);
        }

        sendBtn.onclick = send;
        input.addEventListener('keydown', e => { 
            if (e.key === 'Enter' && !e.shiftKey) { 
                if (e.isComposing) return;
                e.preventDefault(); 
                send(); 
            } 
        });
        let _lastActivityBump = 0;
        const ACTIVITY_BUMP_INTERVAL_MS = 5000;
        const bumpActivity = () => {
            const now = Date.now();
            if (now - _lastActivityBump < ACTIVITY_BUMP_INTERVAL_MS) return;
            _lastActivityBump = now;
            vscode.postMessage({ type: 'activity' });
        };
        input.addEventListener('input', () => {
            input.style.height = 'auto';
            input.style.height = input.scrollHeight + 'px';
            // Draft State: 내용이 있으면 cancelBtn 표시
            setDraftActive(input.value.trim().length > 0 || pendingFiles.length > 0);
            bumpActivity();
            // Slash 자동완성 dropdown — 첫 단어가 `/` 면 후보 표시.
            updateSlashSuggest();
        });

        // ── Slash command autocomplete ──────────────────────────────────────
        // 등록된 slash 명령 (extension 측 listSlashCommands() 결과). webview ready
        // 시 한 번 전달받아 캐싱.
        let _slashCommands = []; // [{ name, description }]
        let _slashActiveIdx = -1;
        const slashSuggestEl = document.getElementById('slashSuggest');

        function setSlashCommands(cmds) {
            _slashCommands = Array.isArray(cmds) ? cmds.slice().sort((a, b) => a.name.localeCompare(b.name)) : [];
        }

        function hideSlashSuggest() {
            if (slashSuggestEl) {
                slashSuggestEl.style.display = 'none';
                slashSuggestEl.innerHTML = '';
            }
            _slashActiveIdx = -1;
        }

        function getCurrentSlashHead() {
            // input 의 첫 단어가 `/` 로 시작할 때만 자동완성 활성. arg 입력 중에는 닫음.
            const v = input.value;
            const trimmed = v.trimStart();
            if (!trimmed.startsWith('/')) return null;
            const spaceIdx = trimmed.indexOf(' ');
            // 공백이 있으면 사용자가 이미 명령 입력 끝내고 arg 치는 중 — dropdown 닫음.
            if (spaceIdx !== -1) return null;
            // 커서가 첫 단어 안에 있는지 확인 (입력 중간 편집 시 false-positive 방지).
            const caret = input.selectionStart ?? v.length;
            const leading = v.length - v.trimStart().length;
            if (caret > leading + trimmed.length) return null;
            return trimmed.toLowerCase();
        }

        function renderSlashSuggest(matched) {
            if (!slashSuggestEl) return;
            if (matched.length === 0) {
                slashSuggestEl.innerHTML = '<div class="ss-empty">일치하는 명령 없음</div>';
            } else {
                slashSuggestEl.innerHTML = matched.map((c, i) => {
                    const cls = i === _slashActiveIdx ? 'ss-item active' : 'ss-item';
                    const desc = (c.description || '').replace(/[<>&]/g, ch => ({'<':'&lt;','>':'&gt;','&':'&amp;'}[ch]));
                    return `<div class="${cls}" data-idx="${i}" data-name="${c.name}">` +
                        `<span class="ss-name">${c.name}</span>` +
                        `<span class="ss-desc">${desc}</span>` +
                        `</div>`;
                }).join('');
                // 클릭으로 선택.
                slashSuggestEl.querySelectorAll('.ss-item').forEach(el => {
                    el.addEventListener('mousedown', e => {
                        e.preventDefault(); // textarea blur 막음
                        applySlashSuggest(el.dataset.name);
                    });
                });
            }
            slashSuggestEl.style.display = 'block';
        }

        function updateSlashSuggest() {
            const head = getCurrentSlashHead();
            if (head === null) { hideSlashSuggest(); return; }
            const matched = _slashCommands.filter(c => c.name.toLowerCase().startsWith(head));
            if (_slashActiveIdx >= matched.length) _slashActiveIdx = matched.length - 1;
            if (_slashActiveIdx < 0 && matched.length > 0) _slashActiveIdx = 0;
            renderSlashSuggest(matched);
        }

        function applySlashSuggest(cmdName) {
            // input 의 첫 단어를 cmdName 으로 치환 + 공백 추가 (arg 입력 시작 친화적).
            const v = input.value;
            const leading = v.length - v.trimStart().length;
            const head = v.trimStart().split(/\s+/, 1)[0];
            input.value = ' '.repeat(leading) + cmdName + ' ' + v.slice(leading + head.length).trimStart();
            // textarea 높이 갱신 + 커서 명령 끝으로 이동.
            input.style.height = 'auto';
            input.style.height = input.scrollHeight + 'px';
            const newCaret = leading + cmdName.length + 1;
            input.setSelectionRange(newCaret, newCaret);
            hideSlashSuggest();
            input.focus();
            setDraftActive(true);
        }

        // 키보드 네비게이션 — dropdown 표시 중일 때만 적용.
        input.addEventListener('keydown', (e) => {
            if (!slashSuggestEl || slashSuggestEl.style.display === 'none') return;
            const items = slashSuggestEl.querySelectorAll('.ss-item');
            if (items.length === 0) return;
            if (e.key === 'ArrowDown') {
                e.preventDefault();
                _slashActiveIdx = (_slashActiveIdx + 1) % items.length;
                updateSlashSuggest();
            } else if (e.key === 'ArrowUp') {
                e.preventDefault();
                _slashActiveIdx = (_slashActiveIdx - 1 + items.length) % items.length;
                updateSlashSuggest();
            } else if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                if (_slashActiveIdx >= 0 && _slashActiveIdx < items.length) {
                    applySlashSuggest(items[_slashActiveIdx].dataset.name);
                }
            } else if (e.key === 'Tab') {
                e.preventDefault();
                if (_slashActiveIdx >= 0 && _slashActiveIdx < items.length) {
                    applySlashSuggest(items[_slashActiveIdx].dataset.name);
                }
            } else if (e.key === 'Escape') {
                e.preventDefault();
                hideSlashSuggest();
            }
        });

        // input blur 시 살짝 delay 두고 닫음 (dropdown mousedown 먼저 처리되게).
        input.addEventListener('blur', () => {
            setTimeout(() => hideSlashSuggest(), 150);
        });

        cancelBtn.onclick = () => clearDraft();
        stopBtn.onclick = () => {
            vscode.postMessage({ type: 'stopGeneration' });
            setGenerating(false);
            thinkingBar.classList.remove('active');
            showToast('■ 생성이 중단되었습니다.', 'warn');
            Sound.warn();
        };

        const startNewChat = () => { Sound.play(660, 'sine', 0.1); vscode.postMessage({ type: 'newChat' }); };
        document.getElementById('newChatBtn').onclick = startNewChat;
        // Note: input-footer "New Chat" / "Sync Knowledge" buttons were removed.
        // Both actions remain available in the top toolbar (newChatBtn / brainBtn / Tools menu).

        document.getElementById('settingsBtn').onclick = () => vscode.postMessage({ type: 'openSettings' });
        document.getElementById('brainTraceBtn').onclick = () => {
            secondBrainTraceEnabled = !secondBrainTraceEnabled;
            const btn = document.getElementById('brainTraceBtn');
            btn.classList.toggle('active', secondBrainTraceEnabled);
            btn.setAttribute('data-tooltip', secondBrainTraceEnabled ? 'Second Brain Trace Mode: On' : 'Second Brain Trace Mode: Off');
            saveUiState();
        };
        document.getElementById('brainTraceDebugBtn').onclick = () => {
            secondBrainTraceDebug = !secondBrainTraceDebug;
            const btn = document.getElementById('brainTraceDebugBtn');
            btn.classList.toggle('active', secondBrainTraceDebug);
            btn.setAttribute('data-tooltip', secondBrainTraceDebug ? 'Second Brain Debug JSON: On' : 'Second Brain Debug JSON: Off');
            saveUiState();
        };
        // v2.2.70 — 자동 기록 토글. 클릭 시 즉시 서버에 새 상태 전송. 서버가 config 저장 후
        // chronicleAutoRecordStatus 메시지로 다시 푸시 → renderChronicleAutoToggle 가 UI 동기화.
        const _chronicleAutoBtn = document.getElementById('chronicleAutoRecordBtn');
        if (_chronicleAutoBtn) {
            _chronicleAutoBtn.onclick = () => {
                const next = !chronicleAutoEnabled;
                // 낙관적 갱신 — 서버 응답 전에도 즉시 라벨이 바뀌어 클릭 반응성이 좋다. 응답이 오면 다시 정정.
                chronicleAutoEnabled = next;
                renderChronicleAutoToggle();
                syncRecordsLine();
                vscode.postMessage({ type: 'setChronicleAutoRecord', enabled: next });
            };
        }

        const syncBrain = () => { Sound.play(550, 'sine', 0.1); vscode.postMessage({ type: 'syncBrain' }); };
        document.getElementById('brainBtn').onclick = syncBrain;
        saveWikiRawBtn.onclick = () => vscode.postMessage({ type: 'saveWikiRaw' });
        addBrainBtn.onclick = () => vscode.postMessage({ type: 'addBrain' });
        // v2.2.67 — 만약 dropdown 이 'new' 상태로 굳어있어도 수정/삭제는 직전 유효 선택(또는 첫 실제 옵션)
        // 을 기준으로 동작하도록 폴백. 이전엔 'new' 인 순간 그냥 early-return 해서 "수정 안 됨" 버그 발생.
        function _resolveActiveBrainId() {
            if (brainSel.value && brainSel.value !== 'new') return brainSel.value;
            const last = brainSel.dataset.lastSelected;
            if (last && last !== 'new') return last;
            for (const opt of brainSel.options) {
                if (opt.value && opt.value !== 'new') return opt.value;
            }
            return '';
        }
        editBrainBtn.onclick = () => {
            const id = _resolveActiveBrainId();
            if (!id) return;
            vscode.postMessage({ type: 'editBrain', id });
        };
        deleteBrainBtn.onclick = () => {
            const id = _resolveActiveBrainId();
            if (!id) return;
            vscode.postMessage({ type: 'deleteBrain', id });
        };
        // (inputSyncBtn removed — Sync Knowledge is reachable via the top brainBtn / Tools menu.)
        document.getElementById('historyBtn').onclick = () => vscode.postMessage({ type: 'getSessions' });
        document.getElementById('historyBtn').addEventListener('click', () => historyOverlay.classList.add('visible'));
        document.getElementById('closeHistoryBtn').onclick = () => historyOverlay.classList.remove('visible');
        // The input placeholder is now a constant brand label — the model name
        // lives in the footer pill itself, so we don't repeat it here.
        const updateInputPlaceholder = () => {
            if (typeof input !== 'undefined' && input) input.placeholder = 'Ask Astra...';
        };

        // Shared handler so the header dropdown and the footer pill dropdown
        // always commit the same way and stay visually synced.
        const applyModelSelection = (selectedModel, originEl) => {
            if (!selectedModel) return;
            // [State Persistence - Tier 2] 모델 변경 시 LocalStorage에 즉시 저장 (클라이언트 측 지속성)
            try {
                localStorage.setItem('g1nation_last_model', selectedModel);
            } catch(e) {
                console.warn('[Astra] LocalStorage 저장 실패:', e);
            }
            // [State Persistence - Tier 1] VS Code 전역 설정에 동기화 (영구 저장)
            vscode.postMessage({ type: 'model', value: selectedModel });
            // Mirror the value to the *other* dropdown so both pickers reflect reality.
            const inlineSel = document.getElementById('modelInlineSel');
            if (originEl !== modelSel && modelSel.value !== selectedModel) modelSel.value = selectedModel;
            if (inlineSel && originEl !== inlineSel && inlineSel.value !== selectedModel) inlineSel.value = selectedModel;
        };
        modelSel.onchange = () => applyModelSelection(modelSel.value, modelSel);
        const _inlineModelSelEl = document.getElementById('modelInlineSel');
        if (_inlineModelSelEl) {
            _inlineModelSelEl.onchange = () => applyModelSelection(_inlineModelSelEl.value, _inlineModelSelEl);
        }
        brainSel.onchange = () => {
            if (brainSel.value === 'new') {
                // v2.2.67 — '+ Add New Brain...' 클릭 시 addBrain 메시지를 보내고, 사용자가 폴더 선택을
                // 취소해도 dropdown 이 'new' 로 굳지 않도록 즉시 직전 유효 선택으로 되돌린다. 추가가 실제로
                // 성공하면 _postBrainProfiles → brainProfiles 메시지가 새 brain 으로 다시 옮긴다.
                // 이 복원이 없으면 brainSel.value === 'new' 상태가 유지되어 수정/삭제 버튼이 early-return 으로 죽는다.
                const prev = brainSel.dataset.lastSelected;
                vscode.postMessage({ type: 'addBrain' });
                if (prev && prev !== 'new') {
                    brainSel.value = prev;
                    syncContextBar();
                }
            } else {
                brainSel.dataset.lastSelected = brainSel.value;
                vscode.postMessage({ type: 'setBrainProfile', id: brainSel.value });
            }
        };

        designerSel.onchange = () => {
            if (designerSel.value === 'new') {
                vscode.postMessage({ type: 'createChronicleProject' });
            } else {
                vscode.postMessage({ type: 'setChronicleProject', id: designerSel.value });
                vscode.postMessage({ type: 'getChronicleRecords' });
            }
        };

        agentSel.onchange = () => {
            if (agentSel.value !== 'none') {
                vscode.postMessage({ type: 'getAgentContent', path: agentSel.value });
                // [State Persistence Fix] 에이전트 선택값을 즉시 백엔드에 저장
                vscode.postMessage({ type: 'saveAgentSelection', path: agentSel.value });
                if (editMode) agentConfigPanel.style.display = 'flex';
            } else {
                agentConfigPanel.style.display = 'none';
                editMode = false;
                editAgentBtn.classList.remove('active');
                agentPrompt.value = '';
                negativePrompt.value = '';
                // [State Persistence Fix] 에이전트 해제도 즉시 저장
                vscode.postMessage({ type: 'saveAgentSelection', path: 'none' });
            }
            vscode.postMessage({ type: 'getKnowledgeScope', agentPath: agentSel.value });
        };

        if (editKnowledgeMapBtn) {
            editKnowledgeMapBtn.onclick = () => openAgentMapModal();
        }
        if (reloadKnowledgeMapBtn) {
            reloadKnowledgeMapBtn.onclick = () => vscode.postMessage({ type: 'getKnowledgeScope', agentPath: agentSel.value });
        }
        if (closeAgentMapBtn) closeAgentMapBtn.onclick = closeAgentMapModal;
        if (cancelAgentMapBtn) cancelAgentMapBtn.onclick = closeAgentMapModal;
        if (editAgentMapJsonBtn) {
            editAgentMapJsonBtn.onclick = () => vscode.postMessage({ type: 'editKnowledgeMap' });
        }
        if (addKnowledgeFolderBtn) {
            addKnowledgeFolderBtn.onclick = () => vscode.postMessage({ type: 'pickPath', kind: 'knowledgeFolder' });
        }
        if (addSkillFolderBtn) {
            addSkillFolderBtn.onclick = () => vscode.postMessage({ type: 'pickPath', kind: 'skillFolder' });
        }
        if (addSkillFileBtn) {
            addSkillFileBtn.onclick = () => vscode.postMessage({ type: 'pickPath', kind: 'skillFile' });
        }
        if (saveAgentMapBtn) {
            saveAgentMapBtn.onclick = () => {
                agentMapStatus.className = 'map-status';
                agentMapStatus.textContent = '저장 중...';
                vscode.postMessage({
                    type: 'saveAgentMap',
                    agentPath: agentMapDraft.agentPath,
                    knowledgeFolders: agentMapDraft.knowledgeFolders,
                    skillFolders: agentMapDraft.skillFolders,
                    // Empty string = "Use current model" (override removed).
                    model: agentMapDraft.model || '',
                    // null = "Use global setting" (override removed); number 0–100 = pinned.
                    secondBrainWeight: agentMapDraft.secondBrainWeight,
                });
            };
        }
        // Track changes to the per-agent model dropdown so the draft stays in sync.
        const _agentMapModelSelEl = document.getElementById('agentMapModelSel');
        if (_agentMapModelSelEl) {
            _agentMapModelSelEl.onchange = () => {
                agentMapDraft.model = _agentMapModelSelEl.value || '';
            };
        }
        // ── Per-agent Knowledge Mix slider + "Use global" checkbox ────────────
        const _agentMapMixCb = document.getElementById('agentMapMixUseGlobal');
        const _agentMapMixSlider = document.getElementById('agentMapMixSlider');
        if (_agentMapMixCb && _agentMapMixSlider) {
            _agentMapMixCb.addEventListener('change', () => {
                if (_agentMapMixCb.checked) {
                    agentMapDraft.secondBrainWeight = null;
                } else {
                    // Snap to whatever the slider currently shows so the user has a starting point.
                    agentMapDraft.secondBrainWeight = parseInt(_agentMapMixSlider.value, 10) || 50;
                }
                syncAgentMapMixUi();
            });
            _agentMapMixSlider.addEventListener('input', () => {
                if (_agentMapMixCb.checked) return; // disabled state, but guard anyway
                const w = Math.max(0, Math.min(100, parseInt(_agentMapMixSlider.value, 10) || 50));
                agentMapDraft.secondBrainWeight = w;
                const hint = document.getElementById('agentMapMixHint');
                if (hint) hint.textContent = fmtMixHint(w);
            });
        }

        editAgentBtn.onclick = () => {
            if (agentSel.value === 'none') return;
            editMode = !editMode;
            editAgentBtn.classList.toggle('active', editMode);
            agentConfigPanel.style.display = editMode ? 'flex' : 'none';
        };

        updateAgentBtn.onclick = () => {
            if (agentSel.value !== 'none') {
                vscode.postMessage({ 
                    type: 'updateAgent', 
                    path: agentSel.value, 
                    content: agentPrompt.value,
                    negativePrompt: negativePrompt.value.trim()
                });
            }
        };

        addAgentBtn.onclick = () => vscode.postMessage({ type: 'createAgent' });
        deleteAgentBtn.onclick = () => {
            if (agentSel.value === 'none') return;
            vscode.postMessage({ type: 'deleteAgent', path: agentSel.value });
        };

        document.getElementById('addDesignerBtn').onclick = () => vscode.postMessage({ type: 'createChronicleProject' });
        document.getElementById('openDesignerBtn').onclick = () => vscode.postMessage({ type: 'openChronicleFolder' });
        document.getElementById('refreshChronicleRecordsBtn').onclick = () => vscode.postMessage({ type: 'getChronicleRecords' });
        document.getElementById('openChronicleRecordBtn').onclick = () => {
            if (!chronicleRecordSel.value) return;
            vscode.postMessage({ type: 'openChronicleRecord', path: chronicleRecordSel.value });
        };

        // ── Header dropdowns (Tools ▾ / Edit ▾ / Records ▾) ──────────────────
        function closeAllDropdowns(except) {
            document.querySelectorAll('.hdr-menu.open').forEach(m => { if (m !== except) m.classList.remove('open'); });
        }
        document.querySelectorAll('[data-dd]').forEach(dd => {
            const trigger = dd.querySelector('[data-dd-trigger]');
            const menu = dd.querySelector('[data-dd-menu]');
            if (!trigger || !menu) return;
            trigger.addEventListener('click', e => {
                e.stopPropagation();
                const willOpen = !menu.classList.contains('open');
                closeAllDropdowns(menu);
                menu.classList.toggle('open', willOpen);
            });
            // Clicks inside the menu shouldn't bubble to the document (which would close it). A click
            // on a <button> means "I picked something" → close after its own handler runs; a <select>
            // (or label/spacer) keeps the menu open so the user can change several things.
            menu.addEventListener('click', e => {
                e.stopPropagation();
                if (e.target && e.target.closest && e.target.closest('button')) {
                    setTimeout(() => menu.classList.remove('open'), 0);
                }
            });
        });
        document.addEventListener('click', () => closeAllDropdowns());

        // Keep the Context Bar / Records line in sync with the (now-collapsed) selectors.
        [brainSel, agentSel, designerSel].forEach(s => s && s.addEventListener('change', syncContextBar));
        if (chronicleRecordSel) chronicleRecordSel.addEventListener('change', syncRecordsLine);
        syncContextBar();
        syncRecordsLine();

        vscode.postMessage({ type: 'getModels' });
        vscode.postMessage({ type: 'getAgents' });
        vscode.postMessage({ type: 'getChronicleProjects' });
        vscode.postMessage({ type: 'getChronicleRecords' });
        vscode.postMessage({ type: 'getChronicleAutoRecord' });
        vscode.postMessage({ type: 'getKnowledgeMix' });
        vscode.postMessage({ type: 'getArchitectureStatus' });
        vscode.postMessage({ type: 'getCompanyStatus' });
        vscode.postMessage({ type: 'ready' });

        // ── Project Architecture chip buttons ─────────────────────────────────
        const _archOpenBtn = document.getElementById('archOpenBtn');
        const _archRefreshBtn = document.getElementById('archRefreshBtn');
        const _archDetachBtn = document.getElementById('archDetachBtn');
        if (_archOpenBtn) _archOpenBtn.onclick = () => vscode.postMessage({ type: 'openArchitectureDoc' });
        if (_archRefreshBtn) _archRefreshBtn.onclick = () => vscode.postMessage({ type: 'refreshArchitecture' });
        if (_archDetachBtn) _archDetachBtn.onclick = () => vscode.postMessage({ type: 'detachArchitecture' });
        // [Attach] is visible only in the inactive chip state; clicking it
        // re-enables architecture mode for the current workspace's project.
        const _archAttachBtn = document.getElementById('archAttachBtn');
        if (_archAttachBtn) _archAttachBtn.onclick = () => vscode.postMessage({ type: 'attachArchitecture' });

        // ── 1인 기업 (Company) Mode chip + manage overlay ─────────────────────
        // The chip itself toggles enabled/disabled. The ▾ button opens the
        // manage overlay where the user picks active agents + per-agent
        // model overrides. State round-trips through `companyStatus` /
        // `companyAgents` messages so the webview and extension stay in sync.
        const _companyChip = document.getElementById('companyChip');
        const _companyManageBtn = document.getElementById('companyManageBtn');
        const _companyOverlay = document.getElementById('companyOverlay');
        const _closeCompanyBtns = [
            document.getElementById('closeCompanyOverlayBtn'),
            document.getElementById('closeCompanyOverlayBtn2'),
        ].filter(Boolean);
        const _companyNameInput = document.getElementById('companyNameInput');
        const _saveCompanyNameBtn = document.getElementById('saveCompanyNameBtn');
        const _companyAgentList = document.getElementById('companyAgentList');
        const _companyStatusEl = document.getElementById('companyStatus');

        /**
         * Chip lives in the main header toolbar now, so it uses the same
         * `icon-btn.active` styling as `brainTraceBtn`. Detail (company name
         * + agent count) goes in the tooltip — the label stays a constant
         * "Corp" so the toolbar tone-and-manner isn't broken by a wildly
         * varying-width chip.
         */
        const renderCompanyChip = (active, summary) => {
            if (!_companyChip) return;
            _companyChip.classList.toggle('active', !!active);
            _companyChip.setAttribute(
                'data-tooltip',
                active
                    ? `1인 기업 ON · ${summary || ''}`.trim()
                    : '1인 기업 모드 OFF — 클릭해서 켜기',
            );
            // 스코프 프리셋 segmented control 도 기업 모드 ON 일 때만 노출.
            const scopeSeg = document.getElementById('companyScopeSeg');
            if (scopeSeg) scopeSeg.hidden = !active;
        };

        // 활성 pipeline 의 id 가 어느 SCOPE 프리셋의 suggestedPipelineId 와 매칭되는지로 active 표시.
        // companyStatus 메시지가 activePipelineId 를 보낼 때마다 호출.
        const SCOPE_PRESET_TO_PIPELINE_ID = {
            'plan-only': 'plan-only',
            'dev-only': 'dev-only',
            'full-product-dev': 'product-dev',
        };
        const renderScopeSeg = (activePipelineId) => {
            const scopeSeg = document.getElementById('companyScopeSeg');
            if (!scopeSeg) return;
            for (const btn of scopeSeg.querySelectorAll('.scope-seg-btn')) {
                const tplId = btn.getAttribute('data-scope');
                const expected = SCOPE_PRESET_TO_PIPELINE_ID[tplId];
                btn.classList.toggle('active', !!activePipelineId && activePipelineId === expected);
            }
        };
        // Wire up clicks once.
        const _scopeSeg = document.getElementById('companyScopeSeg');
        if (_scopeSeg && !_scopeSeg.dataset.wired) {
            _scopeSeg.dataset.wired = '1';
            _scopeSeg.addEventListener('click', (e) => {
                const btn = e.target && e.target.closest && e.target.closest('.scope-seg-btn');
                if (!btn) return;
                const tplId = btn.getAttribute('data-scope');
                if (!tplId) return;
                // Optimistic visual flip — backend ack 가 companyStatus 갱신으로 결과 확정.
                for (const b of _scopeSeg.querySelectorAll('.scope-seg-btn')) {
                    b.classList.toggle('active', b === btn);
                }
                vscode.postMessage({ type: 'setCompanyScopePreset', templateId: tplId });
            });
        }

        if (_companyChip) {
            _companyChip.onclick = () => {
                const isActive = _companyChip.classList.contains('active');
                // Optimistic flip — backend echoes the canonical state back.
                renderCompanyChip(!isActive, '');
                vscode.postMessage({ type: 'setCompanyEnabled', value: !isActive });
            };
        }
        if (_companyManageBtn) {
            _companyManageBtn.onclick = () => {
                if (!_companyOverlay) return;
                _companyOverlay.classList.add('visible');
                _companyStatusEl.textContent = '불러오는 중...';
                vscode.postMessage({ type: 'getCompanyAgents' });
                vscode.postMessage({ type: 'getCompanyPipelines' });
                vscode.postMessage({ type: 'getCompanyResumable' });
            };
        }
        for (const btn of _closeCompanyBtns) {
            btn.onclick = () => _companyOverlay?.classList.remove('visible');
        }
        if (_saveCompanyNameBtn && _companyNameInput) {
            _saveCompanyNameBtn.onclick = () => {
                vscode.postMessage({ type: 'setCompanyName', value: _companyNameInput.value });
            };
        }

        // ── Add-agent form: toggle, clear, submit ──
        const _addCompanyAgentBtn = document.getElementById('addCompanyAgentBtn');
        const _addAgentForm = document.getElementById('addCompanyAgentForm');
        const _cancelAddAgentBtn = document.getElementById('cancelAddAgentBtn');
        const _saveAddAgentBtn = document.getElementById('saveAddAgentBtn');
        const _addAgentError = document.getElementById('addAgentError');
        const _addAgentFields = () => ({
            id: document.getElementById('newAgentId'),
            name: document.getElementById('newAgentName'),
            role: document.getElementById('newAgentRole'),
            emoji: document.getElementById('newAgentEmoji'),
            color: document.getElementById('newAgentColor'),
            roleCategory: document.getElementById('newAgentRoleCategory'),
            tagline: document.getElementById('newAgentTagline'),
            specialty: document.getElementById('newAgentSpecialty'),
            persona: document.getElementById('newAgentPersona'),
        });
        const _populateAddAgentRoleSelect = () => {
            const sel = document.getElementById('newAgentRoleCategory');
            if (!sel) return;
            sel.innerHTML = '';
            for (const cat of _roleCategoryOrder) {
                if (cat === 'ceo') continue; // ceo는 빌트인 전용
                const opt = document.createElement('option');
                opt.value = cat;
                opt.textContent = _roleCategoryLabels[cat] || cat;
                sel.appendChild(opt);
            }
            sel.value = 'support'; // 기본은 가장 무해한 직군
        };
        const _clearAddAgentForm = () => {
            const f = _addAgentFields();
            for (const k of Object.keys(f)) {
                if (!f[k]) continue;
                if (k === 'roleCategory') continue; // select는 별도 populate
                f[k].value = '';
            }
            if (_addAgentError) _addAgentError.textContent = '';
            _populateAddAgentRoleSelect();
        };
        if (_addCompanyAgentBtn && _addAgentForm) {
            _addCompanyAgentBtn.onclick = () => {
                const open = _addAgentForm.getAttribute('data-open') === 'true';
                _addAgentForm.setAttribute('data-open', open ? 'false' : 'true');
                if (!open) {
                    _clearAddAgentForm();
                    _addAgentFields().id?.focus();
                }
            };
        }
        if (_cancelAddAgentBtn && _addAgentForm) {
            _cancelAddAgentBtn.onclick = () => {
                _addAgentForm.setAttribute('data-open', 'false');
                _clearAddAgentForm();
            };
        }
        if (_saveAddAgentBtn) {
            _saveAddAgentBtn.onclick = () => {
                const f = _addAgentFields();
                const def = {
                    id: (f.id?.value || '').trim().toLowerCase(),
                    name: (f.name?.value || '').trim(),
                    role: (f.role?.value || '').trim(),
                    emoji: (f.emoji?.value || '').trim(),
                    color: (f.color?.value || '').trim(),
                    roleCategory: (f.roleCategory?.value || 'support'),
                    tagline: (f.tagline?.value || '').trim(),
                    specialty: (f.specialty?.value || '').trim(),
                    persona: (f.persona?.value || '').trim(),
                };
                if (!def.id || !def.name || !def.role) {
                    if (_addAgentError) _addAgentError.textContent = 'id · 이름 · 역할은 필수입니다.';
                    return;
                }
                if (_addAgentError) _addAgentError.textContent = '';
                vscode.postMessage({ type: 'addCompanyAgent', def });
            };
        }

        // ── Work Pipeline editor (card-based) ──
        const _activePipelineSel = document.getElementById('activePipelineSel');
        const _pipelineList = document.getElementById('companyPipelineList');
        const _addPipelineBtn = document.getElementById('addCompanyPipelineBtn');
        const _pipelineEditForm = document.getElementById('pipelineEditForm');
        const _pipelineEditId = document.getElementById('pipelineEditId');
        const _pipelineEditName = document.getElementById('pipelineEditName');
        const _pipelineStageList = document.getElementById('pipelineStageList');
        const _addStageBtn = document.getElementById('addStageBtn');
        const _pipelineEditError = document.getElementById('pipelineEditError');
        const _cancelPipelineEditBtn = document.getElementById('cancelPipelineEditBtn');
        const _savePipelineEditBtn = document.getElementById('savePipelineEditBtn');

        // pipeline 페이로드에 같이 오는 직군별 활성 에이전트 캐시.
        // 카드의 "직군 → 담당" cascading dropdown이 이걸 참조한다.
        let _activeAgentsByCategory = {};

        // 현재 편집 중인 stages의 in-memory 표현. 카드 UI는 이 배열만 보고
        // 다시 그려지므로 텍스트 입력 외 모든 상태 변경(추가/삭제/순서변경/
        // 직군 변경)이 즉시 re-render를 통해 화면과 데이터를 동기화한다.
        let _editStages = [];
        // 드래그 중인 stage의 원본 인덱스. -1 = 드래그 중 아님.
        // 한 페이지에 에디터 인스턴스는 하나뿐이라 모듈 스코프로 충분.
        let _draggedStageIndex = -1;

        const _genStageId = (taken) => {
            const used = new Set(taken);
            let n = 1;
            while (used.has(`stage-${n}`)) n++;
            return `stage-${n}`;
        };

        const _emptyStage = () => ({
            id: _genStageId(_editStages.map((s) => s.id)),
            label: '',
            roleCategory: 'planner',
            agentId: '',
            modelOverride: '',
            requiresApproval: false,
            reviewWith: '',
            reviewMaxRounds: 3,
            instructionTemplate: '',
            loopBackPattern: '',
            loopBackTo: '',
            maxIterations: 3,
        });

        const _firstAgentOfCategory = (cat) => {
            const list = _activeAgentsByCategory[cat] || [];
            return list[0]?.id || '';
        };

        const _buildStageCard = (stage, index, total) => {
            const li = document.createElement('li');
            li.className = 'pipeline-stage-card';
            li.dataset.stageId = stage.id;
            li.dataset.index = String(index);

            // ── head: number + label input + ↑/↓/🗑 ──
            const head = document.createElement('div');
            head.className = 'psc-head';
            const num = document.createElement('span');
            num.className = 'psc-num';
            num.textContent = String(index + 1);
            num.title = '드래그하여 순서 변경 (↑/↓ 버튼도 가능)';

            // ── 드래그 핸들: 번호 칩만 드래그 시작점이 되도록.
            // textarea·input 영역에서 드래그가 시작되면 텍스트 선택과 충돌함.
            // li 자체는 드래그 가능하되 dragstart는 핸들 클릭 후에만 허용.
            li.draggable = false;
            let _dragArmed = false;
            num.addEventListener('mousedown', () => {
                li.draggable = true;
                _dragArmed = true;
            });
            // mouseup이 어디서든 일어나면 드래그 해제 (drop 후엔 dragend가 처리).
            const _disarm = () => {
                if (_dragArmed) { _dragArmed = false; li.draggable = false; }
            };
            li.addEventListener('mouseup', _disarm);
            li.addEventListener('mouseleave', _disarm);

            li.addEventListener('dragstart', (e) => {
                if (!_dragArmed) { e.preventDefault(); return; }
                e.dataTransfer.effectAllowed = 'move';
                // 텍스트 페이로드는 필요 없지만 일부 브라우저가 비어 있으면 무시.
                try { e.dataTransfer.setData('text/plain', stage.id); } catch {}
                li.classList.add('dragging');
                // li reference를 모듈 스코프 변수에 기록 — dragover/drop이 참조.
                _draggedStageIndex = index;
            });
            li.addEventListener('dragend', () => {
                li.classList.remove('dragging');
                li.draggable = false;
                _dragArmed = false;
                _draggedStageIndex = -1;
                // 모든 카드의 drop indicator 제거.
                _pipelineStageList?.querySelectorAll('.drop-above, .drop-below')
                    .forEach((el) => el.classList.remove('drop-above', 'drop-below'));
            });
            li.addEventListener('dragover', (e) => {
                if (_draggedStageIndex < 0 || _draggedStageIndex === index) return;
                e.preventDefault();
                e.dataTransfer.dropEffect = 'move';
                // 카드의 위/아래 절반에 따라 indicator 위치 결정.
                const rect = li.getBoundingClientRect();
                const above = (e.clientY - rect.top) < rect.height / 2;
                li.classList.toggle('drop-above', above);
                li.classList.toggle('drop-below', !above);
            });
            li.addEventListener('dragleave', () => {
                li.classList.remove('drop-above', 'drop-below');
            });
            li.addEventListener('drop', (e) => {
                e.preventDefault();
                li.classList.remove('drop-above', 'drop-below');
                if (_draggedStageIndex < 0 || _draggedStageIndex === index) return;
                const rect = li.getBoundingClientRect();
                const above = (e.clientY - rect.top) < rect.height / 2;
                const from = _draggedStageIndex;
                let to = above ? index : index + 1;
                // splice 보정: from < to 인 경우 to를 한 칸 당겨야 의도한 위치.
                if (from < to) to -= 1;
                if (from === to) return;
                const [moved] = _editStages.splice(from, 1);
                _editStages.splice(to, 0, moved);
                _draggedStageIndex = -1;
                _renderStages();
            });
            const lbl = document.createElement('input');
            lbl.type = 'text';
            lbl.className = 'psc-label';
            lbl.placeholder = '단계 이름 (예: 기획 논의)';
            lbl.value = stage.label || '';
            lbl.oninput = () => { stage.label = lbl.value; };
            const ctrls = document.createElement('div');
            ctrls.className = 'psc-controls';
            const upBtn = document.createElement('button');
            upBtn.textContent = '↑'; upBtn.title = '위로'; upBtn.disabled = index === 0;
            upBtn.onclick = () => { if (index > 0) { [_editStages[index - 1], _editStages[index]] = [_editStages[index], _editStages[index - 1]]; _renderStages(); } };
            const downBtn = document.createElement('button');
            downBtn.textContent = '↓'; downBtn.title = '아래로'; downBtn.disabled = index === total - 1;
            downBtn.onclick = () => { if (index < _editStages.length - 1) { [_editStages[index + 1], _editStages[index]] = [_editStages[index], _editStages[index + 1]]; _renderStages(); } };
            const delBtn = document.createElement('button');
            delBtn.className = 'del'; delBtn.textContent = '🗑'; delBtn.title = '단계 삭제';
            delBtn.onclick = () => {
                if (!confirm(`'${stage.label || stage.id}' 단계를 삭제할까요?`)) return;
                _editStages.splice(index, 1);
                _renderStages();
            };
            ctrls.appendChild(upBtn); ctrls.appendChild(downBtn); ctrls.appendChild(delBtn);
            head.appendChild(num); head.appendChild(lbl); head.appendChild(ctrls);
            li.appendChild(head);

            // ── body ──
            const body = document.createElement('div');
            body.className = 'psc-body';

            // row: 직군 → 담당
            const row = document.createElement('div');
            row.className = 'psc-row';
            const roleLabel = document.createElement('label');
            roleLabel.textContent = '직군:';
            const roleSel = document.createElement('select');
            for (const cat of _roleCategoryOrder) {
                if (cat === 'ceo') continue; // stage agent로 CEO는 안 씀
                const opt = document.createElement('option');
                opt.value = cat;
                opt.textContent = _roleCategoryLabels[cat] || cat;
                roleSel.appendChild(opt);
            }
            roleSel.value = stage.roleCategory || 'planner';
            const agentLabel = document.createElement('label');
            agentLabel.textContent = '담당:';
            const agentSel = document.createElement('select');
            const _refillAgentSel = () => {
                agentSel.innerHTML = '';
                // "⚙️ CEO 자동 선택" 옵션을 항상 맨 위에 추가 — value=''로 빈
                // agentId 저장됨. dispatcher가 보고 직군 후보 중 매번 CEO에게
                // 적임자 결정 요청. 사용자 의도 "CEO가 배분"의 정공법.
                const autoOpt = document.createElement('option');
                autoOpt.value = '';
                autoOpt.textContent = '⚙️ CEO 자동 선택 (이 직군 중)';
                agentSel.appendChild(autoOpt);
                const list = _activeAgentsByCategory[roleSel.value] || [];
                if (list.length === 0) {
                    const opt = document.createElement('option');
                    opt.value = '__no_agents__';
                    opt.textContent = '(이 직군의 활성 에이전트 없음)';
                    opt.disabled = true;
                    agentSel.appendChild(opt);
                    // CEO 자동 선택은 여전히 가능 — 활성 후보가 없으면 dispatcher에서
                    // no-active-agent-in-role 에러로 사용자에게 알린다.
                } else {
                    for (const a of list) {
                        const opt = document.createElement('option');
                        opt.value = a.id;
                        opt.textContent = `${a.emoji} ${a.name}`;
                        agentSel.appendChild(opt);
                    }
                }
                // 현재 stage.agentId가 빈값이면 CEO 자동 선택(default). 활성 후보에 있으면
                // 그 사람, 없으면 다시 CEO 자동으로 되돌림 — stale agentId 박제 방지.
                const aid = stage.agentId || '';
                if (aid && list.some((a) => a.id === aid)) {
                    agentSel.value = aid;
                } else {
                    agentSel.value = '';
                    stage.agentId = '';
                }
            };
            _refillAgentSel();
            roleSel.onchange = () => {
                stage.roleCategory = roleSel.value;
                // 직군 바뀌면 명시적 담당자 박힌 게 더 이상 의미 없음 — CEO 자동 선택으로 리셋.
                stage.agentId = '';
                _refillAgentSel();
            };
            agentSel.onchange = () => { stage.agentId = agentSel.value; };
            row.appendChild(roleLabel); row.appendChild(roleSel);
            row.appendChild(agentLabel); row.appendChild(agentSel);

            // 모델 select — 비워두면 에이전트의 모델 override → 글로벌 default 사용.
            const modelLbl = document.createElement('label'); modelLbl.textContent = '모델:';
            const stageModelSel = document.createElement('select');
            stageModelSel.title = '비워두면 담당 에이전트의 모델 설정(또는 글로벌 기본)을 사용';
            populateAgentModelSelect(stageModelSel, stage.modelOverride || '');
            // populateAgentModelSelect는 첫 옵션 라벨이 "default (global)"인데
            // stage 맥락에선 "기본 (에이전트 설정 사용)"이 더 정확. 첫 옵션 텍스트만 교체.
            if (stageModelSel.options.length > 0 && stageModelSel.options[0].value === '') {
                stageModelSel.options[0].text = '담당자 설정 사용';
            }
            stageModelSel.onchange = () => { stage.modelOverride = stageModelSel.value || ''; };
            row.appendChild(modelLbl); row.appendChild(stageModelSel);

            body.appendChild(row);

            // ── 승인 게이트 체크박스 ──
            // 한 stage가 끝난 뒤 사용자가 직접 ✅승인 / ✎수정요청 / 🛑중단 을
            // 누를 때까지 dispatcher가 대기. 검토 stage(inspector)나 중요 결정
            // 직전에 켜두면 자동 진행을 막을 수 있다.
            const approvalWrap = document.createElement('label');
            approvalWrap.style.cssText = 'display:flex; gap:6px; align-items:center; font-size:10px; color:var(--text-dim); cursor:pointer;';
            const approvalCb = document.createElement('input');
            approvalCb.type = 'checkbox';
            approvalCb.checked = !!stage.requiresApproval;
            approvalCb.onchange = () => { stage.requiresApproval = approvalCb.checked; };
            approvalWrap.appendChild(approvalCb);
            const approvalText = document.createElement('span');
            approvalText.textContent = '이 단계 후 내 승인 받기';
            approvalText.title = '체크하면 단계 완료 후 자동 진행하지 않고 승인/수정요청/중단 버튼이 채팅창에 뜹니다';
            approvalWrap.appendChild(approvalText);
            body.appendChild(approvalWrap);

            // ── 3-way 합의 검수 사이클 ──
            // 작업자 산출물 → 검수자 의견 → CEO 메타-판단을 라운드로 반복해
            // "작업자 + 검수자 + 대표" 셋이 모두 만족할 때까지 진행. 작은
            // 모델에선 라운드를 많이 돌 수 있으므로 maxRounds 안전망 필요.
            const reviewWrap = document.createElement('label');
            reviewWrap.style.cssText = 'display:flex; gap:6px; align-items:center; font-size:10px; color:var(--text-dim); cursor:pointer; margin-top:4px;';
            const reviewCb = document.createElement('input');
            reviewCb.type = 'checkbox';
            reviewCb.checked = !!stage.reviewWith;
            const reviewText = document.createElement('span');
            reviewText.textContent = '🔍 검수 사이클 (작업자 + 검수자 + CEO 합의)';
            reviewText.title = '체크하면 작업자 산출물 직후 검수자 + CEO 메타-판단을 라운드로 돌려 셋이 모두 만족할 때만 통과';
            reviewWrap.appendChild(reviewCb);
            reviewWrap.appendChild(reviewText);
            body.appendChild(reviewWrap);

            // 검수자 선택 + 최대 라운드 (체크박스 ON일 때만 노출)
            const reviewDetail = document.createElement('div');
            reviewDetail.style.cssText = 'display:none; gap:8px; flex-wrap:wrap; align-items:center; font-size:10px; color:var(--text-dim); margin: 2px 0 4px 22px;';
            const inspLbl = document.createElement('label'); inspLbl.textContent = '검수자:';
            const inspSel = document.createElement('select');
            // 옵션: 직군 inspector 자동 + 활성 inspector 직군 에이전트 + 'any active agent of given category' 정도면 충분
            const inspectorOpts = (_activeAgentsByCategory['inspector'] || []);
            const autoOpt = document.createElement('option');
            autoOpt.value = 'inspector';
            autoOpt.textContent = '⚙️ 감리 직군 자동';
            inspSel.appendChild(autoOpt);
            for (const a of inspectorOpts) {
                const opt = document.createElement('option');
                opt.value = `agent:${a.id}`;
                opt.textContent = `${a.emoji} ${a.name}`;
                inspSel.appendChild(opt);
            }
            // 현재값 적용
            inspSel.value = stage.reviewWith || 'inspector';
            inspSel.onchange = () => {
                stage.reviewWith = inspSel.value;
            };
            const roundLbl = document.createElement('label'); roundLbl.textContent = '최대 라운드:';
            const roundInput = document.createElement('input');
            roundInput.type = 'number'; roundInput.min = '1'; roundInput.max = '10';
            roundInput.style.cssText = 'width:55px; padding:2px 4px; background:var(--bg); color:var(--text-primary); border:1px solid var(--border); border-radius:4px;';
            roundInput.value = String(stage.reviewMaxRounds || 3);
            roundInput.oninput = () => {
                const v = parseInt(roundInput.value, 10);
                stage.reviewMaxRounds = Number.isFinite(v) ? Math.max(1, Math.min(10, v)) : 3;
            };
            reviewDetail.appendChild(inspLbl); reviewDetail.appendChild(inspSel);
            reviewDetail.appendChild(roundLbl); reviewDetail.appendChild(roundInput);
            body.appendChild(reviewDetail);

            const _syncReviewDetail = () => {
                reviewDetail.style.display = reviewCb.checked ? 'flex' : 'none';
            };
            _syncReviewDetail();
            reviewCb.onchange = () => {
                if (reviewCb.checked) {
                    stage.reviewWith = inspSel.value || 'inspector';
                    if (!stage.reviewMaxRounds) stage.reviewMaxRounds = 3;
                } else {
                    stage.reviewWith = '';
                }
                _syncReviewDetail();
            };

            // 지시 텍스트 + 토큰 버튼
            const instrLabelDiv = document.createElement('div');
            instrLabelDiv.className = 'psc-field-label';
            instrLabelDiv.textContent = '담당자에게 전달할 요청';
            body.appendChild(instrLabelDiv);

            const tokens = document.createElement('div');
            tokens.className = 'psc-tokens';
            const instr = document.createElement('textarea');
            instr.className = 'psc-instr';
            instr.placeholder = '예: {{userPrompt}} 에 대한 기획서 초안을 작성해주세요. 시장 조사 결과({{stage.research}})를 참고하세요.';
            instr.value = stage.instructionTemplate || '';
            instr.oninput = () => { stage.instructionTemplate = instr.value; };

            const _insertToken = (token) => {
                const start = instr.selectionStart ?? instr.value.length;
                const end = instr.selectionEnd ?? instr.value.length;
                instr.value = instr.value.slice(0, start) + token + instr.value.slice(end);
                stage.instructionTemplate = instr.value;
                instr.focus();
                instr.selectionStart = instr.selectionEnd = start + token.length;
            };
            const mkTokenBtn = (label, token) => {
                const b = document.createElement('button');
                b.textContent = label;
                b.onclick = (e) => { e.preventDefault(); _insertToken(token); };
                return b;
            };
            tokens.appendChild(mkTokenBtn('+ 사용자 요청', '{{userPrompt}}'));
            tokens.appendChild(mkTokenBtn('+ CEO 브리프', '{{brief}}'));
            // 이전 stages의 출력을 참조하는 토큰들 — 자기 자신 이후는 제외.
            for (let i = 0; i < index; i++) {
                const prev = _editStages[i];
                const label = prev.label || prev.id;
                tokens.appendChild(mkTokenBtn(`+ ${label}`, `{{stage.${prev.id}}}`));
            }
            body.appendChild(tokens);
            body.appendChild(instr);

            // ── loop-back details ──
            const loop = document.createElement('details');
            loop.className = 'psc-loop';
            if (stage.loopBackPattern || stage.loopBackTo) loop.open = true;
            const summary = document.createElement('summary');
            const hasLoop = !!(stage.loopBackPattern && stage.loopBackTo);
            summary.textContent = hasLoop
                ? `재작업 활성: "${stage.loopBackPattern}" 발견 시 → ${_editStages.find((s) => s.id === stage.loopBackTo)?.label || stage.loopBackTo}`
                : '문제가 있으면 이전 단계로 되돌리기 (선택)';
            loop.appendChild(summary);
            const grid = document.createElement('div');
            grid.className = 'psc-loop-grid';
            // condition
            const condLbl = document.createElement('label'); condLbl.textContent = '감지할 표현:';
            const condInput = document.createElement('input');
            condInput.type = 'text';
            condInput.placeholder = '예: 버그|오류|fail|재작업';
            condInput.value = stage.loopBackPattern || '';
            condInput.oninput = () => { stage.loopBackPattern = condInput.value; };
            // target
            const tgtLbl = document.createElement('label'); tgtLbl.textContent = '돌아갈 단계:';
            const tgtSel = document.createElement('select');
            const emptyOpt = document.createElement('option');
            emptyOpt.value = ''; emptyOpt.textContent = '(선택 안 함)';
            tgtSel.appendChild(emptyOpt);
            for (let i = 0; i < index; i++) {
                const prev = _editStages[i];
                const opt = document.createElement('option');
                opt.value = prev.id;
                opt.textContent = `${i + 1}. ${prev.label || prev.id}`;
                tgtSel.appendChild(opt);
            }
            tgtSel.value = (stage.loopBackTo && index > 0) ? stage.loopBackTo : '';
            tgtSel.onchange = () => { stage.loopBackTo = tgtSel.value; };
            // max
            const maxLbl = document.createElement('label'); maxLbl.textContent = '최대 반복:';
            const maxInput = document.createElement('input');
            maxInput.type = 'number'; maxInput.min = '1'; maxInput.max = '10';
            maxInput.value = String(stage.maxIterations || 3);
            maxInput.oninput = () => {
                const v = parseInt(maxInput.value, 10);
                stage.maxIterations = Number.isFinite(v) ? Math.max(1, Math.min(10, v)) : 3;
            };
            grid.appendChild(condLbl); grid.appendChild(condInput);
            grid.appendChild(tgtLbl); grid.appendChild(tgtSel);
            grid.appendChild(maxLbl); grid.appendChild(maxInput);
            if (index === 0) {
                const note = document.createElement('div');
                note.style.cssText = 'grid-column:1/-1; font-style:italic; opacity:0.7;';
                note.textContent = '첫 단계는 되돌아갈 곳이 없어 재시도 설정이 적용되지 않습니다.';
                grid.appendChild(note);
            }
            loop.appendChild(grid);
            body.appendChild(loop);

            li.appendChild(body);
            return li;
        };

        const _renderStages = () => {
            if (!_pipelineStageList) return;
            _pipelineStageList.innerHTML = '';
            for (let i = 0; i < _editStages.length; i++) {
                _pipelineStageList.appendChild(_buildStageCard(_editStages[i], i, _editStages.length));
            }
        };

        const _openPipelineEditor = (pipeline) => {
            if (!_pipelineEditForm) return;
            _pipelineEditForm.setAttribute('data-open', 'true');
            if (_pipelineEditError) _pipelineEditError.textContent = '';
            if (pipeline) {
                if (_pipelineEditId) { _pipelineEditId.value = pipeline.id; _pipelineEditId.disabled = true; }
                if (_pipelineEditName) _pipelineEditName.value = pipeline.name || '';
                // stages 데이터를 깊은 복사 — 사용자 수정이 cancel 시 안 새도록.
                _editStages = (pipeline.stages || []).map((s) => ({
                    id: s.id,
                    label: s.label || '',
                    // 기존 데이터에 roleCategory가 없으면 담당 agentId의 직군으로 추정.
                    roleCategory: s.roleCategory || _deriveRoleFromAgent(s.agentId) || 'planner',
                    agentId: s.agentId || '',
                    modelOverride: s.modelOverride || '',
                    requiresApproval: !!s.requiresApproval,
                    reviewWith: s.reviewWith || '',
                    reviewMaxRounds: s.reviewMaxRounds || 3,
                    instructionTemplate: s.instructionTemplate || '',
                    loopBackPattern: s.loopBackPattern || '',
                    loopBackTo: s.loopBackTo || '',
                    maxIterations: s.maxIterations || 3,
                }));
            } else {
                if (_pipelineEditId) { _pipelineEditId.value = ''; _pipelineEditId.disabled = false; }
                if (_pipelineEditName) _pipelineEditName.value = '';
                _editStages = [];
            }
            _renderStages();
        };

        // 빌트인 + 커스텀 에이전트의 직군 매핑 캐시 (lastCompanyAgentsPayload 활용)
        const _deriveRoleFromAgent = (agentId) => {
            if (!agentId || !_lastCompanyAgentsPayload) return null;
            const agent = (_lastCompanyAgentsPayload.agents || []).find((a) => a.id === agentId);
            return agent?.roleCategory || null;
        };

        const _closePipelineEditor = () => {
            if (_pipelineEditForm) _pipelineEditForm.setAttribute('data-open', 'false');
            if (_pipelineEditId) _pipelineEditId.disabled = false;
            _editStages = [];
        };

        if (_addPipelineBtn) {
            _addPipelineBtn.onclick = () => {
                _openPipelineEditor(null);
                _pipelineEditId?.focus();
            };
        }

        // 템플릿에서 새 pipeline 만들기. 선택 시 백엔드에 stages를 요청 →
        // companyPipelineTemplateContent 응답이 오면 에디터를 미리 채워 연다.
        const _pipelineTemplateSel = document.getElementById('pipelineTemplateSel');
        if (_pipelineTemplateSel) {
            _pipelineTemplateSel.onchange = () => {
                const tplId = _pipelineTemplateSel.value;
                if (!tplId) return;
                vscode.postMessage({ type: 'getCompanyPipelineTemplate', templateId: tplId });
                // 선택 후 즉시 초기화 — 사용자가 같은 템플릿을 다시 찍어도 onchange가 다시 발화하도록.
                _pipelineTemplateSel.value = '';
            };
        }
        if (_addStageBtn) {
            _addStageBtn.onclick = () => {
                const ns = _emptyStage();
                ns.agentId = _firstAgentOfCategory(ns.roleCategory);
                _editStages.push(ns);
                _renderStages();
                // 새로 추가한 카드의 라벨 입력으로 포커스 — 이름부터 적게 유도.
                const newCard = _pipelineStageList?.lastElementChild;
                newCard?.querySelector('.psc-label')?.focus();
            };
        }
        if (_cancelPipelineEditBtn) {
            _cancelPipelineEditBtn.onclick = _closePipelineEditor;
        }
        if (_savePipelineEditBtn) {
            _savePipelineEditBtn.onclick = () => {
                const id = (_pipelineEditId?.value || '').trim().toLowerCase();
                if (!id) {
                    if (_pipelineEditError) _pipelineEditError.textContent = 'id는 필수입니다 (소문자로 시작, /-/_).';
                    return;
                }
                // 각 stage 검증: 라벨 + (담당자 또는 직군) 중 하나는 반드시.
                // CEO 자동 선택 모드(agentId 비어 있음)면 직군이 필수.
                for (let i = 0; i < _editStages.length; i++) {
                    const s = _editStages[i];
                    if (!s.label?.trim()) {
                        if (_pipelineEditError) _pipelineEditError.textContent = `${i + 1}번 단계: 이름이 비어 있습니다.`;
                        return;
                    }
                    if (!s.agentId && !s.roleCategory) {
                        if (_pipelineEditError) _pipelineEditError.textContent = `${i + 1}번 단계: 담당자(또는 직군)을 지정하세요.`;
                        return;
                    }
                }
                const def = {
                    id,
                    name: (_pipelineEditName?.value || '').trim() || id,
                    stages: _editStages.map((s) => {
                        const out = {
                            id: s.id,
                            label: s.label.trim(),
                            // 빈 agentId(= CEO 자동 선택) 일 땐 필드 자체를 빼서 backend에
                            // optional로 전달. dispatcher가 roleCategory 보고 실시간 결정.
                            roleCategory: s.roleCategory,
                            instructionTemplate: s.instructionTemplate || '',
                        };
                        if (s.agentId && s.agentId.trim()) {
                            out.agentId = s.agentId.trim();
                        }
                        if (s.modelOverride && s.modelOverride.trim()) {
                            out.modelOverride = s.modelOverride.trim();
                        }
                        if (s.requiresApproval) out.requiresApproval = true;
                        if (s.reviewWith && s.reviewWith.trim()) {
                            out.reviewWith = s.reviewWith.trim();
                            out.reviewMaxRounds = s.reviewMaxRounds || 3;
                        }
                        if (s.loopBackPattern && s.loopBackTo) {
                            out.loopBackPattern = s.loopBackPattern;
                            out.loopBackTo = s.loopBackTo;
                            out.maxIterations = s.maxIterations || 3;
                        }
                        return out;
                    }),
                };
                vscode.postMessage({ type: 'upsertCompanyPipeline', def });
            };
        }
        if (_activePipelineSel) {
            _activePipelineSel.onchange = () => {
                vscode.postMessage({
                    type: 'setActiveCompanyPipeline',
                    pipelineId: _activePipelineSel.value || null,
                });
            };
        }

        let _lastCompanyPipelinesPayload = null;
        const renderCompanyPipelines = (payload) => {
            if (!_pipelineList || !_activePipelineSel) return;
            _lastCompanyPipelinesPayload = payload || null;
            const pipelines = (payload && payload.pipelines && typeof payload.pipelines === 'object') ? payload.pipelines : {};
            const activeId = payload && payload.activePipelineId ? payload.activePipelineId : '';
            // 직군별 에이전트 + 라벨 캐시 갱신. 에디터가 열려 있는 동안
            // 새 페이로드가 오면 카드의 담당 dropdown도 새 목록 반영.
            if (payload && payload.activeAgentsByCategory) {
                _activeAgentsByCategory = payload.activeAgentsByCategory;
            }
            if (payload && payload.roleCategoryLabels) _roleCategoryLabels = payload.roleCategoryLabels;
            if (payload && Array.isArray(payload.roleCategoryOrder)) _roleCategoryOrder = payload.roleCategoryOrder;
            // 템플릿 드롭다운 채우기.
            const tplSel = document.getElementById('pipelineTemplateSel');
            if (tplSel && payload && Array.isArray(payload.templates)) {
                tplSel.innerHTML = '<option value="">템플릿으로 시작</option>';
                for (const t of payload.templates) {
                    const opt = document.createElement('option');
                    opt.value = t.templateId;
                    opt.textContent = `${t.name} · ${t.stageCount}단계`;
                    opt.title = t.description || '';
                    tplSel.appendChild(opt);
                }
            }
            // 에디터가 열려 있으면 stages 다시 그려서 새로운 담당 옵션 반영.
            if (_pipelineEditForm?.getAttribute('data-open') === 'true' && _editStages.length > 0) {
                _renderStages();
            }
            // active dropdown
            _activePipelineSel.innerHTML = '<option value="">대표가 알아서 분배</option>';
            for (const p of Object.values(pipelines)) {
                const opt = document.createElement('option');
                opt.value = p.id;
                opt.textContent = `${p.name || p.id} · ${(p.stages || []).length}단계`;
                _activePipelineSel.appendChild(opt);
            }
            _activePipelineSel.value = activeId;
            // list
            _pipelineList.innerHTML = '';
            const entries = Object.values(pipelines);
            if (entries.length === 0) {
                const li = document.createElement('li');
                li.className = 'pipeline-empty-state';
                li.textContent = '아직 저장된 작업 흐름이 없습니다. 템플릿으로 시작하거나 직접 만들어보세요.';
                _pipelineList.appendChild(li);
                return;
            }
            for (const p of entries) {
                const li = document.createElement('li');
                li.className = 'pipeline-summary-card';
                const head = document.createElement('div');
                head.className = 'pipeline-summary-head';
                const title = document.createElement('div');
                title.className = 'pipeline-summary-title';
                title.innerHTML = `<strong>${escAttr(p.name || p.id)}</strong><span>${(p.stages || []).length}단계${p.id === activeId ? ' · 현재 사용 중' : ''}</span>`;
                const actions = document.createElement('div');
                actions.className = 'pipeline-summary-actions';
                const editBtn = document.createElement('button');
                editBtn.className = 'company-agent-edit';
                editBtn.textContent = '편집';
                editBtn.onclick = () => _openPipelineEditor(p);
                const delBtn = document.createElement('button');
                delBtn.className = 'company-agent-edit';
                delBtn.textContent = '삭제';
                delBtn.title = '작업 흐름 삭제';
                delBtn.onclick = () => {
                    if (!confirm(`'${p.name || p.id}' 작업 흐름을 삭제할까요?`)) return;
                    vscode.postMessage({ type: 'deleteCompanyPipeline', pipelineId: p.id });
                };
                actions.appendChild(editBtn);
                actions.appendChild(delBtn);
                head.appendChild(title);
                head.appendChild(actions);
                li.appendChild(head);
                const flow = document.createElement('div');
                flow.className = 'pipeline-summary-flow';
                const stages = Array.isArray(p.stages) ? p.stages : [];
                flow.textContent = stages.length > 0
                    ? stages.map((s) => s.label || s.id).join(' → ')
                    : '단계가 없습니다';
                li.appendChild(flow);
                _pipelineList.appendChild(li);
            }
        };
        // expose for the message handler below
        window.__renderCompanyPipelines = renderCompanyPipelines;
        window.__closePipelineEditor = _closePipelineEditor;

        // ──────────────────────────────────────────────────────────────────────
        // Pixel Office 렌더러 — 백엔드의 pixelOfficeUpdate 메시지를 받아 캐릭터,
        // 정보 패널, 말풍선 큐를 갱신. 어떤 상태도 그대로 그리기만 함 (read-only).
        // ──────────────────────────────────────────────────────────────────────
        (function setupPixelOffice() {
            const root = document.getElementById('pixelOffice');
            if (!root) return;
            const collapseBtn = document.getElementById('poCollapseBtn');
            const expandBtn = document.getElementById('poExpandBtn');
            const head = document.querySelector('#pixelOffice .po-head');
            const charEl = document.getElementById('poChar');
            const charEmoji = document.getElementById('poCharEmoji');
            const charProp = document.getElementById('poCharProp');
            const bubblesEl = document.getElementById('poBubbles');
            const progressBar = document.getElementById('poProgressBar');
            const statusLabel = document.getElementById('poStatusLabel');
            const statusVal = document.getElementById('poStatusVal');
            const agentName = document.getElementById('poAgentName');
            const taskEl = document.getElementById('poTask');
            const stepEl = document.getElementById('poStep');
            const nextStepRow = document.getElementById('poNextStepRow');
            const nextStepEl = document.getElementById('poNextStep');
            const messageRow = document.getElementById('poMessageRow');
            const messageEl = document.getElementById('poMessage');
            const needInputSection = document.getElementById('poNeedInputSection');
            const needInputList = document.getElementById('poNeedInputList');
            const approvalSection = document.getElementById('poApprovalSection');
            const approvalText = document.getElementById('poApprovalText');
            const contractSection = document.getElementById('poContractSection');
            const contractEl = document.getElementById('poContract');
            const logsSection = document.getElementById('poLogsSection');
            const logsEl = document.getElementById('poLogs');

            // 상태별 캐릭터·소품 매핑 — 픽셀아트 대신 이모지 조합으로.
            const STATUS_VIS = {
                idle:               { emoji: '🧑‍💼', prop: '' },
                intake:             { emoji: '🧑‍💼', prop: '📨' },
                analyzing:          { emoji: '🧐',    prop: '🔍' },
                need_clarification: { emoji: '🤔',    prop: '❓' },
                contract_ready:     { emoji: '🧑‍💼', prop: '📋' },
                planning:           { emoji: '🧑‍💼', prop: '📝' },
                executing:          { emoji: '🧑‍💻', prop: '⚙️' },
                reviewing:          { emoji: '🧐',    prop: '✅' },
                waiting_approval:   { emoji: '🧑‍💼', prop: '🛑' },
                error:              { emoji: '😵',    prop: '⚠️' },
                done:               { emoji: '😎',    prop: '☕' },
            };

            // ── 말풍선 큐 ──
            // 큐 크기 제한 + 자동 사라짐 타이머. 같은 텍스트 연속 등장 시 1개로 합침.
            let cfg = { enabled: true, bubblesEnabled: true, maxVisibleBubbles: 3, bubbleDurationMs: 4500 };
            const bubbleQueue = []; // { el, timer }
            let lastBubbleText = '';

            const collapseToggle = () => {
                const cur = root.getAttribute('data-collapsed') === 'true';
                root.setAttribute('data-collapsed', cur ? 'false' : 'true');
            };
            if (collapseBtn) collapseBtn.onclick = (e) => { e.stopPropagation(); collapseToggle(); };
            if (expandBtn) expandBtn.onclick = (e) => {
                e.stopPropagation();
                // 백엔드에 전체보기 panel 열기 요청.
                vscode.postMessage({ type: 'openPixelOfficePanel' });
            };
            // head 영역 자체 클릭으로도 토글 (버튼 외 영역).
            if (head) head.addEventListener('click', (e) => {
                if (e.target instanceof HTMLElement && (e.target.closest('.po-collapse') || e.target.closest('.po-expand'))) return;
                collapseToggle();
            });

            const dropOldestBubble = () => {
                const first = bubbleQueue.shift();
                if (!first) return;
                if (first.timer) clearTimeout(first.timer);
                first.el.classList.add('po-bubble-fading');
                setTimeout(() => { try { first.el.remove(); } catch {} }, 300);
            };

            const pushBubble = (b) => {
                if (!cfg.bubblesEnabled) return;
                if (!b || !b.text) return;
                if (b.text === lastBubbleText) return; // 연속 중복 차단
                lastBubbleText = b.text;
                const el = document.createElement('div');
                el.className = 'po-bubble po-bubble-' + (b.type || 'status');
                el.textContent = b.text;
                bubblesEl.appendChild(el);
                const duration = b.durationMs || cfg.bubbleDurationMs || 4500;
                const timer = setTimeout(() => {
                    const idx = bubbleQueue.findIndex((x) => x.el === el);
                    if (idx >= 0) {
                        bubbleQueue.splice(idx, 1);
                        el.classList.add('po-bubble-fading');
                        setTimeout(() => { try { el.remove(); } catch {} }, 300);
                    }
                }, duration);
                bubbleQueue.push({ el, timer });
                while (bubbleQueue.length > Math.max(1, cfg.maxVisibleBubbles)) {
                    dropOldestBubble();
                }
            };

            const setText = (el, val) => { if (el) el.textContent = (val == null || val === '') ? '—' : String(val); };

            const apply = (payload) => {
                cfg = Object.assign(cfg, payload && payload.config ? payload.config : {});
                root.setAttribute('data-enabled', cfg.enabled ? 'true' : 'false');
                if (!cfg.enabled) return;
                const state = payload && payload.state;
                if (state) {
                    const vis = STATUS_VIS[state.status] || STATUS_VIS.idle;
                    if (charEmoji) charEmoji.textContent = vis.emoji;
                    if (charProp) charProp.textContent = vis.prop;
                    root.setAttribute('data-status', state.status || 'idle');
                    // 상태 라벨 색상 클래스 새로.
                    if (statusLabel) {
                        statusLabel.className = 'po-status-label po-status-' + (state.status || 'idle');
                        statusLabel.textContent = state.status || 'idle';
                    }
                    setText(statusVal, state.status);
                    setText(agentName, state.agentName);
                    setText(taskEl, state.currentTask);
                    setText(stepEl, state.currentStep);
                    if (state.nextStep) {
                        nextStepRow.style.display = '';
                        setText(nextStepEl, state.nextStep);
                    } else {
                        nextStepRow.style.display = 'none';
                    }
                    if (state.message) {
                        messageRow.style.display = '';
                        setText(messageEl, state.message);
                    } else {
                        messageRow.style.display = 'none';
                    }
                    // Progress
                    if (progressBar) {
                        const pct = typeof state.progress === 'number'
                            ? Math.round(Math.max(0, Math.min(1, state.progress)) * 100)
                            : 0;
                        progressBar.style.width = pct + '%';
                    }
                    // Need input
                    if (Array.isArray(state.needUserInput) && state.needUserInput.length > 0) {
                        needInputSection.style.display = '';
                        needInputList.innerHTML = '';
                        for (const q of state.needUserInput) {
                            const li = document.createElement('li'); li.textContent = q;
                            needInputList.appendChild(li);
                        }
                    } else {
                        needInputSection.style.display = 'none';
                    }
                    // Approval
                    if (state.awaitingApproval) {
                        approvalSection.style.display = '';
                        setText(approvalText, state.awaitingApproval);
                    } else {
                        approvalSection.style.display = 'none';
                    }
                    // Contract
                    const c = state.requirementContract;
                    if (c && (c.goal || c.context || (c.criteria && c.criteria.length) || c.format)) {
                        contractSection.style.display = '';
                        contractEl.innerHTML = '';
                        const addRow = (k, v) => {
                            if (!v || (Array.isArray(v) && v.length === 0)) return;
                            const ke = document.createElement('div');
                            ke.className = 'po-contract-key'; ke.textContent = k;
                            const ve = document.createElement('div');
                            ve.className = 'po-contract-val';
                            ve.textContent = Array.isArray(v) ? v.map((x) => '• ' + x).join('\n') : String(v);
                            ve.style.whiteSpace = 'pre-line';
                            contractEl.appendChild(ke);
                            contractEl.appendChild(ve);
                        };
                        addRow('Goal', c.goal);
                        addRow('Ctx', c.context);
                        addRow('Crit', c.criteria);
                        addRow('Fmt', c.format);
                        if (c.confidence) addRow('Conf', c.confidence);
                    } else {
                        contractSection.style.display = 'none';
                    }
                    // Recent logs
                    if (Array.isArray(state.recentLogs) && state.recentLogs.length > 0) {
                        logsSection.style.display = '';
                        logsEl.innerHTML = '';
                        for (const line of state.recentLogs) {
                            const d = document.createElement('div');
                            d.textContent = line;
                            logsEl.appendChild(d);
                        }
                    } else {
                        logsSection.style.display = 'none';
                    }
                }
                // Bubbles
                if (Array.isArray(payload?.bubbles)) {
                    for (const b of payload.bubbles) pushBubble(b);
                }
            };
            window.__pixelOfficeApply = apply;

            // webview 로드 직후 백엔드 캐시 상태 요청.
            try { vscode.postMessage({ type: 'getPixelOfficeState' }); } catch {}
        })();

        // ──────────────────────────────────────────────────────────────────────
        // 이어서 진행 가능 세션 렌더링.
        //
        // 백엔드가 보낸 items 배열을 카드 목록으로 그린다. 비어 있으면 섹션 자체를
        // data-empty="true"로 숨겨 평소 시야에서 사라지게 만든다. 카드는 두 액션:
        //   - 이어서 진행 → resumeCompanyTurn 메시지
        //   - 버리기     → discardResumableSession (resume 파일을 'failed'로 마킹)
        // ──────────────────────────────────────────────────────────────────────
        const _formatRelative = (iso) => {
            if (!iso) return '';
            const d = new Date(iso); if (Number.isNaN(d.getTime())) return iso;
            const diff = Date.now() - d.getTime();
            const m = Math.round(diff / 60000);
            if (m < 1) return '방금 전';
            if (m < 60) return `${m}분 전`;
            const h = Math.round(m / 60);
            if (h < 24) return `${h}시간 전`;
            const days = Math.round(h / 24);
            return `${days}일 전`;
        };

        const _formatAbortReason = (reason) => {
            if (!reason) return '도중에 멈춤';
            const map = {
                'signal-aborted': '시작 직전에 중단',
                'aborted-after-plan': '계획 직후 중단',
                'aborted-mid-dispatch': '실행 중에 중단',
                'aborted-mid-pipeline': '단계 진행 중 중단',
                'aborted-mid-approval': '승인 대기 중 중단',
                'aborted-by-user-at-approval': '승인 단계에서 중단',
                'aborted-before-report': '보고서 직전 중단',
            };
            return map[reason] || reason;
        };

        const renderCompanyResumable = (payload) => {
            const section = document.getElementById('companyResumableSection');
            const list = document.getElementById('companyResumableList');
            if (!section || !list) return;
            const items = (payload && Array.isArray(payload.items)) ? payload.items : [];
            list.innerHTML = '';
            if (items.length === 0) {
                section.setAttribute('data-empty', 'true');
                return;
            }
            section.setAttribute('data-empty', 'false');
            for (const it of items) {
                const li = document.createElement('li');
                li.className = 'company-resumable-card';
                li.dataset.timestamp = it.timestamp;

                const head = document.createElement('div');
                head.className = 'company-resumable-head';
                const prompt = document.createElement('div');
                prompt.className = 'company-resumable-prompt';
                prompt.textContent = it.userPrompt || '(빈 요청)';
                prompt.title = it.userPrompt || '';
                const actions = document.createElement('div');
                actions.className = 'company-resumable-actions';

                const resumeBtn = document.createElement('button');
                resumeBtn.className = 'primary company-resumable-resume';
                resumeBtn.textContent = '이어서 진행';
                resumeBtn.title = '이 작업을 멈췄던 다음 단계부터 같은 세션에 이어 기록합니다.';
                resumeBtn.onclick = () => {
                    // 사용자에게 곧 시작될 거라는 시각 피드백.
                    resumeBtn.disabled = true;
                    resumeBtn.textContent = '재개 중…';
                    vscode.postMessage({ type: 'resumeCompanyTurn', timestamp: it.timestamp });
                    // overlay를 닫아 채팅 화면이 보이게 — 사용자가 진행 상황 즉시 확인.
                    document.getElementById('companyOverlay')?.classList.remove('visible');
                };

                const discardBtn = document.createElement('button');
                discardBtn.className = 'company-resumable-discard';
                discardBtn.textContent = '버리기';
                discardBtn.title = '이 작업을 더 이상 이어가지 않습니다. 목록에서만 빠지고 기존 산출물 파일은 그대로 남습니다.';
                discardBtn.onclick = () => {
                    // VS Code webview iframe 에서 `confirm()` 은 보안상 차단되어 false
                    // 즉시 반환됨 (사용자 화면엔 다이얼로그 안 뜸) → postMessage 가
                    // 영영 호출 안 되는 사고가 있었음. 산출물 파일은 안 지우므로
                    // 별도 확인 없이 바로 처리 — 사용자가 실수로 눌러도 ad-hoc 복구 가능.
                    // 작업 자체의 두 번 클릭 방지로 버튼은 즉시 disabled 처리.
                    discardBtn.disabled = true;
                    discardBtn.textContent = '버리는 중...';
                    vscode.postMessage({ type: 'discardResumableSession', timestamp: it.timestamp });
                };

                actions.appendChild(resumeBtn);
                actions.appendChild(discardBtn);
                head.appendChild(prompt);
                head.appendChild(actions);
                li.appendChild(head);

                const meta = document.createElement('div');
                meta.className = 'company-resumable-meta';
                const pipelineLabel = it.pipelineName
                    ? `📋 ${escAttr(it.pipelineName)}`
                    : '🧭 대표 분배 모드';
                const progress = (it.totalCount > 0)
                    ? `${it.completedCount}/${it.totalCount} 단계 완료`
                    : '진행도 정보 없음';
                const when = _formatRelative(it.lastUpdatedAt);
                const why = it.status === 'aborted'
                    ? `· ${_formatAbortReason(it.abortReason)}`
                    : (it.status === 'in-progress' ? '· 프로세스 중단 추정' : '');
                meta.innerHTML = `<span>${pipelineLabel}</span><span>${escAttr(progress)}</span><span>${escAttr(when)} ${escAttr(why)}</span>`;
                li.appendChild(meta);

                list.appendChild(li);
            }
        };
        window.__renderCompanyResumable = renderCompanyResumable;
        // 템플릿 stamp 시 호출 — id/name 제안값 + stages를 카드 에디터에 미리 채움.
        window.__openPipelineEditorWithTemplate = (tpl) => {
            if (!tpl) return;
            // suggested id가 이미 존재하면 -2, -3 식으로 충돌 회피.
            const taken = new Set(Object.keys((_lastCompanyPipelinesPayload?.pipelines) || {}));
            let id = tpl.suggestedPipelineId || 'pipeline';
            if (taken.has(id)) {
                let n = 2;
                while (taken.has(`${id}-${n}`)) n++;
                id = `${id}-${n}`;
            }
            _openPipelineEditor({
                id,
                name: tpl.suggestedPipelineName || id,
                stages: tpl.stages || [],
            });
            // suggested id는 새로 만드는 것이므로 잠가두지 않고 사용자가 바꿀 수 있게.
            if (_pipelineEditId) _pipelineEditId.disabled = false;
        };

        /**
         * Keep the last payload around so we can re-render whenever the
         * model list refreshes (the top `#modelSel` is the source of truth
         * for available models — see `populateAgentModelSelect`).
         */
        let _lastCompanyAgentsPayload = null;

        /**
         * Populate one agent's model `<select>` from the master `#modelSel`
         * options. Mirrors the pattern used by `refreshAgentMapModelOptions`
         * so the user picks from the same canonical list everywhere.
         * Empty value = "default (global)". Saved overrides not in the list
         * are preserved as a "(saved)" option so the value never gets lost.
         */
        function populateAgentModelSelect(sel, current) {
            sel.innerHTML = '';
            const useDefault = document.createElement('option');
            useDefault.value = '';
            useDefault.innerText = '기본 모델 사용';
            sel.appendChild(useDefault);
            const seen = new Set();
            for (const opt of modelSel.options) {
                if (!opt.value || seen.has(opt.value)) continue;
                seen.add(opt.value);
                const o = document.createElement('option');
                o.value = opt.value;
                o.innerText = opt.innerText;
                sel.appendChild(o);
            }
            if (current && !seen.has(current)) {
                const o = document.createElement('option');
                o.value = current;
                o.innerText = `${current} (저장됨)`;
                sel.appendChild(o);
            }
            sel.value = current || '';
        }

        /**
         * Render the agent cards in the manage overlay. Each card has:
         *  - a model dropdown (default + every loaded model)
         *  - an ON/OFF toggle (CEO always-on)
         *  - an Edit button that toggles an inline prompt editor with
         *    tagline / specialty / persona textareas + Reset/Save/Cancel.
         */
        // 직군 라벨/순서 캐시. 페이로드에 같이 오므로 첫 렌더 후엔 항상 채워져 있음.
        // pipeline 에디터(다음 phase)도 같은 캐시를 본다.
        let _roleCategoryLabels = {
            ceo: 'CEO', planner: '기획', researcher: '리서치', designer: '디자인',
            developer: '개발', qa: 'QA', inspector: '감리', support: '지원',
        };
        let _roleCategoryOrder = ['ceo','planner','researcher','designer','developer','qa','inspector','support'];
        window.__getRoleLabels = () => _roleCategoryLabels;
        window.__getRoleOrder = () => _roleCategoryOrder;

        function renderCompanyAgentCards(payload) {
            if (!_companyAgentList) return;
            _lastCompanyAgentsPayload = payload;
            _companyAgentList.innerHTML = '';
            if (_companyNameInput && payload && typeof payload.companyName === 'string') {
                _companyNameInput.value = payload.companyName;
            }
            if (payload && payload.roleCategoryLabels) _roleCategoryLabels = payload.roleCategoryLabels;
            if (payload && Array.isArray(payload.roleCategoryOrder)) _roleCategoryOrder = payload.roleCategoryOrder;
            const rawAgents = (payload && Array.isArray(payload.agents)) ? payload.agents : [];
            // 활성/비활성 정렬: CEO는 항상 최상단(alwaysOn), 그 다음 활성(켜짐) 에이전트,
            // 마지막으로 비활성. 같은 그룹 내에서는 백엔드가 보낸 원래 순서를 유지(stable).
            const agents = rawAgents
                .map((a, i) => ({ a, i }))
                .sort((x, y) => {
                    // CEO/alwaysOn 최우선
                    const xLocked = x.a.alwaysOn ? 0 : 1;
                    const yLocked = y.a.alwaysOn ? 0 : 1;
                    if (xLocked !== yLocked) return xLocked - yLocked;
                    // 그 다음 active 여부
                    const xAct = x.a.active ? 0 : 1;
                    const yAct = y.a.active ? 0 : 1;
                    if (xAct !== yAct) return xAct - yAct;
                    // tiebreak: 원래 순서
                    return x.i - y.i;
                })
                .map((p) => p.a);
            let sectionKey = '';
            const appendTeamSection = (key, title, hint) => {
                if (sectionKey === key) return;
                sectionKey = key;
                const section = document.createElement('li');
                section.className = 'company-agent-section-label';
                // 시각적 그룹핑용 라벨 — 스크린리더는 카드 갯수만 세도록 presentation 처리.
                section.setAttribute('role', 'presentation');
                section.setAttribute('aria-hidden', 'true');
                section.innerHTML = `<span>${escAttr(title)}</span><small>${escAttr(hint)}</small>`;
                _companyAgentList.appendChild(section);
            };
            for (const a of agents) {
                if (a.alwaysOn || a.active) {
                    appendTeamSection('active', '참여 중', '이번 작업에 함께 응답합니다');
                } else {
                    appendTeamSection('standby', '대기 중', '필요할 때 팀에 합류시킬 수 있습니다');
                }
                const li = document.createElement('li');
                li.className = 'company-agent-card';
                li.setAttribute('data-active', a.active ? 'true' : 'false');
                if (a.alwaysOn) li.setAttribute('data-locked', 'true');
                li.dataset.agentId = a.id;
                li.style.setProperty('--agent-color', a.color || 'var(--accent)');

                // ── Row 1: emoji + name/tagline + controls ──
                // CSS handles layout via `.company-agent-head` (flex-wrap,
                // gap, etc.) so we don't repeat inline styles here.
                const row = document.createElement('div');
                row.className = 'company-agent-head';

                const emoji = document.createElement('span');
                emoji.className = 'company-agent-emoji';
                emoji.textContent = a.emoji;

                const body = document.createElement('div');
                body.className = 'company-agent-body';
                const name = document.createElement('div');
                name.className = 'company-agent-name';
                name.innerHTML = `<span>${escAttr(a.name)}</span><span class="company-agent-role">${escAttr(a.role)}</span>`;
                const tag = document.createElement('div');
                tag.className = 'company-agent-tagline';
                tag.textContent = a.tagline || '';
                tag.title = a.specialty || '';
                body.appendChild(name);
                body.appendChild(tag);
                const meta = document.createElement('div');
                meta.className = 'company-agent-meta';
                const roleChip = document.createElement('span');
                roleChip.className = 'company-agent-chip';
                roleChip.textContent = _roleCategoryLabels[a.roleCategory || a.defaultRoleCategory] || a.roleCategory || a.defaultRoleCategory || '역할';
                const modelChip = document.createElement('span');
                modelChip.className = 'company-agent-chip';
                modelChip.textContent = a.modelOverride ? '전용 모델' : '기본 모델';
                meta.appendChild(roleChip);
                meta.appendChild(modelChip);
                if (a.personaOverridden || a.specialtyOverridden || a.taglineOverridden || a.nameOverridden || a.roleOverridden) {
                    const tunedChip = document.createElement('span');
                    tunedChip.className = 'company-agent-chip tuned';
                    tunedChip.textContent = '커스텀';
                    meta.appendChild(tunedChip);
                }
                body.appendChild(meta);

                const controls = document.createElement('div');
                controls.className = 'company-agent-controls';

                // 직군 select — CEO는 잠금. role 라벨 보다 먼저 와서 시선 흐름이
                // "이 사람은 [직군] 직군"으로 자연스럽게 읽히도록 controls 첫번째에.
                const roleSelEl = document.createElement('select');
                roleSelEl.className = 'company-agent-role-select';
                roleSelEl.title = '이 에이전트의 직군. 파이프라인 에디터에서 직군별로 담당자를 고를 때 사용됩니다.';
                if (a.id === 'ceo') {
                    const opt = document.createElement('option');
                    opt.value = 'ceo'; opt.textContent = _roleCategoryLabels.ceo || 'CEO';
                    roleSelEl.appendChild(opt);
                    roleSelEl.value = 'ceo';
                    roleSelEl.disabled = true;
                } else {
                    for (const cat of _roleCategoryOrder) {
                        if (cat === 'ceo') continue;
                        const opt = document.createElement('option');
                        opt.value = cat;
                        opt.textContent = _roleCategoryLabels[cat] || cat;
                        roleSelEl.appendChild(opt);
                    }
                    roleSelEl.value = a.roleCategory || a.defaultRoleCategory || 'support';
                    if (a.roleCategoryOverridden) roleSelEl.classList.add('overridden');
                    roleSelEl.onchange = () => {
                        const v = roleSelEl.value;
                        const sendValue = v === a.defaultRoleCategory ? null : v;
                        vscode.postMessage({
                            type: 'setCompanyAgentRoleCategory',
                            agentId: a.id,
                            value: sendValue,
                        });
                    };
                }

                const modelSelEl = document.createElement('select');
                modelSelEl.className = 'company-agent-model';
                modelSelEl.title = '비워두면 글로벌 기본 모델 사용';
                populateAgentModelSelect(modelSelEl, a.modelOverride || '');
                modelSelEl.onchange = () => {
                    vscode.postMessage({
                        type: 'setCompanyAgentModel',
                        agentId: a.id,
                        model: modelSelEl.value || '',
                    });
                };

                const editBtn = document.createElement('button');
                editBtn.className = 'company-agent-edit';
                editBtn.textContent = '프로필';
                if (a.personaOverridden || a.specialtyOverridden || a.taglineOverridden) {
                    editBtn.classList.add('dirty');
                    editBtn.title = '프로필과 응답 방식이 사용자에 의해 조정되었습니다';
                } else {
                    editBtn.title = '이름, 역할, 잘하는 일, 응답 스타일 편집';
                }
                editBtn.onclick = () => {
                    const expanded = li.getAttribute('data-expanded') === 'true';
                    li.setAttribute('data-expanded', expanded ? 'false' : 'true');
                    // 카드가 갑자기 두 배로 길어지지 않게 다른 패널은 자동으로 접는다.
                    li.setAttribute('data-settings-expanded', 'false');
                };
                const settingsBtn = document.createElement('button');
                settingsBtn.className = 'company-agent-edit company-agent-settings-btn';
                settingsBtn.textContent = '설정';
                settingsBtn.title = '역할 그룹, 모델, 참고 지식 반영 조정';
                settingsBtn.onclick = () => {
                    const expanded = li.getAttribute('data-settings-expanded') === 'true';
                    li.setAttribute('data-settings-expanded', expanded ? 'false' : 'true');
                    li.setAttribute('data-expanded', 'false');
                };

                const toggle = document.createElement('button');
                toggle.className = 'company-agent-toggle';
                toggle.textContent = a.active ? '참여 중' : '+ 참여';
                if (a.alwaysOn) {
                    toggle.disabled = true;
                    toggle.textContent = '항상 참여';
                } else {
                    toggle.onclick = () => {
                        const wantActive = !(li.getAttribute('data-active') === 'true');
                        // ── Optimistic update ──
                        // 백엔드 응답이 오기 전에 카드를 옳은 섹션(참여 중 / 대기 중)으로
                        // 즉시 이동시켜야 라벨과 카드 위치가 어긋난 상태로 머무르지 않는다.
                        // 캐시된 페이로드의 active 플래그를 갱신한 뒤 같은 페이로드로 한 번 더
                        // 렌더하면 sort + 섹션 라벨이 새 상태대로 재계산됨.
                        if (_lastCompanyAgentsPayload && Array.isArray(_lastCompanyAgentsPayload.agents)) {
                            const found = _lastCompanyAgentsPayload.agents.find((x) => x.id === a.id);
                            if (found) found.active = wantActive;
                        }
                        const nextIds = (_lastCompanyAgentsPayload?.agents || [])
                            .filter((x) => x.active || x.alwaysOn)
                            .map((x) => x.id)
                            .filter(Boolean);
                        // 백엔드 알림은 먼저 보내고 (네트워크 latency 흡수), 그 직후 동기 재렌더로
                        // UI를 정렬. 백엔드 응답이 오면 같은 결과로 한 번 더 그려지지만 idempotent.
                        vscode.postMessage({ type: 'setCompanyActiveAgents', value: nextIds });
                        if (_lastCompanyAgentsPayload) renderCompanyAgentCards(_lastCompanyAgentsPayload);
                    };
                }
                controls.appendChild(editBtn);
                controls.appendChild(settingsBtn);
                // 삭제 버튼 — CEO만 빼고 빌트인/커스텀 모두 노출. 단, 어떤 워크 파이프라인의
                // stage라도 이 에이전트를 참조하고 있으면 disabled로 처리하고 tooltip에
                // 사용 중인 파이프라인을 적어, 사용자가 어디로 가서 빼야 하는지 보이게 한다.
                if (!a.alwaysOn) {
                    const delBtn = document.createElement('button');
                    delBtn.className = 'company-agent-edit company-agent-delete';
                    delBtn.textContent = a.custom ? '삭제' : '숨김';
                    const usedIn = Array.isArray(a.usedInPipelines) ? a.usedInPipelines : [];
                    if (usedIn.length > 0) {
                        delBtn.disabled = true;
                        delBtn.classList.add('disabled');
                        delBtn.title = `숨길 수 없음 — 다음 작업 흐름에서 사용 중: ${usedIn.map((n) => `'${n}'`).join(', ')}. 작업 흐름에서 먼저 빼거나 다른 팀원으로 교체하세요.`;
                    } else {
                        delBtn.title = a.custom
                            ? `'${a.name}' 팀원 삭제 (모든 설정 함께 제거)`
                            : `'${a.name}' 팀원 숨기기 (기본 팀원이라 복원 가능)`;
                        delBtn.onclick = () => {
                            const confirmMsg = a.custom
                                ? `'${a.name}' 팀원을 삭제할까요? 이 팀원의 모든 설정(모델·응답 방식·지식 반영)도 함께 삭제됩니다.`
                                : `기본 팀원 '${a.name}'을(를) 목록에서 숨길까요? 나중에 [목록 맨 아래 → 숨긴 기본 팀원] 영역에서 복원할 수 있습니다.`;
                            if (!confirm(confirmMsg)) return;
                            vscode.postMessage({ type: 'deleteCompanyAgent', agentId: a.id });
                        };
                    }
                    controls.appendChild(delBtn);
                }
                controls.appendChild(toggle);

                row.appendChild(emoji);
                row.appendChild(body);
                row.appendChild(controls);
                li.appendChild(row);

                const settings = document.createElement('div');
                settings.className = 'company-agent-settings';
                const settingsGrid = document.createElement('div');
                settingsGrid.className = 'company-agent-settings-grid';
                const roleWrap = document.createElement('label');
                roleWrap.textContent = '역할 그룹';
                roleWrap.appendChild(roleSelEl);
                const modelWrap = document.createElement('label');
                modelWrap.textContent = '모델';
                modelWrap.appendChild(modelSelEl);
                settingsGrid.appendChild(roleWrap);
                settingsGrid.appendChild(modelWrap);
                settings.appendChild(settingsGrid);
                // CEO doesn't dispatch agents itself, it only synthesises,
                // so the brain mix for CEO turns is governed by specialists.
                if (a.id !== 'ceo') {
                    settings.appendChild(_buildAgentKnowledgeMixSlider(a, payload.globalKnowledgeMixWeight));
                }
                li.appendChild(settings);

                // ── Row 2 (collapsed by default): prompt editor ──
                li.appendChild(_buildAgentPromptEditor(a));
                _companyAgentList.appendChild(li);
            }
            // ── 숨겨진 기본 에이전트 복원 영역 ──
            // 사용자가 "삭제"한 빌트인 에이전트를 다시 꺼낼 수 있는 출구. 빈 배열이면
            // 섹션 자체를 그리지 않아 평소엔 시야에서 사라진다.
            const hidden = Array.isArray(payload && payload.hiddenBuiltins) ? payload.hiddenBuiltins : [];
            if (hidden.length > 0) {
                const restoreLi = document.createElement('li');
                restoreLi.className = 'company-agent-hidden-section';
                const head = document.createElement('div');
                head.className = 'company-agent-hidden-head';
                head.textContent = `숨긴 기본 팀원 (${hidden.length}명)`;
                restoreLi.appendChild(head);
                const hint = document.createElement('div');
                hint.className = 'company-agent-hidden-hint';
                hint.textContent = '복원하면 목록에 다시 추가됩니다. 설정은 모두 디폴트로 시작합니다.';
                restoreLi.appendChild(hint);
                const list = document.createElement('div');
                list.className = 'company-agent-hidden-list';
                for (const h of hidden) {
                    const chip = document.createElement('button');
                    chip.className = 'company-agent-hidden-chip';
                    chip.textContent = `${h.emoji || '🙂'} ${h.name} · ${h.role}  ↩ 복원`;
                    chip.title = `'${h.name}' 복원`;
                    chip.onclick = () => {
                        vscode.postMessage({ type: 'restoreHiddenAgent', agentId: h.id });
                    };
                    list.appendChild(chip);
                }
                restoreLi.appendChild(list);
                _companyAgentList.appendChild(restoreLi);
            }
            if (_companyStatusEl) _companyStatusEl.textContent = '';
        }

        /**
         * Inline Knowledge Mix slider for one agent. Empty (override = null)
         * means "use the global slider value" — shown as a hint label so the
         * user knows what they'll fall back to. Slider commits on `change`
         * (not `input`) so dragging doesn't spam writes.
         */
        function _buildAgentKnowledgeMixSlider(a, globalWeight) {
            const row = document.createElement('div');
            row.className = 'company-agent-mix-row';
            const usingOverride = a.knowledgeMixOverride !== null && a.knowledgeMixOverride !== undefined;
            const effective = a.effectiveKnowledgeMixWeight;

            const label = document.createElement('span');
            label.className = 'company-agent-mix-label';
            label.textContent = '참고 지식 반영';

            const sourceBadge = document.createElement('span');
            sourceBadge.className = 'company-agent-mix-source' + (usingOverride ? ' override' : '');
            sourceBadge.textContent = usingOverride ? '직접 조정' : '전체 설정';

            const slider = document.createElement('input');
            slider.type = 'range';
            slider.min = '0'; slider.max = '100'; slider.step = '5';
            slider.value = String(effective);
            slider.disabled = !usingOverride;
            slider.className = 'company-agent-mix-slider';

            const hint = document.createElement('span');
            hint.className = 'company-agent-mix-hint';
            const renderHint = () => {
                const w = parseInt(slider.value, 10) || 50;
                hint.textContent = `${w}%`;
            };

            const cb = document.createElement('input');
            cb.type = 'checkbox'; cb.checked = !usingOverride;
            cb.className = 'company-agent-mix-cb';
            cb.title = '전체 참고 지식 설정 사용';
            cb.onchange = () => {
                if (cb.checked) {
                    // Reset to global.
                    slider.disabled = true;
                    slider.value = String(globalWeight ?? 50);
                    sourceBadge.textContent = '전체 설정';
                    sourceBadge.classList.remove('override');
                    renderHint();
                    vscode.postMessage({
                        type: 'setCompanyAgentKnowledgeMix',
                        agentId: a.id, value: null,
                    });
                } else {
                    // Take ownership at the current displayed value.
                    slider.disabled = false;
                    sourceBadge.textContent = '직접 조정';
                    sourceBadge.classList.add('override');
                    const w = parseInt(slider.value, 10) || 50;
                    vscode.postMessage({
                        type: 'setCompanyAgentKnowledgeMix',
                        agentId: a.id, value: w,
                    });
                }
            };
            slider.addEventListener('input', renderHint);
            slider.addEventListener('change', () => {
                if (cb.checked) return; // safety: shouldn't fire when disabled
                const w = parseInt(slider.value, 10) || 50;
                vscode.postMessage({
                    type: 'setCompanyAgentKnowledgeMix',
                    agentId: a.id, value: w,
                });
            });
            renderHint();
            const cbWrap = document.createElement('label');
            cbWrap.className = 'company-agent-mix-cbwrap';
            cbWrap.appendChild(cb);
            cbWrap.appendChild(document.createTextNode(' 전체 설정 사용'));
            row.appendChild(label);
            row.appendChild(sourceBadge);
            row.appendChild(slider);
            row.appendChild(hint);
            row.appendChild(cbWrap);
            return row;
        }

        /**
         * Build the per-agent prompt editor. Hidden until the user clicks
         * the Edit button. Three fields (tagline / specialty / persona);
         * Save sends whichever fields actually changed; Reset sends `null`
         * to wipe all overrides at once.
         */
        function _buildAgentPromptEditor(a) {
            const editor = document.createElement('div');
            editor.className = 'company-agent-editor';

            const _field = (key, labelText, isTextarea, current, defaultVal, overridden) => {
                const lbl = document.createElement('label');
                lbl.className = 'field-label';
                lbl.innerHTML = `<span>${labelText}</span>` +
                    (overridden
                        ? '<span class="field-flag">조정됨</span>'
                        : '<span class="field-flag" style="color:var(--text-dim)">기본값</span>');
                editor.appendChild(lbl);
                const el = isTextarea
                    ? document.createElement('textarea')
                    : document.createElement('input');
                if (!isTextarea) el.type = 'text';
                el.value = current || '';
                el.placeholder = defaultVal || '';
                editor.appendChild(el);
                return el;
            };

            // ── 디스플레이 필드 (이름·역할·이모지·색상) ──
            // 빌트인이든 커스텀이든 사용자가 자유롭게 리네이밍 가능. 변경 후
            // CEO 보고서·planner enumeration·세션 로그 등 모든 표시 지점에
            // 즉시 반영된다 — resolveAgent가 override를 머지하므로.
            const nameInput = _field('name', '이름', false, a.name, a.defaultName, a.nameOverridden);
            const roleInput = _field('role', '역할 소개', false, a.role, a.defaultRole, a.roleOverridden);
            // 이모지·색상은 한 줄에 나란히 — CSS는 grid 없이 inline flex로 처리.
            const visualWrap = document.createElement('div');
            visualWrap.style.cssText = 'display:flex; gap:8px;';
            const emojiInput = _field('emoji', '이모지', false, a.emoji, a.defaultEmoji, a.emojiOverridden);
            const colorInput = _field('color', '테마 색상', false, a.color, a.defaultColor, a.colorOverridden);
            // 위에서 만든 두 필드는 editor에 이미 append됨. 한 줄로 묶고 싶으면
            // 부모에서 분리해 visualWrap에 다시 넣는다 — label은 직전 sibling.
            // 더 단순하게: emoji/color 입력 후 reflow는 그냥 두고 max-width만 줄임.
            emojiInput.style.maxWidth = '80px';
            colorInput.style.maxWidth = '120px';

            const tagInput = _field('tagline', '한 줄 소개', false, a.tagline, a.defaultTagline, a.taglineOverridden);
            const specInput = _field('specialty', '잘하는 일', true, a.specialty, a.defaultSpecialty, a.specialtyOverridden);
            const persInput = _field('persona', '응답 스타일', true, a.persona, a.defaultPersona, a.personaOverridden);
            specInput.rows = 3;
            persInput.rows = 5;

            const actions = document.createElement('div');
            actions.className = 'editor-actions';

            const resetBtn = document.createElement('button');
            resetBtn.className = 'danger';
            resetBtn.textContent = '기본값으로';
            resetBtn.title = '이 에이전트의 모든 사용자 변경 사항 제거 → 디폴트로 복귀 (이름·역할·프롬프트 전부)';
            resetBtn.onclick = () => {
                vscode.postMessage({ type: 'setCompanyAgentPrompt', agentId: a.id, override: null });
                vscode.postMessage({ type: 'setCompanyAgentDisplay', agentId: a.id, override: null });
            };

            const cancelBtn = document.createElement('button');
            cancelBtn.textContent = '취소';
            cancelBtn.onclick = () => {
                const card = editor.closest('.company-agent-card');
                if (card) card.setAttribute('data-expanded', 'false');
            };

            const saveBtn = document.createElement('button');
            saveBtn.className = 'primary';
            saveBtn.textContent = '저장';
            saveBtn.onclick = () => {
                // 두 개의 message로 분리 전송 — display(name/role/emoji/color)와
                // prompt(tagline/specialty/persona)는 백엔드에서 서로 다른
                // override 테이블을 쓴다. 빈 문자열은 그 필드만 reset, 디폴트와
                // 같은 값은 override 안 함(원본 그대로 두기) — 사용자가 일부러
                // 디폴트 값을 다시 입력해 "override 박제"하는 케이스는 드물고,
                // 안 박는 게 다음 코드 업데이트 시 새 디폴트를 자동 흡수해서
                // 유리하다.
                vscode.postMessage({
                    type: 'setCompanyAgentDisplay',
                    agentId: a.id,
                    override: {
                        name: nameInput.value === a.defaultName ? '' : nameInput.value,
                        role: roleInput.value === a.defaultRole ? '' : roleInput.value,
                        emoji: emojiInput.value === a.defaultEmoji ? '' : emojiInput.value,
                        color: colorInput.value === a.defaultColor ? '' : colorInput.value,
                    },
                });
                vscode.postMessage({
                    type: 'setCompanyAgentPrompt',
                    agentId: a.id,
                    override: {
                        tagline: tagInput.value === a.defaultTagline ? '' : tagInput.value,
                        specialty: specInput.value === a.defaultSpecialty ? '' : specInput.value,
                        persona: persInput.value === a.defaultPersona ? '' : persInput.value,
                    },
                });
            };

            actions.appendChild(resetBtn);
            actions.appendChild(cancelBtn);
            actions.appendChild(saveBtn);
            editor.appendChild(actions);
            return editor;
        }

        /**
         * Render one phase event from the dispatcher. The chat gets a
         * card per phase so the user can follow progress in real time —
         * "🧭 CEO 작업 분배 중..." → "📺 레오 작업 수행 중..." → final report.
         */
        function renderCompanyPhase(ev) {
            const chatEl = document.getElementById('chat');
            if (!chatEl) return;
            const card = document.createElement('div');
            card.className = 'company-phase-card';
            if (ev.phase === 'plan-start') {
                card.innerHTML = '<div class="cph-head">🧭 CEO</div><div class="cph-meta">작업 분배 중…</div>';
            } else if (ev.phase === 'plan-ready') {
                const tasks = (ev.plan?.tasks || []).map((t, i) => `${i + 1}. <strong>${escAttr(t.agent)}</strong> — ${escAttr(t.task)}`).join('<br>');
                card.innerHTML = `<div class="cph-head">🧭 CEO 브리프</div>
                    <div>${escAttr(ev.plan?.brief || '(brief 없음)')}</div>
                    <div class="cph-meta" style="margin-top:6px">${tasks || '(no tasks — chat reply)'}</div>`;
            } else if (ev.phase === 'agent-start') {
                card.innerHTML = `<div class="cph-head">${escAttr(ev.agentId)} 작업 수행 중…</div>
                    <div class="cph-meta">${escAttr(ev.task)} <em>(${ev.index + 1}/${ev.total})</em></div>`;
            } else if (ev.phase === 'agent-done') {
                const o = ev.output || {};
                const body = (o.response || '').slice(0, 4000);
                card.innerHTML = `<div class="cph-head">${escAttr(ev.agentId)} 완료 <span class="cph-meta">${(o.durationMs/1000).toFixed(1)}s${o.error ? ' · ⚠️ ' + escAttr(o.error) : ''}</span></div>
                    <div class="markdown-body">${fmt(body)}</div>`;
            } else if (ev.phase === 'stage-loop') {
                card.innerHTML = `<div class="cph-head">🔁 Stage 재시도</div>
                    <div class="cph-meta">${escAttr(ev.from)} → ${escAttr(ev.to)} (반복 ${ev.iteration}회)</div>`;
            } else if (ev.phase === 'review-start') {
                // 검수 사이클 시작 — 같은 stageId에 라운드를 누적할 컨테이너 카드.
                // 이후 review-round 이벤트가 이 카드의 .rev-rounds 자식 안에 한 줄씩 append.
                card.className += ' review';
                card.dataset.stageId = ev.stageId;
                card.dataset.reviewMaxRounds = String(ev.maxRounds || 3);
                card.innerHTML = `<div class="cph-head">🔍 <strong>${escAttr(ev.stageLabel || ev.stageId)}</strong> 검수 사이클 시작 <span class="cph-meta">검수자: ${escAttr(ev.inspectorAgentId)} · 최대 ${ev.maxRounds}라운드</span></div>
                    <div class="rev-rounds"></div>`;
            } else if (ev.phase === 'review-round') {
                // 이미 그려둔 review-start 카드 안에 라운드 한 줄을 누적.
                const target = chatEl.querySelector(`.company-phase-card.review[data-stage-id="${CSS.escape(ev.stageId)}"] .rev-rounds`);
                if (target) {
                    const row = document.createElement('div');
                    row.className = 'rev-round';
                    const inspIcon = ev.inspectorVerdict === 'pass' ? '✅' : ev.inspectorVerdict === 'revise' ? '❌' : '❓';
                    const ceoIcon = ev.ceoVerdict === 'pass' ? '✅' : ev.ceoVerdict === 'abort' ? '🛑' : ev.ceoVerdict === 'revise' ? '🔁' : '❓';
                    row.innerHTML = `<div class="rev-round-head">라운드 ${ev.round} <span class="cph-meta">${(ev.durationMs/1000).toFixed(1)}s</span></div>
                        <div class="rev-line"><span class="rev-actor">${inspIcon} 검수</span><span class="rev-body">${fmt((ev.inspectorText || '').slice(0, 1500))}</span></div>
                        <div class="rev-line"><span class="rev-actor">${ceoIcon} CEO</span><span class="rev-body">${fmt((ev.ceoText || '').slice(0, 1000))}</span></div>`;
                    target.appendChild(row);
                }
                return; // 새 카드 만들지 않음
            } else if (ev.phase === 'review-end') {
                // 사이클 종료 라벨을 컨테이너 카드 끝에 붙임 — 별도 카드 X.
                const target = chatEl.querySelector(`.company-phase-card.review[data-stage-id="${CSS.escape(ev.stageId)}"]`);
                if (target) {
                    const tail = document.createElement('div');
                    tail.className = 'rev-end';
                    const label = ev.final === 'pass'
                        ? `✅ 합의 통과 (${ev.rounds}라운드)`
                        : ev.final === 'maxed-out'
                            ? `⚠️ 최대 라운드 도달 — 현 결과로 진행 (${ev.rounds}라운드)`
                            : `🛑 사이클 중단 (${ev.rounds}라운드)`;
                    tail.textContent = label;
                    target.appendChild(tail);
                }
                return;
            } else if (ev.phase === 'awaiting-approval') {
                // 승인 게이트 카드 — 사용자가 클릭할 때까지 대기.
                card.className += ' approval';
                card.dataset.stageId = ev.stageId;
                const head = document.createElement('div');
                head.className = 'cph-head';
                head.innerHTML = `✋ <strong>${escAttr(ev.stageLabel || ev.stageId)}</strong> 완료 — 검토 후 승인해 주세요`;
                const actions = document.createElement('div');
                actions.style.cssText = 'display:flex; gap:6px; margin-top:8px; flex-wrap:wrap;';
                const approveBtn = document.createElement('button');
                approveBtn.className = 'send-btn';
                approveBtn.textContent = '✅ 승인 (다음으로)';
                approveBtn.onclick = () => {
                    vscode.postMessage({ type: 'respondCompanyApproval', stageId: ev.stageId, decision: 'approve' });
                    actions.querySelectorAll('button').forEach((b) => b.disabled = true);
                    approveBtn.textContent = '✅ 승인됨';
                };
                const reviseBtn = document.createElement('button');
                reviseBtn.className = 'secondary-btn';
                reviseBtn.textContent = '✎ 수정 요청';
                reviseBtn.onclick = () => {
                    const comment = prompt('어떤 부분을 수정하면 좋을까요? (담당 에이전트에게 전달됩니다)', '');
                    if (comment === null) return; // 취소
                    vscode.postMessage({ type: 'respondCompanyApproval', stageId: ev.stageId, decision: 'revise', comment });
                    actions.querySelectorAll('button').forEach((b) => b.disabled = true);
                    reviseBtn.textContent = '✎ 수정 요청 전송됨';
                };
                const abortBtn = document.createElement('button');
                abortBtn.className = 'secondary-btn';
                abortBtn.textContent = '🛑 중단';
                abortBtn.title = '여기서 파이프라인 중단';
                abortBtn.onclick = () => {
                    if (!confirm('이 라운드를 여기서 중단할까요? 이미 완료된 stage 결과는 보존됩니다.')) return;
                    vscode.postMessage({ type: 'respondCompanyApproval', stageId: ev.stageId, decision: 'abort' });
                    actions.querySelectorAll('button').forEach((b) => b.disabled = true);
                    abortBtn.textContent = '🛑 중단 요청';
                };
                actions.appendChild(approveBtn);
                actions.appendChild(reviseBtn);
                actions.appendChild(abortBtn);
                card.innerHTML = '';
                card.appendChild(head);
                card.appendChild(actions);
            } else if (ev.phase === 'approval-resolved') {
                // 이전에 그려둔 awaiting-approval 카드를 갱신 — 별도 카드를 또 만들지 않음.
                const prev = chatEl.querySelector(`.company-phase-card.approval[data-stage-id="${CSS.escape(ev.stageId)}"]`);
                if (prev) {
                    const tail = document.createElement('div');
                    tail.className = 'cph-meta';
                    tail.style.marginTop = '6px';
                    const label = ev.decision === 'approve' ? '✅ 승인됨' : ev.decision === 'revise' ? '✎ 수정 요청됨' : '🛑 사용자가 중단';
                    tail.textContent = label;
                    prev.appendChild(tail);
                }
                return; // 새 카드 안 만듦
            } else if (ev.phase === 'report-start') {
                card.innerHTML = '<div class="cph-head">🧭 CEO 종합 보고서 작성 중…</div>';
            } else if (ev.phase === 'report-done') {
                card.className += ' report';
                card.innerHTML = `<div class="cph-head">🧭 CEO 보고서${ev.ok ? '' : ' (fallback)'}</div>
                    <div class="markdown-body">${fmt(ev.report || '')}</div>`;
            } else if (ev.phase === 'telegram-mirror') {
                // Reflect whether the secretary actually mirrored the round
                // to Telegram. `ok === null` = the user hasn't opted in to
                // Telegram at all (no token / chat id / enabled). We render
                // that as a quiet line instead of an error to avoid nagging.
                if (ev.ok === true) {
                    card.innerHTML = '<div class="cph-meta">📱 영숙이 텔레그램에 보고 완료</div>';
                } else if (ev.ok === false) {
                    card.innerHTML = `<div class="cph-meta">⚠️ 텔레그램 보고 실패${ev.reason ? ` — ${escAttr(ev.reason)}` : ''}</div>`;
                } else {
                    // null → not configured. Skip rendering entirely to keep chat clean.
                    return;
                }
            } else if (ev.phase === 'session-saved') {
                card.innerHTML = `<div class="cph-meta">세션 저장 완료 — 클릭하여 열기</div>`;
                card.style.cursor = 'pointer';
                card.onclick = () => vscode.postMessage({ type: 'openCompanySession', sessionDir: ev.sessionDir });
            } else if (ev.phase === 'aborted') {
                card.innerHTML = `<div class="cph-head">⛔ 회사 모드 중단</div><div class="cph-meta">${escAttr(ev.reason)}</div>`;
            }
            chatEl.appendChild(card);
            chatEl.scrollTop = chatEl.scrollHeight;
        }

        // ── Knowledge Mix: global slider ──────────────────────────────────────
        // Mirrors `g1nation.knowledgeMix.secondBrainWeight`. The hint label updates
        // live as the user drags; the value is committed (postMessage) on `change`
        // so we don't spam settings updates while scrubbing.
        const knowledgeMixSlider = document.getElementById('knowledgeMixSlider');
        const knowledgeMixHint = document.getElementById('knowledgeMixHint');
        const renderGlobalMixHint = () => {
            if (!knowledgeMixSlider || !knowledgeMixHint) return;
            knowledgeMixHint.textContent = fmtMixHint(parseInt(knowledgeMixSlider.value, 10) || 50);
        };
        if (knowledgeMixSlider) {
            knowledgeMixSlider.addEventListener('input', renderGlobalMixHint);
            knowledgeMixSlider.addEventListener('change', () => {
                const w = Math.max(0, Math.min(100, parseInt(knowledgeMixSlider.value, 10) || 50));
                vscode.postMessage({ type: 'setKnowledgeMix', value: w });
            });
            renderGlobalMixHint();
        }

        // --- Proactive Behavioral Tracking ---
        let hoverTimer = null;
        const trackBehavior = (elementId, context) => {
            const el = document.getElementById(elementId);
            if (!el) return;
            el.addEventListener('mouseenter', () => {
                hoverTimer = setTimeout(() => {
                    vscode.postMessage({ type: 'proactiveTrigger', context: context });
                }, 5000); // 5 seconds threshold
            });
            el.addEventListener('mouseleave', () => {
                if (hoverTimer) clearTimeout(hoverTimer);
            });
        };

        trackBehavior('settingsBtn', 'settings_exploration');
        trackBehavior('brainBtn', 'brain_sync_exploration');
        trackBehavior('agentSel', 'agent_selection_exploration');
