release: v2.0.1 - Advanced Knowledge Mix & Architectural Intelligence

This commit is contained in:
g1nation
2026-05-13 22:05:39 +09:00
parent c32b17377b
commit e85e11aac6
23 changed files with 1758 additions and 78 deletions
+264 -16
View File
@@ -179,6 +179,23 @@
const recordsLatest = document.getElementById('recordsLatest');
function escAttr(t) { return String(t == null ? '' : t).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }
function fmtMixHint(w) { return `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';
@@ -335,6 +352,19 @@
const titleAttr = files.length ? `사용된 브레인 파일:\n${files.join('\n')}` : '에이전트↔지식 매핑 편집';
footer.innerHTML = `<span class="scope-link" data-act="map" title="${escAttr(titleAttr)}">🔎 참조: ${agentTag}${escAttr(scopeLabel)}</span>${fileTag}${lessonTag}${layerTag}`;
}
// Knowledge Mix indicator — shows the policy that actually drove this turn so the
// user can see *why* the answer leaned the way it did.
if (v.knowledgeMix && typeof v.knowledgeMix.weight === 'number') {
const w = Math.max(0, Math.min(100, v.knowledgeMix.weight));
const src = v.knowledgeMix.source;
const srcLabel = src === 'agent'
? `agent: ${v.knowledgeMix.agent || v.agentName || ''}`
: src === 'global' ? 'global' : 'default';
const mix = document.createElement('div');
mix.className = 'scope-mix';
mix.innerHTML = `🎚 Knowledge Mix · Model ${100 - w}% / Brain ${w}% <span class="scope-dim">(${escAttr(srcLabel)})</span>`;
footer.appendChild(mix);
}
// Non-blocking flag: lesson Prevention-Checklist items the answer doesn't visibly address.
const unaddressed = Array.isArray(v.unaddressedChecklist) ? v.unaddressedChecklist : [];
if (unaddressed.length) {
@@ -353,7 +383,66 @@
if (actions) target.insertBefore(footer, actions); else target.appendChild(footer);
}
let agentMapDraft = { agentPath: '', name: '', knowledgeFolders: [], skillFolders: [] };
// `model: ''` means "Use current model" (i.e. no per-agent override).
// `secondBrainWeight: null` means "Use global setting"; a number 0100 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) => {
@@ -393,10 +482,12 @@
}
agentMapStatus.className = 'map-status';
agentMapStatus.textContent = '불러오는 중...';
agentMapDraft = { agentPath: agentSel.value, name: agentSel.options[agentSel.selectedIndex]?.text || '', knowledgeFolders: [], skillFolders: [] };
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 });
}
@@ -569,6 +660,8 @@
break;
case 'modelsList': {
modelSel.innerHTML = '';
const inlineModelSel = document.getElementById('inlineModelSel');
if (inlineModelSel) inlineModelSel.innerHTML = '';
// [State Persistence - Tier 2] LocalStorage에서 마지막 선택 모델 복원 시도
const _savedModel = localStorage.getItem('g1nation_last_model');
// 서버 추천 모델 vs 로컬 저장 모델 중 우선순위 결정
@@ -577,13 +670,21 @@
? _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);
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)) {
@@ -591,6 +692,8 @@
}
if (typeof updateInputPlaceholder === 'function') updateInputPlaceholder();
statusLabel.innerText = `Model: ${_preferredModel}`;
// Refresh per-agent model dropdown options (if currently visible) so it stays in sync.
if (typeof refreshAgentMapModelOptions === 'function') refreshAgentMapModelOptions();
break;
}
case 'brainProfiles':
@@ -672,6 +775,71 @@
vscode.postMessage({ type: 'getKnowledgeScope', agentPath: msg.selected });
syncContextBar();
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('inlineModelSel');
// 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);
statusLabel.innerText = `Model: ${pinned} (agent override)`;
if (typeof updateInputPlaceholder === 'function') updateInputPlaceholder();
}
break;
}
case 'agentMapData':
if (msg.value) {
agentMapDraft = {
@@ -679,8 +847,15 @@
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';
}
@@ -936,7 +1111,8 @@
const startNewChat = () => { Sound.play(660, 'sine', 0.1); vscode.postMessage({ type: 'newChat' }); };
document.getElementById('newChatBtn').onclick = startNewChat;
document.getElementById('inputNewChatBtn').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 = () => {
@@ -969,7 +1145,7 @@
if (!brainSel.value || brainSel.value === 'new') return;
vscode.postMessage({ type: 'deleteBrain', id: brainSel.value });
};
document.getElementById('inputSyncBtn').onclick = syncBrain;
// (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');
@@ -979,20 +1155,31 @@
}
};
modelSel.onchange = () => {
const _selectedModel = modelSel.value;
// Shared handler so the top-bar dropdown and the inline-below-input 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);
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 });
vscode.postMessage({ type: 'model', value: selectedModel });
// Mirror the value to the *other* dropdown so both pickers reflect reality.
const inlineSel = document.getElementById('inlineModelSel');
if (originEl !== modelSel && modelSel.value !== selectedModel) modelSel.value = selectedModel;
if (inlineSel && originEl !== inlineSel && inlineSel.value !== selectedModel) inlineSel.value = selectedModel;
updateInputPlaceholder();
// 상태 레이블 즉시 업데이트
statusLabel.innerText = `Model: ${_selectedModel}`;
statusLabel.innerText = `Model: ${selectedModel}`;
};
modelSel.onchange = () => applyModelSelection(modelSel.value, modelSel);
const _inlineModelSelEl = document.getElementById('inlineModelSel');
if (_inlineModelSelEl) {
_inlineModelSelEl.onchange = () => applyModelSelection(_inlineModelSelEl.value, _inlineModelSelEl);
}
brainSel.onchange = () => {
if (brainSel.value === 'new') {
vscode.postMessage({ type: 'addBrain' });
@@ -1057,9 +1244,41 @@
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 0100 = 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;
@@ -1129,8 +1348,37 @@
vscode.postMessage({ type: 'getAgents' });
vscode.postMessage({ type: 'getChronicleProjects' });
vscode.postMessage({ type: 'getChronicleRecords' });
vscode.postMessage({ type: 'getKnowledgeMix' });
vscode.postMessage({ type: 'getArchitectureStatus' });
vscode.postMessage({ type: 'ready' });
// ── Project Architecture chip buttons ─────────────────────────────────
const _archOpenBtn = document.getElementById('archOpenBtn');
const _archRefreshBtn = document.getElementById('archRefreshBtn');
const _archDetachBtn = document.getElementById('archDetachBtn');
if (_archOpenBtn) _archOpenBtn.onclick = () => vscode.postMessage({ type: 'openArchitectureDoc' });
if (_archRefreshBtn) _archRefreshBtn.onclick = () => vscode.postMessage({ type: 'refreshArchitecture' });
if (_archDetachBtn) _archDetachBtn.onclick = () => vscode.postMessage({ type: 'detachArchitecture' });
// ── Knowledge Mix: global slider ──────────────────────────────────────
// Mirrors `g1nation.knowledgeMix.secondBrainWeight`. The hint label updates
// live as the user drags; the value is committed (postMessage) on `change`
// so we don't spam settings updates while scrubbing.
const knowledgeMixSlider = document.getElementById('knowledgeMixSlider');
const knowledgeMixHint = document.getElementById('knowledgeMixHint');
const renderGlobalMixHint = () => {
if (!knowledgeMixSlider || !knowledgeMixHint) return;
knowledgeMixHint.textContent = fmtMixHint(parseInt(knowledgeMixSlider.value, 10) || 50);
};
if (knowledgeMixSlider) {
knowledgeMixSlider.addEventListener('input', renderGlobalMixHint);
knowledgeMixSlider.addEventListener('change', () => {
const w = Math.max(0, Math.min(100, parseInt(knowledgeMixSlider.value, 10) || 50));
vscode.postMessage({ type: 'setKnowledgeMix', value: w });
});
renderGlobalMixHint();
}
// --- Proactive Behavioral Tracking ---
let hoverTimer = null;
const trackBehavior = (elementId, context) => {