chore: v2.2.73 — ASTRA-DEBUG 로그 레벨 + webview CSP font-src 보강

- ASTRA-DEBUG 정상 흐름 로그를 console.error → logInfo/console.log 로 강등
  (chatHandlers, extension, slashRouter): DevTools에 ERR로 찍히던 오탐 제거
- sidebar webview에 명시적 CSP meta 추가 + font-src에 data: 허용
  (sidebar.html, sidebarProvider._getHtml): VS Code outer iframe이 codicon.ttf를
  data:font/ttf 로 inject하면서 기본 CSP에 막혀 매 prompt 마다 violation
  경고가 찍히던 문제 해소
- 누적된 LM Studio / agent / 컨텍스트 매니저 / 테스트 갱신 동반

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
g1nation
2026-05-23 15:52:19 +09:00
parent 36db170844
commit 0712014fcb
43 changed files with 2417 additions and 977 deletions
+13 -20
View File
@@ -2,6 +2,7 @@
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="__CSP__">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Astra</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
@@ -40,7 +41,15 @@
<button class="icon-btn" id="companyManageBtn" data-tooltip="기업 모드 관리 (에이전트 · 모델 · 프롬프트 · 지식 비중)"></button>
<div class="hdr-dropdown" data-dd>
<button class="icon-btn" id="toolsMenuBtn" data-dd-trigger data-tooltip="개발자 도구 모음">도구 ▾</button>
<div class="hdr-menu" id="toolsMenu" data-dd-menu>
<div class="hdr-menu hdr-menu-wide" id="toolsMenu" data-dd-menu>
<div class="hdr-menu-label">자동 기록</div>
<button class="hdr-menu-item toggle-item" id="chronicleAutoRecordBtn" data-tooltip="의미 있는 대화 turn 을 활성 프로젝트의 Chronicle 폴더에 자동 저장">자동 기록: 켜짐</button>
<div class="hdr-menu-hint" id="chronicleAutoStatus" title="가장 최근에 자동 저장된 기록"><span class="status-dot ready"></span> <span id="recordsLatest"></span></div>
<div class="select-wrap"><select id="chronicleRecordSel" title="열어볼 작업 기록 선택"></select></div>
<button class="hdr-menu-item" id="openChronicleRecordBtn" data-tooltip="선택한 기록 열기">선택한 기록 열기</button>
<button class="hdr-menu-item" id="refreshChronicleRecordsBtn" data-tooltip="기록 목록 다시 불러오기">기록 새로고침</button>
<button class="hdr-menu-item" id="openDesignerBtn" data-tooltip="기록이 저장된 폴더 열기">기록 폴더 열기</button>
<div class="hdr-menu-label">도구</div>
<button class="hdr-menu-item toggle-item" id="brainTraceDebugBtn" data-tooltip="근거 추적의 원본 JSON 표시 (개발자용)">근거 추적 JSON 보기</button>
<button class="hdr-menu-item" id="saveWikiRawBtn" data-tooltip="현재 답변의 원본 마크다운을 두뇌(지식)에 저장">원본 답변을 두뇌에 저장</button>
@@ -123,25 +132,9 @@
</div>
</div>
<div class="records-line">
<div class="rl-summary">
<span class="status-dot ready"></span>
<span id="chronicleAutoStatus" title="의미 있는 대화 후 프로젝트 기록이 자동으로 저장됩니다.">자동 기록</span>
<span class="rl-latest" id="recordsLatest"></span>
</div>
<!-- (Removed) Corp chip moved to the header toolbar above —
see #companyChip / #companyManageBtn alongside New/Trace/Web. -->
<div class="hdr-dropdown" data-dd>
<button class="icon-btn" id="recordsMenuBtn" data-dd-trigger data-tooltip="저장된 작업 기록 열기">기록 ▾</button>
<div class="hdr-menu hdr-menu-wide" id="recordsMenu" data-dd-menu>
<div class="hdr-menu-label">작업 기록</div>
<div class="select-wrap"><select id="chronicleRecordSel" title="열어볼 작업 기록 선택"></select></div>
<button class="hdr-menu-item" id="openChronicleRecordBtn" data-tooltip="선택한 기록 열기">선택한 기록 열기</button>
<button class="hdr-menu-item" id="refreshChronicleRecordsBtn" data-tooltip="기록 목록 다시 불러오기">기록 새로고침</button>
<button class="hdr-menu-item" id="openDesignerBtn" data-tooltip="기록이 저장된 폴더 열기">기록 폴더 열기</button>
</div>
</div>
</div>
<!-- v2.2.71 — records-line 전체를 도구 ▾ 드롭다운 안으로 이동. 사이드바 본체엔 더 이상 자동 기록
라벨/selector/기록 ▾ 가 노출되지 않는다. 모든 자동 기록 UI 는 도구 ▾ 메뉴 첫 섹션 (자동 기록) 에서 접근. -->
<!--
Company manage overlay. Uses the same overlay framework as the agent
+144 -9
View File
@@ -359,10 +359,31 @@
try { _renderWelcome(); } catch {}
}
});
// v2.2.70 — 자동 기록 on/off 토글 상태. 도구 ▾ 메뉴의 토글 항목과 records-line 라벨
// (자동 기록 / 자동 기록 (꺼짐)) 모두 이 변수에서 동기화.
let chronicleAutoEnabled = true;
function renderChronicleAutoToggle() {
const btn = document.getElementById('chronicleAutoRecordBtn');
if (btn) {
btn.textContent = '자동 기록: ' + (chronicleAutoEnabled ? '켜짐' : '꺼짐');
btn.classList.toggle('active', chronicleAutoEnabled);
}
// v2.2.71 — chronicleAutoStatus 는 이제 도구 메뉴 안에서 "최근 저장 기록" 표시 컨테이너.
// textContent 를 직접 쓰면 자식(status-dot · recordsLatest)이 지워지므로 opacity / title 만 갱신.
const statusEl = document.getElementById('chronicleAutoStatus');
if (statusEl) {
statusEl.style.opacity = chronicleAutoEnabled ? '' : '0.55';
statusEl.title = chronicleAutoEnabled
? '자동 기록 켜짐 — 의미 있는 대화가 자동 저장됨'
: '자동 기록 꺼짐 — 자동 저장 안 됨 (수동 기록은 가능)';
}
}
function syncRecordsLine() {
if (!recordsLatest) return;
const opt = chronicleRecordSel && chronicleRecordSel.value ? selText(chronicleRecordSel) : '';
recordsLatest.textContent = opt ? '· ' + truncMid(opt, 38) : '';
// OFF 면 최근 기록 라벨도 dim 처리해서 "지금은 저장 안 됨" 이 한눈에 보이게.
recordsLatest.style.opacity = chronicleAutoEnabled ? '' : '0.55';
}
// ── Ready-status bar (Engine / Model / Brain count / Context / Memory) ──
@@ -403,6 +424,8 @@
}
// ── Context-budget badge (직전 요청 기준) ────────────────────────────
// Last LM Studio prediction stats — merged into the badge after the turn finishes.
let lastLmStats = null;
function renderCtxBadge(b) {
if (!ctxBadge) return;
if (!b || typeof b.inputTokens !== 'number') { ctxBadge.textContent = ''; ctxBadge.className = 'ctx-badge'; ctxBadge.title = ''; return; }
@@ -420,8 +443,35 @@
const warn = b.tight || b.systemTruncated;
ctxBadge.textContent = parts.join(' · ');
ctxBadge.className = 'ctx-badge' + (warn ? ' warn' : ' ok');
// New turn starts → drop stale stats from the previous answer.
lastLmStats = null;
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 + ')' : ''}`;
}
function renderLmStudioStats(s) {
if (!ctxBadge || !s) return;
lastLmStats = s;
const extra = [];
if (typeof s.tokensPerSecond === 'number') extra.push(`${s.tokensPerSecond.toFixed(1)} tok/s`);
if (typeof s.timeToFirstTokenSec === 'number') extra.push(`TTFT ${s.timeToFirstTokenSec.toFixed(2)}s`);
// Speculative-decoding hit rate — shows whether the draft model is paying for itself.
// A healthy ratio is ~60%+; below ~30% means the draft is mis-predicting and slowing things down.
if (typeof s.draftTokensCount === 'number' && s.draftTokensCount > 0 && typeof s.acceptedDraftTokensCount === 'number') {
const pct = Math.round((s.acceptedDraftTokensCount / s.draftTokensCount) * 100);
extra.push(`spec ${pct}%`);
}
if (extra.length === 0) return;
// Append to badge without clobbering — only if current badge text doesn't already include it.
const current = ctxBadge.textContent || '';
const tail = ' · ' + extra.join(' · ');
if (!current.endsWith(tail)) ctxBadge.textContent = current + tail;
if (ctxBadge.title) {
ctxBadge.title += `\n\n[LM Studio]\n${extra.join(' · ')}`;
if (typeof s.predictedTokensCount === 'number') ctxBadge.title += `\n출력 ${s.predictedTokensCount} tokens`;
if (typeof s.totalTimeSec === 'number') ctxBadge.title += ` / 총 ${s.totalTimeSec.toFixed(2)}s`;
if (s.draftModelKey) ctxBadge.title += `\ndraft: ${s.draftModelKey} (${s.acceptedDraftTokensCount || 0}/${s.draftTokensCount || 0} accepted)`;
if (s.stopReason) ctxBadge.title += `\nstop: ${s.stopReason}`;
}
}
if (readyBar) {
readyBar.addEventListener('click', e => {
const t = e.target;
@@ -865,18 +915,39 @@
try { _renderWelcome(); } catch {}
break;
}
case 'brainProfiles':
case 'brainProfiles': {
// 방어: profiles 가 비어/null 로 오면 기존 옵션 보존. 잘못된 상태로 dropdown 을 비워
// "+ Add New Brain..." 만 남기는 회귀를 막는다 (v2.2.66 fix).
const profilesArr = (msg.value && Array.isArray(msg.value.profiles)) ? msg.value.profiles : [];
const activeId = msg.value && msg.value.activeBrainId;
if (profilesArr.length === 0) {
console.warn('[Astra] brainProfiles message had empty profiles list — preserving existing dropdown.');
break;
}
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;
profilesArr.forEach(p => {
const o = document.createElement('option');
o.value = p.id;
o.innerText = p.name;
brainSel.appendChild(o);
});
const addOpt = document.createElement('option');
addOpt.value = 'new'; addOpt.innerText = '+ Add New Brain...';
addOpt.value = 'new';
addOpt.innerText = '+ Add New Brain...';
brainSel.appendChild(addOpt);
// v2.2.67 — 옵션을 모두 넣은 *후* selection 을 적용한다. appendChild 이전에 `o.selected = true`
// 를 거는 방식은 일부 Chromium webview 에서 무시되어 dropdown 이 마지막 옵션('+ Add New Brain...')
// 에 머무는 회귀가 발생했다. brainSel.value 로 한 번에 잡으면 이전에 'new' 로 굳어있던 값도 확실히 덮어쓴다.
if (activeId && profilesArr.some(p => p.id === activeId)) {
brainSel.value = activeId;
} else {
brainSel.value = profilesArr[0].id;
}
// 다음에 사용자가 '+ Add New Brain...' 을 클릭하고 취소했을 때 복원할 "이전 유효 선택" 기억.
brainSel.dataset.lastSelected = brainSel.value;
syncContextBar();
break;
}
case 'sessionList':
historyList.innerHTML = '';
msg.value.forEach(s => {
@@ -912,6 +983,9 @@
case 'contextBudget':
renderCtxBadge(msg.value);
break;
case 'lmStudioStats':
renderLmStudioStats(msg.value);
break;
case 'usedScope': {
let target = streamBody && streamBody._parent;
if (!target) {
@@ -924,6 +998,13 @@
case 'lessonCandidate':
renderLessonCandidate(msg.value || {});
break;
case 'chronicleAutoRecordStatus': {
// v2.2.70 — 자동 기록 on/off 상태 푸시. 도구 메뉴 토글과 records-line 라벨 갱신.
chronicleAutoEnabled = !!(msg.value && msg.value.enabled);
renderChronicleAutoToggle();
syncRecordsLine();
break;
}
case 'autoContinue':
statusLabel.innerText = msg.value; thinkingBar.classList.add('active');
if (msg.value.includes('Analyzing')) setStep('analyze');
@@ -931,6 +1012,23 @@
if (msg.value.includes('Executing')) setStep('execute');
setTimeout(() => { thinkingBar.classList.remove('active'); }, 3000);
break;
case 'workflowStage': {
// [5-stage pipeline] 채팅 본문에 단계 메시지를 흘리는 대신, 사이드바 상단의
// 얇은 status strip 에만 한 줄로 표시한다. done=true 면 strip 을 닫는다.
const v = msg.value || {};
const step = String(v.step || '');
const text = String(v.message || '');
const done = !!v.done;
if (done || (!step && !text)) {
statusLabel.innerText = '';
thinkingBar.classList.remove('active');
} else {
const compact = step && text ? step + ' · ' + text : (step || text);
statusLabel.innerText = compact;
thinkingBar.classList.add('active');
}
break;
}
case 'agentsList':
agentSel.innerHTML = '<option value="none">No Agent</option>';
msg.value.forEach(a => {
@@ -1655,18 +1753,44 @@
btn.setAttribute('data-tooltip', secondBrainTraceDebug ? 'Second Brain Debug JSON: On' : 'Second Brain Debug JSON: Off');
saveUiState();
};
// v2.2.70 — 자동 기록 토글. 클릭 시 즉시 서버에 새 상태 전송. 서버가 config 저장 후
// chronicleAutoRecordStatus 메시지로 다시 푸시 → renderChronicleAutoToggle 가 UI 동기화.
const _chronicleAutoBtn = document.getElementById('chronicleAutoRecordBtn');
if (_chronicleAutoBtn) {
_chronicleAutoBtn.onclick = () => {
const next = !chronicleAutoEnabled;
// 낙관적 갱신 — 서버 응답 전에도 즉시 라벨이 바뀌어 클릭 반응성이 좋다. 응답이 오면 다시 정정.
chronicleAutoEnabled = next;
renderChronicleAutoToggle();
syncRecordsLine();
vscode.postMessage({ type: 'setChronicleAutoRecord', enabled: next });
};
}
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' });
// v2.2.67 — 만약 dropdown 이 'new' 상태로 굳어있어도 수정/삭제는 직전 유효 선택(또는 첫 실제 옵션)
// 을 기준으로 동작하도록 폴백. 이전엔 'new' 인 순간 그냥 early-return 해서 "수정 안 됨" 버그 발생.
function _resolveActiveBrainId() {
if (brainSel.value && brainSel.value !== 'new') return brainSel.value;
const last = brainSel.dataset.lastSelected;
if (last && last !== 'new') return last;
for (const opt of brainSel.options) {
if (opt.value && opt.value !== 'new') return opt.value;
}
return '';
}
editBrainBtn.onclick = () => {
if (!brainSel.value || brainSel.value === 'new') return;
vscode.postMessage({ type: 'editBrain', id: brainSel.value });
const id = _resolveActiveBrainId();
if (!id) return;
vscode.postMessage({ type: 'editBrain', id });
};
deleteBrainBtn.onclick = () => {
if (!brainSel.value || brainSel.value === 'new') return;
vscode.postMessage({ type: 'deleteBrain', id: brainSel.value });
const id = _resolveActiveBrainId();
if (!id) return;
vscode.postMessage({ type: 'deleteBrain', id });
};
// (inputSyncBtn removed — Sync Knowledge is reachable via the top brainBtn / Tools menu.)
document.getElementById('historyBtn').onclick = () => vscode.postMessage({ type: 'getSessions' });
@@ -1702,8 +1826,18 @@
}
brainSel.onchange = () => {
if (brainSel.value === 'new') {
// v2.2.67 — '+ Add New Brain...' 클릭 시 addBrain 메시지를 보내고, 사용자가 폴더 선택을
// 취소해도 dropdown 이 'new' 로 굳지 않도록 즉시 직전 유효 선택으로 되돌린다. 추가가 실제로
// 성공하면 _postBrainProfiles → brainProfiles 메시지가 새 brain 으로 다시 옮긴다.
// 이 복원이 없으면 brainSel.value === 'new' 상태가 유지되어 수정/삭제 버튼이 early-return 으로 죽는다.
const prev = brainSel.dataset.lastSelected;
vscode.postMessage({ type: 'addBrain' });
if (prev && prev !== 'new') {
brainSel.value = prev;
syncContextBar();
}
} else {
brainSel.dataset.lastSelected = brainSel.value;
vscode.postMessage({ type: 'setBrainProfile', id: brainSel.value });
}
};
@@ -1868,6 +2002,7 @@
vscode.postMessage({ type: 'getAgents' });
vscode.postMessage({ type: 'getChronicleProjects' });
vscode.postMessage({ type: 'getChronicleRecords' });
vscode.postMessage({ type: 'getChronicleAutoRecord' });
vscode.postMessage({ type: 'getKnowledgeMix' });
vscode.postMessage({ type: 'getArchitectureStatus' });
vscode.postMessage({ type: 'getCompanyStatus' });