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 => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); } function fmtMixHint(w) { return `Model ${100 - w}% · Brain ${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) || '—'; } function syncRecordsLine() { if (!recordsLatest) return; const opt = chronicleRecordSel && chronicleRecordSel.value ? selText(chronicleRecordSel) : ''; recordsLatest.textContent = opt ? '· ' + truncMid(opt, 38) : ''; } // ── 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(`${escAttr(tag)}`); } if (s.model && s.model.name) { const loaded = s.model.loaded; segs.push(`${escAttr(shortModel(s.model.name))}`); } if (s.brain && typeof s.brain.files === 'number') { segs.push(`Brain ${s.brain.files}`); } if (typeof s.contextLength === 'number') { if (s.cappedForSmallModel) { segs.push(`ctx ${fmtK(s.contextLength)} · 소형모델 제한`); } else { segs.push(`ctx ${fmtK(s.contextLength)}`); } } segs.push(`메모리 ${s.memory ? 'On' : 'Off'}`); if (s.multiAgent) segs.push(`멀티에이전트`); rbContent.innerHTML = segs.join('·'); if (rbDot) { const on = s.engine && s.engine.online; rbDot.className = 'rb-dot ' + (on === true ? 'ok' : on === false ? 'bad' : 'warn'); } } // ── Context-budget badge (직전 요청 기준) ──────────────────────────── 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.tight) parts.push('⚠ 컨텍스트 거의 가득'); const warn = b.tight || b.systemTruncated; ctxBadge.textContent = parts.join(' · '); ctxBadge.className = 'ctx-badge' + (warn ? ' warn' : ' ok'); 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 + ')' : ''}`; } 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 ? `
사유: ${escAttr(String(v.reason))}
` : ''; // 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 = `
${escAttr(titleText)}
` + reasonLine + `
`; 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 = `🔎 참조 지식 없음 — 모델 자체 지식으로 답변`; } 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 ? ` · 파일 ${files.length}` : ''; const layerTag = layers.length ? ` · 메모리 ${escAttr(layers.join('·'))}` : ''; const lessonTag = lessons.length ? ` · ⚠ 교훈 ${lessons.length}` : ''; const titleAttr = files.length ? `사용된 브레인 파일:\n${files.join('\n')}` : '에이전트↔지식 매핑 편집'; footer.innerHTML = `🔎 참조: ${agentTag}${escAttr(scopeLabel)}${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}% (${escAttr(srcLabel)})`; 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('
'); const w = document.createElement('div'); w.className = 'scope-unaddressed'; w.innerHTML = `⚠ 답변에서 안 보이는 교훈 체크리스트 항목:
${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 ? `Use global · ${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 internetEnabled = false; 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 ? '
U
You' : '
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 '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(); 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': chat.innerHTML = '
Welcome to Astra

Your premium local AI assistant.
Ready to analyze projects and build reports.

'; 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(); break; } case 'brainProfiles': brainSel.innerHTML = ''; msg.value.profiles.forEach(p => { const o = document.createElement('option'); o.value = p.id; o.innerText = p.name; if (p.id === msg.value.activeBrainId) o.selected = true; brainSel.appendChild(o); }); const addOpt = document.createElement('option'); addOpt.value = 'new'; addOpt.innerText = '+ Add New Brain...'; brainSel.appendChild(addOpt); 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 = `
${s.title}
${new Date(s.timestamp).toLocaleString()} · ${s.messageCount} msgs
`; 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 '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 '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 'agentsList': agentSel.innerHTML = ''; 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 || ''); break; } case 'companyAgents': { renderCompanyAgentCards(msg.value || {}); break; } case 'openCompanyManageOverlay': { // Triggered by the Command Palette `Manage 1인 기업 Agents`. document.getElementById('companyOverlay')?.classList.add('visible'); vscode.postMessage({ type: 'getCompanyAgents' }); break; } case 'companyTurnUpdate': { if (msg.value) renderCompanyPhase(msg.value); break; } case 'architectureStatus': { // Show / hide the chip + reflect current state. 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-active', 'false'); break; } chip.setAttribute('data-active', 'true'); 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}`; break; } case 'architectureRefreshFailed': { const reason = msg.value && msg.value.reason; if (reason === 'no-active-project') { showToast('활성 프로젝트가 없습니다. 먼저 프로젝트를 선택하세요.', 'warn'); } else { showToast('Architecture 갱신 실패', 'warn'); } 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 = '
🛡️ 작업 승인 대기 중 (Action Approval Required)
' + '
위의 변경 사항을 프로젝트에 반영할까요?
' + '
' + ' ' + ' ' + '
'; 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 = `📎 ${f.name} `; 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, internet: internetEnabled, 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(); }); 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('internetBtn').onclick = () => { internetEnabled = !internetEnabled; document.getElementById('internetBtn').classList.toggle('active', internetEnabled); }; 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(); }; 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' }); editBrainBtn.onclick = () => { if (!brainSel.value || brainSel.value === 'new') return; vscode.postMessage({ type: 'editBrain', id: brainSel.value }); }; deleteBrainBtn.onclick = () => { if (!brainSel.value || brainSel.value === 'new') return; vscode.postMessage({ type: 'deleteBrain', id: brainSel.value }); }; // (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') { vscode.postMessage({ type: 'addBrain' }); } else { 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