release: v2.0.1 - Advanced Knowledge Mix & Architectural Intelligence
This commit is contained in:
+264
-16
@@ -179,6 +179,23 @@
|
||||
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';
|
||||
@@ -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 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) => {
|
||||
@@ -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 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;
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user