(function () { const vscode = acquireVsCodeApi(); const $ = (id) => document.getElementById(id); // ---- Telegram ---- const tokenInput = $('tgToken'); const saveBtn = $('tgSaveToken'); const clearBtn = $('tgClearToken'); const testBtn = $('tgTest'); const enabledChk = $('tgEnabled'); const enrollBtn = $('tgEnroll'); const enrollCancelBtn = $('tgEnrollCancel'); const enrollStatus = $('tgEnrollStatus'); const tokenStatus = $('tgTokenStatus'); const botName = $('tgBotName'); const tgFeedback = $('tgFeedback'); const tgError = $('tgError'); const chatList = $('tgChatIds'); // ---- Connection ---- const cnUrl = $('cnUrl'); const cnModel = $('cnModel'); const cnTimeout = $('cnTimeout'); const cnRefreshModels = $('cnRefreshModels'); const cnModelHint = $('cnModelHint'); // ---- Datacollect ---- const dcBridgeTarget = $('dcBridgeTarget'); const dcBridgeUrl = $('dcBridgeUrl'); const dcBridgeNasUrl = $('dcBridgeNasUrl'); const dcBridgeNasToken = $('dcBridgeNasToken'); const dcSavePath = $('dcSavePath'); const dcCrawlDepth = $('dcCrawlDepth'); const dcMaxPages = $('dcMaxPages'); const dcSynthTemp = $('dcSynthTemp'); // ---- Memory ---- const memEnabled = $('memEnabled'); const memShort = $('memShort'); const memMid = $('memMid'); const memLong = $('memLong'); // ---- Brain ---- const brainName = $('brainName'); const brainPath = $('brainPath'); const brainAutoPush = $('brainAutoPush'); // ---- Advanced ---- const advDryRun = $('advDryRun'); const advMulti = $('advMulti'); const advAutoSteps = $('advAutoSteps'); const advCtxSize = $('advCtxSize'); const advChatTemp = $('advChatTemp'); const advChunkedSwitch = $('advChunkedSwitch'); const advChunkedMax = $('advChunkedMax'); const advPolishPersona = $('advPolishPersona'); // ---- Google (Calendar + Sheets) ---- const gClientId = $('gClientId'); const gClientSecret = $('gClientSecret'); const gCalendarId = $('gCalendarId'); const gDefaultDur = $('gDefaultDur'); const gIcalUrl = $('gIcalUrl'); const gIcalDays = $('gIcalDays'); const googleConnStatus = $('googleConnStatus'); const googleConnStatusInline = $('googleConnStatusInline'); const googleConnectBtn = $('googleConnect'); const googleDisconnectBtn = $('googleDisconnect'); const googleIcalRefreshBtn = $('googleIcalRefresh'); const googleIcalStatus = $('googleIcalStatus'); const googleFeedback = $('googleFeedback'); const googleError = $('googleError'); // ---- Devil Agent ---- const devilEnabled = $('devilEnabled'); // ---- Cloud LLM Providers ---- const prOpenrouterEnabled = $('prOpenrouterEnabled'); const prOpenrouterKey = $('prOpenrouterKey'); const prOpenrouterDefault = $('prOpenrouterDefault'); const prAnthropicEnabled = $('prAnthropicEnabled'); const prAnthropicKey = $('prAnthropicKey'); const prAnthropicDefault = $('prAnthropicDefault'); const prGeminiEnabled = $('prGeminiEnabled'); const prGeminiKey = $('prGeminiKey'); const prGeminiDefault = $('prGeminiDefault'); // ---- Banner ---- const bannerError = $('bannerError'); // ---- Telegram listeners ---- saveBtn.addEventListener('click', () => { const t = (tokenInput.value || '').trim(); if (!t) return; vscode.postMessage({ type: 'telegram.saveToken', token: t }); tokenInput.value = ''; }); clearBtn.addEventListener('click', () => vscode.postMessage({ type: 'telegram.clearToken' })); testBtn.addEventListener('click', () => vscode.postMessage({ type: 'telegram.testConnection' })); enabledChk.addEventListener('change', (e) => vscode.postMessage({ type: 'telegram.toggleEnabled', enabled: e.target.checked }) ); enrollBtn.addEventListener('click', () => vscode.postMessage({ type: 'telegram.enroll' })); enrollCancelBtn.addEventListener('click', () => vscode.postMessage({ type: 'telegram.cancelEnroll' })); chatList.addEventListener('click', (e) => { if (!(e.target instanceof HTMLElement)) return; if (e.target.classList.contains('remove')) { const id = Number(e.target.dataset.id); if (Number.isFinite(id)) vscode.postMessage({ type: 'telegram.removeChatId', chatId: id }); } }); // ---- Connection listeners ---- document.querySelector('[data-save="connection.url"]').addEventListener('click', () => vscode.postMessage({ type: 'connection.update', ollamaUrl: cnUrl.value }) ); document.querySelector('[data-save="connection.model"]').addEventListener('click', () => vscode.postMessage({ type: 'connection.update', defaultModel: cnModel.value }) ); cnRefreshModels.addEventListener('click', () => vscode.postMessage({ type: 'connection.update', refreshModels: true }) ); cnModel.addEventListener('change', () => vscode.postMessage({ type: 'connection.update', defaultModel: cnModel.value }) ); document.querySelector('[data-save="connection.timeout"]').addEventListener('click', () => vscode.postMessage({ type: 'connection.update', requestTimeout: Number(cnTimeout.value) }) ); // ---- Datacollect listeners ---- document.querySelector('[data-save="datacollect.bridgeTarget"]').addEventListener('click', () => vscode.postMessage({ type: 'datacollect.update', bridgeTarget: dcBridgeTarget.value }) ); document.querySelector('[data-save="datacollect.bridgeUrl"]').addEventListener('click', () => vscode.postMessage({ type: 'datacollect.update', bridgeUrl: dcBridgeUrl.value }) ); document.querySelector('[data-save="datacollect.bridgeNasUrl"]').addEventListener('click', () => vscode.postMessage({ type: 'datacollect.update', bridgeNasUrl: dcBridgeNasUrl.value }) ); document.querySelector('[data-save="datacollect.bridgeNasToken"]').addEventListener('click', () => vscode.postMessage({ type: 'datacollect.update', bridgeNasToken: dcBridgeNasToken.value }) ); document.querySelector('[data-save="datacollect.savePath"]').addEventListener('click', () => vscode.postMessage({ type: 'datacollect.update', savePath: dcSavePath.value }) ); document.querySelector('[data-save="datacollect.crawlDepth"]').addEventListener('click', () => vscode.postMessage({ type: 'datacollect.update', crawlDepth: Number(dcCrawlDepth.value) }) ); document.querySelector('[data-save="datacollect.maxPages"]').addEventListener('click', () => vscode.postMessage({ type: 'datacollect.update', maxPages: Number(dcMaxPages.value) }) ); document.querySelector('[data-save="datacollect.synthesisTemperature"]').addEventListener('click', () => vscode.postMessage({ type: 'datacollect.update', synthesisTemperature: Number(dcSynthTemp.value) }) ); // ---- Memory listeners ---- memEnabled.addEventListener('change', (e) => vscode.postMessage({ type: 'memory.update', memoryEnabled: e.target.checked }) ); document.querySelector('[data-save="memory.short"]').addEventListener('click', () => vscode.postMessage({ type: 'memory.update', memoryShortTermMessages: Number(memShort.value) }) ); document.querySelector('[data-save="memory.mid"]').addEventListener('click', () => vscode.postMessage({ type: 'memory.update', memoryMediumTermSessions: Number(memMid.value) }) ); document.querySelector('[data-save="memory.long"]').addEventListener('click', () => vscode.postMessage({ type: 'memory.update', memoryLongTermFiles: Number(memLong.value) }) ); // ---- Brain listeners ---- brainAutoPush.addEventListener('change', (e) => vscode.postMessage({ type: 'brain.update', autoPushBrain: e.target.checked }) ); $('brainOpenSettings').addEventListener('click', () => vscode.postMessage({ type: 'openVscodeSettings' }) ); // ---- Advanced listeners ---- advDryRun.addEventListener('change', (e) => vscode.postMessage({ type: 'advanced.update', dryRun: e.target.checked }) ); advMulti.addEventListener('change', (e) => vscode.postMessage({ type: 'advanced.update', multiAgentEnabled: e.target.checked }) ); document.querySelector('[data-save="advanced.autoSteps"]').addEventListener('click', () => vscode.postMessage({ type: 'advanced.update', maxAutoSteps: Number(advAutoSteps.value) }) ); // ---- Google listeners ---- document.querySelector('[data-save="google.clientId"]').addEventListener('click', () => vscode.postMessage({ type: 'google.update', clientId: gClientId.value }) ); document.querySelector('[data-save="google.clientSecret"]').addEventListener('click', () => { // 저장 후 입력 필드는 비움 — 다음부터는 placeholder 가 "저장됨" 으로 표시됨. vscode.postMessage({ type: 'google.update', clientSecret: gClientSecret.value }); gClientSecret.value = ''; }); document.querySelector('[data-save="google.calendarId"]').addEventListener('click', () => vscode.postMessage({ type: 'google.update', calendarId: gCalendarId.value }) ); document.querySelector('[data-save="google.defaultEventDurationMinutes"]').addEventListener('click', () => vscode.postMessage({ type: 'google.update', defaultEventDurationMinutes: Number(gDefaultDur.value) }) ); document.querySelector('[data-save="google.icalUrl"]').addEventListener('click', () => { vscode.postMessage({ type: 'google.update', icalUrl: gIcalUrl.value }); gIcalUrl.value = ''; }); document.querySelector('[data-save="google.icalDaysAhead"]').addEventListener('click', () => vscode.postMessage({ type: 'google.update', icalDaysAhead: Number(gIcalDays.value) }) ); googleConnectBtn.addEventListener('click', () => vscode.postMessage({ type: 'google.connect' })); googleDisconnectBtn.addEventListener('click', () => vscode.postMessage({ type: 'google.disconnect' })); googleIcalRefreshBtn.addEventListener('click', () => vscode.postMessage({ type: 'google.icalRefresh' })); // ---- Devil Agent listener ---- devilEnabled.addEventListener('change', (e) => vscode.postMessage({ type: 'devilAgent.toggle', enabled: e.target.checked }) ); // ---- Cloud LLM Providers listeners ---- prOpenrouterEnabled.addEventListener('change', (e) => vscode.postMessage({ type: 'providers.update', providerId: 'openrouter', enabled: e.target.checked }) ); document.querySelector('[data-save="providers.openrouter.apiKey"]').addEventListener('click', () => { vscode.postMessage({ type: 'providers.update', providerId: 'openrouter', apiKey: prOpenrouterKey.value }); prOpenrouterKey.value = ''; }); document.querySelector('[data-save="providers.openrouter.defaultModel"]').addEventListener('click', () => vscode.postMessage({ type: 'providers.update', providerId: 'openrouter', defaultModel: prOpenrouterDefault.value }) ); prAnthropicEnabled.addEventListener('change', (e) => vscode.postMessage({ type: 'providers.update', providerId: 'anthropic', enabled: e.target.checked }) ); document.querySelector('[data-save="providers.anthropic.apiKey"]').addEventListener('click', () => { vscode.postMessage({ type: 'providers.update', providerId: 'anthropic', apiKey: prAnthropicKey.value }); prAnthropicKey.value = ''; }); document.querySelector('[data-save="providers.anthropic.defaultModel"]').addEventListener('click', () => vscode.postMessage({ type: 'providers.update', providerId: 'anthropic', defaultModel: prAnthropicDefault.value }) ); prGeminiEnabled.addEventListener('change', (e) => vscode.postMessage({ type: 'providers.update', providerId: 'gemini', enabled: e.target.checked }) ); document.querySelector('[data-save="providers.gemini.apiKey"]').addEventListener('click', () => { vscode.postMessage({ type: 'providers.update', providerId: 'gemini', apiKey: prGeminiKey.value }); prGeminiKey.value = ''; }); document.querySelector('[data-save="providers.gemini.defaultModel"]').addEventListener('click', () => vscode.postMessage({ type: 'providers.update', providerId: 'gemini', defaultModel: prGeminiDefault.value }) ); document.querySelector('[data-save="advanced.ctxSize"]').addEventListener('click', () => vscode.postMessage({ type: 'advanced.update', maxContextSize: Number(advCtxSize.value) }) ); document.querySelector('[data-save="advanced.chatTemperature"]').addEventListener('click', () => vscode.postMessage({ type: 'advanced.update', chatTemperature: Number(advChatTemp.value) }) ); document.querySelector('[data-save="advanced.chunkedSwitchTokens"]').addEventListener('click', () => vscode.postMessage({ type: 'advanced.update', chunkedSwitchTokens: Number(advChunkedSwitch.value) }) ); document.querySelector('[data-save="advanced.chunkedMaxSections"]').addEventListener('click', () => vscode.postMessage({ type: 'advanced.update', chunkedMaxSections: Number(advChunkedMax.value) }) ); document.querySelector('[data-save="advanced.polishPersonaOverride"]').addEventListener('click', () => vscode.postMessage({ type: 'advanced.update', polishPersonaOverride: String(advPolishPersona.value || '') }) ); // ---- Header ---- $('openVscodeSettings').addEventListener('click', () => vscode.postMessage({ type: 'openVscodeSettings' }) ); // ---- State sync ---- window.addEventListener('message', (e) => { const msg = e.data; if (!msg || msg.type !== 'state') return; renderState(msg.value); }); /** Set input.value only when the field is not currently focused, so user edits aren't clobbered mid-typing. */ function setIfNotFocused(input, value) { if (document.activeElement === input) return; const next = value === undefined || value === null ? '' : String(value); if (input.value !== next) input.value = next; } function renderState(state) { // ---- Banner ---- if (state.bannerError) { bannerError.hidden = false; bannerError.textContent = state.bannerError; } else { bannerError.hidden = true; bannerError.textContent = ''; } // ---- Telegram ---- const tg = state.telegram; tokenStatus.textContent = tg.hasToken ? '저장된 토큰이 있습니다.' : '아직 토큰이 등록되지 않았습니다.'; clearBtn.disabled = !tg.hasToken; if (tg.botName) { botName.textContent = `연결됨: ${tg.botName}` + (tg.connected ? ' · 폴링 중' : ' · 비활성화 상태'); botName.classList.toggle('ok', tg.connected); } else { botName.textContent = ''; botName.classList.remove('ok'); } enabledChk.checked = !!tg.enabled; enabledChk.disabled = !tg.hasToken; if (tg.enrolling) { enrollBtn.hidden = true; enrollCancelBtn.hidden = false; enrollStatus.textContent = '봇에게 메시지를 한 번 보내주세요. 다음 메시지의 채널이 자동 등록됩니다.'; } else { enrollBtn.hidden = false; enrollCancelBtn.hidden = true; enrollStatus.textContent = ''; } enrollBtn.disabled = !tg.hasToken; chatList.innerHTML = ''; if (!tg.allowedChatIds || tg.allowedChatIds.length === 0) { const li = document.createElement('li'); li.className = 'empty'; li.textContent = '등록된 채널 없음'; chatList.appendChild(li); } else { for (const id of tg.allowedChatIds) { const li = document.createElement('li'); li.textContent = String(id); const x = document.createElement('span'); x.className = 'remove'; x.dataset.id = String(id); x.textContent = '✕'; x.title = '허용 목록에서 제거'; li.appendChild(x); chatList.appendChild(li); } } if (tg.lastSuccess) { tgFeedback.hidden = false; tgFeedback.textContent = tg.lastSuccess; } else { tgFeedback.hidden = true; tgFeedback.textContent = ''; } if (tg.lastError) { tgError.hidden = false; tgError.textContent = tg.lastError; } else { tgError.hidden = true; tgError.textContent = ''; } // ---- Connection ---- const cn = state.connection; setIfNotFocused(cnUrl, cn.ollamaUrl); setIfNotFocused(cnTimeout, cn.requestTimeout); // Model dropdown — preserve selection, allow current value even if not in list const wantedModel = cn.defaultModel || ''; const list = Array.isArray(cn.availableModels) ? cn.availableModels.slice() : []; if (wantedModel && !list.includes(wantedModel)) list.unshift(wantedModel); // Only repaint when list actually changed (otherwise we lose the user's // open dropdown state). const currentOptions = Array.from(cnModel.options).map((o) => o.value); const listChanged = currentOptions.length !== list.length || currentOptions.some((v, i) => v !== list[i]); if (listChanged) { cnModel.innerHTML = ''; for (const m of list) { const opt = document.createElement('option'); opt.value = m; opt.textContent = m; cnModel.appendChild(opt); } if (list.length === 0) { const opt = document.createElement('option'); opt.value = ''; opt.textContent = '(모델 없음 — 엔진 연결 확인)'; opt.disabled = true; cnModel.appendChild(opt); } } cnModel.value = wantedModel; cnModelHint.textContent = cn.modelsLoading ? '모델 목록 가져오는 중…' : `사이드바에서 선택한 모델이 여기에도 동기화됩니다. (${list.length}개 발견)`; // ---- Datacollect ---- const dc = state.datacollect; if (dc) { if (dcBridgeTarget && document.activeElement !== dcBridgeTarget && (dc.bridgeTarget === 'local' || dc.bridgeTarget === 'nas')) { dcBridgeTarget.value = dc.bridgeTarget; } setIfNotFocused(dcBridgeUrl, dc.bridgeUrl); setIfNotFocused(dcBridgeNasUrl, dc.bridgeNasUrl); setIfNotFocused(dcBridgeNasToken, dc.bridgeNasToken); setIfNotFocused(dcSavePath, dc.savePath); setIfNotFocused(dcCrawlDepth, dc.crawlDepth); setIfNotFocused(dcMaxPages, dc.maxPages); setIfNotFocused(dcSynthTemp, dc.synthesisTemperature); } // ---- Memory ---- const mem = state.memory; memEnabled.checked = !!mem.memoryEnabled; setIfNotFocused(memShort, mem.memoryShortTermMessages); setIfNotFocused(memMid, mem.memoryMediumTermSessions); setIfNotFocused(memLong, mem.memoryLongTermFiles); // ---- Brain ---- const br = state.brain; brainName.textContent = br.activeBrainName + (br.profileCount > 0 ? ` (전체 ${br.profileCount}개)` : ''); brainPath.textContent = br.activeBrainPath || '경로 없음'; brainAutoPush.checked = !!br.autoPushBrain; // ---- Advanced ---- const adv = state.advanced; advDryRun.checked = !!adv.dryRun; advMulti.checked = !!adv.multiAgentEnabled; setIfNotFocused(advAutoSteps, adv.maxAutoSteps); setIfNotFocused(advCtxSize, adv.maxContextSize); setIfNotFocused(advChatTemp, adv.chatTemperature); setIfNotFocused(advChunkedSwitch, adv.chunkedSwitchTokens); setIfNotFocused(advChunkedMax, adv.chunkedMaxSections); setIfNotFocused(advPolishPersona, adv.polishPersonaOverride); // ---- Google (Calendar + Sheets) ---- const g = state.google; if (g) { setIfNotFocused(gClientId, g.clientId); // Secret 은 값 자체를 화면에 안 그림 — 설정 여부만 placeholder 로 표현. gClientSecret.placeholder = g.hasClientSecret ? '••• 저장됨 (덮어쓰려면 새 값 입력)' : 'GOCSPX-...'; setIfNotFocused(gCalendarId, g.calendarId); setIfNotFocused(gDefaultDur, g.defaultEventDurationMinutes); gIcalUrl.placeholder = g.hasIcalUrl ? '••• 저장됨 (덮어쓰려면 새 URL 입력)' : 'https://calendar.google.com/calendar/ical/.../basic.ics'; setIfNotFocused(gIcalDays, g.icalDaysAhead); // 연결 상태 readout. if (g.connected) { const at = g.connectedAt ? g.connectedAt.slice(0, 16).replace('T', ' ') : ''; googleConnStatus.textContent = `✅ 연결됨${g.connectedAs ? ' · ' + g.connectedAs : ''}${at ? ' (' + at + ')' : ''}`; googleConnStatus.style.color = '#10b981'; googleDisconnectBtn.disabled = false; } else { googleConnStatus.textContent = '⛔ OAuth 연결 안됨 — Client ID/Secret 저장 후 [OAuth 연결] 클릭'; googleConnStatus.style.color = ''; googleDisconnectBtn.disabled = true; } googleIcalStatus.textContent = g.lastIcalFetchAt ? `마지막 새로고침: ${g.lastIcalFetchAt.slice(0, 16).replace('T', ' ')}` : ''; } // ---- Devil Agent ---- if (state.devilAgent) { devilEnabled.checked = !!state.devilAgent.enabled; } // ---- Cloud LLM Providers ---- const pr = state.providers; if (pr) { // OpenRouter prOpenrouterEnabled.checked = !!pr.openrouter.enabled; prOpenrouterKey.placeholder = pr.openrouter.hasApiKey ? '••• 저장됨 (덮어쓰려면 새 값)' : 'sk-or-...'; setIfNotFocused(prOpenrouterDefault, pr.openrouter.defaultModel); // Anthropic prAnthropicEnabled.checked = !!pr.anthropic.enabled; prAnthropicKey.placeholder = pr.anthropic.hasApiKey ? '••• 저장됨 (덮어쓰려면 새 값)' : 'sk-ant-...'; setIfNotFocused(prAnthropicDefault, pr.anthropic.defaultModel); // Gemini prGeminiEnabled.checked = !!pr.gemini.enabled; prGeminiKey.placeholder = pr.gemini.hasApiKey ? '••• 저장됨 (덮어쓰려면 새 값)' : 'AIzaSy...'; setIfNotFocused(prGeminiDefault, pr.gemini.defaultModel); } } // ---- Tabs (카테고리 네비) — 표현 계층만. 기존 refs/리스너/state 에 무영향 ---- (function initTabs() { const tabButtons = Array.from(document.querySelectorAll('.tab')); const sections = Array.from(document.querySelectorAll('.section')); if (tabButtons.length === 0) return; const valid = new Set(tabButtons.map((b) => b.dataset.tab)); function setActiveTab(tab) { tabButtons.forEach((b) => b.classList.toggle('active', b.dataset.tab === tab)); // 숨김만(hidden) — DOM 제거 X. renderState 는 숨겨진 입력에도 값을 채운다. sections.forEach((s) => { s.hidden = s.dataset.tab !== tab; }); try { const st = vscode.getState() || {}; vscode.setState({ ...st, activeTab: tab }); } catch (e) { /* noop */ } } tabButtons.forEach((b) => b.addEventListener('click', () => setActiveTab(b.dataset.tab))); let initial = 'model'; try { const saved = (vscode.getState() || {}).activeTab; if (saved && valid.has(saved)) initial = saved; } catch (e) { /* noop */ } if (!valid.has(initial)) initial = tabButtons[0].dataset.tab; setActiveTab(initial); })(); vscode.postMessage({ type: 'ready' }); })();