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'); 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 '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(); 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 = ''; // [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 : []); msg.value.models.forEach(m => { const o = document.createElement('option'); o.value = m; // ● = 현재 LM Studio 메모리에 로드된 모델 / ○ = 다운로드만 됨 o.innerText = _loadedSet.has(m) ? `● ${m}` : m; if (m === _preferredModel) o.selected = true; modelSel.appendChild(o); }); // LocalStorage에 저장된 모델이 실제로 적용된 경우, 백엔드 설정도 동기화 if (_savedModel && _savedModel !== msg.value.selected && msg.value.models.includes(_savedModel)) { vscode.postMessage({ type: 'model', value: _savedModel }); } if (typeof updateInputPlaceholder === 'function') updateInputPlaceholder(); statusLabel.innerText = `Model: ${_preferredModel}`; 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); 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'; 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 }); 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); 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); 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); }); 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; document.getElementById('inputNewChatBtn').onclick = startNewChat; 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 }); }; document.getElementById('inputSyncBtn').onclick = syncBrain; 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'); const updateInputPlaceholder = () => { if (typeof input !== 'undefined' && input) { input.placeholder = `Ask ${modelSel ? modelSel.value : 'AI'}...`; } }; modelSel.onchange = () => { const _selectedModel = modelSel.value; // [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 }); updateInputPlaceholder(); // 상태 레이블 즉시 업데이트 statusLabel.innerText = `Model: ${_selectedModel}`; }; 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 = () => vscode.postMessage({ type: 'editKnowledgeMap' }); } if (reloadKnowledgeMapBtn) { reloadKnowledgeMapBtn.onclick = () => vscode.postMessage({ type: 'getKnowledgeScope', agentPath: agentSel.value }); } 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 }); }; vscode.postMessage({ type: 'getModels' }); vscode.postMessage({ type: 'getAgents' }); vscode.postMessage({ type: 'getChronicleProjects' }); vscode.postMessage({ type: 'getChronicleRecords' }); vscode.postMessage({ type: 'ready' }); // --- 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');