Add context usage indicator to code editor session bar

세션 바에 현재 컨텍스트 토큰 추정치 / 모델 최대값 표시.
글자 수 ÷ 4 로 토큰 추정, 50% 이하 초록 / 50~80% 노랑 / 80%+ 빨강 색상 진행 바.
메시지 전송·응답 완료 시 즉시 업데이트.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kim
2026-06-06 14:33:54 +09:00
co-authored by Claude Sonnet 4.6
parent d612b32d2e
commit 1350ece55a
2 changed files with 93 additions and 27 deletions
+60 -27
View File
@@ -69,39 +69,53 @@
<!-- CODE SIDEBAR (mirrors #sidebar-code in index.html) --> <!-- CODE SIDEBAR (mirrors #sidebar-code in index.html) -->
<div class="code-sidebar-wrap"> <div class="code-sidebar-wrap">
<div id="sidebar-code" style="display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden"> <div id="sidebar-code" style="display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden">
<!-- 내부 탭 --> <!-- 상단 탭: 파일 | 설정 -->
<div style="display:flex;border-bottom:1px solid var(--line);flex-shrink:0;background:var(--panel-2)"> <div style="display:flex;border-bottom:1px solid var(--line);flex-shrink:0;background:var(--panel-2)">
<button class="code-sb-tab active" id="csb-tab-files" onclick="setCodeSbTab('files')">파일</button> <button class="code-sb-tab active" id="csb-tab-files" onclick="setCodeSbTab('files')">파일</button>
<button class="code-sb-tab" id="csb-tab-model" onclick="setCodeSbTab('model')">모델</button>
<button class="code-sb-tab" id="csb-tab-settings"onclick="setCodeSbTab('settings')">설정</button> <button class="code-sb-tab" id="csb-tab-settings"onclick="setCodeSbTab('settings')">설정</button>
</div> </div>
<!-- 파일 탭 --> <!-- 파일 탭: 상하 분할 -->
<div id="csb-files" style="display:flex;flex-direction:column;flex:1;min-height:0"> <div id="csb-files" style="display:flex;flex-direction:column;flex:1;min-height:0">
<div style="display:flex;align-items:center;justify-content:space-between;padding:7px 10px;border-bottom:1px solid var(--line);flex-shrink:0"> <!-- 상단: 파일 트리 -->
<span id="csb-folder-label" style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em">workspace/code/</span> <div style="display:flex;flex-direction:column;flex:1;min-height:0;border-bottom:1px solid var(--line)">
<div style="display:flex;gap:4px"> <div style="display:flex;align-items:center;justify-content:space-between;padding:7px 10px;border-bottom:1px solid var(--line);flex-shrink:0">
<button id="code-local-btn" onclick="openLocalFolder()" title="로컬 폴더 열기" style="background:none;border:none;cursor:pointer;color:var(--muted);font-size:14px;line-height:1;padding:0 3px">🖥️</button> <span id="csb-folder-label" style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em">폴더 없음</span>
<button onclick="codeNewFile()" title="새 파일" style="background:none;border:none;cursor:pointer;color:var(--muted);font-size:16px;line-height:1;padding:0 3px">+</button> <div style="display:flex;gap:4px">
<button onclick="codeSbRefresh()" title="새로고침" style="background:none;border:none;cursor:pointer;color:var(--muted);font-size:13px;line-height:1;padding:0 3px">↺</button> <button id="code-local-btn" onclick="openLocalFolder()" title="로컬 폴더 열기" style="background:none;border:none;cursor:pointer;color:var(--muted);font-size:14px;line-height:1;padding:0 3px">🖥️</button>
<button onclick="codeNewFile()" title="새 파일" style="background:none;border:none;cursor:pointer;color:var(--muted);font-size:16px;line-height:1;padding:0 3px">+</button>
<button onclick="codeSbRefresh()" title="새로고침" style="background:none;border:none;cursor:pointer;color:var(--muted);font-size:13px;line-height:1;padding:0 3px">↺</button>
</div>
</div>
<!-- 프로젝트 선택 -->
<div id="csb-project-row" style="display:none;align-items:center;gap:5px;padding:5px 8px;border-bottom:1px solid var(--line);flex-shrink:0;background:var(--panel-2)">
<span style="font-size:11px;flex-shrink:0">📁</span>
<select id="csb-project-sel" onchange="switchProject(this.value||null)"
style="flex:1;font-size:11px;padding:3px 5px;background:var(--panel);color:var(--text);border:1px solid var(--line);border-radius:5px;min-width:0;cursor:pointer">
</select>
<button onclick="deleteActiveProject()" title="현재 프로젝트 삭제" style="flex-shrink:0;background:none;border:none;color:var(--muted);cursor:pointer;font-size:13px;padding:2px 4px;border-radius:4px;line-height:1" onmouseover="this.style.color='#f87171'" onmouseout="this.style.color='var(--muted)'">🗑</button>
</div>
<div id="csb-file-tree" style="flex:1;overflow-y:auto;padding:4px 0">
<div style="padding:12px;color:var(--muted);font-size:11px;text-align:center">로딩 중...</div>
</div> </div>
</div> </div>
<div id="csb-file-tree" style="flex:1;overflow-y:auto;padding:4px 0">
<div style="padding:12px;color:var(--muted);font-size:11px;text-align:center">로딩 중...</div>
</div>
</div>
<!-- 모델 탭 --> <!-- 하단: 모델 설정 패널 -->
<div id="csb-model" style="display:none;flex-direction:column;flex:1;min-height:0;overflow-y:auto;padding:14px 12px;gap:12px"> <div style="flex-shrink:0;height:46%;min-height:160px;display:flex;flex-direction:column;overflow:hidden">
<div> <!-- 모델 선택 -->
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">Coder AI 모델</div> <div style="display:flex;flex-direction:column;flex:1;overflow-y:auto;padding:10px 12px;gap:8px">
<select id="csb-model-select" onchange="csbModelChanged()" <div>
style="width:100%;font-size:12px;padding:6px 8px;background:var(--panel-2);color:var(--text);border:1px solid var(--line);border-radius:8px;cursor:pointer"> <div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:5px">Coder AI 모델</div>
</select> <select id="csb-model-select" onchange="csbModelChanged()"
</div> style="width:100%;font-size:12px;padding:5px 8px;background:var(--panel-2);color:var(--text);border:1px solid var(--line);border-radius:8px;cursor:pointer">
<div> </select>
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">세션 초기화</div> </div>
<button onclick="codeAiClear()" style="width:100%;padding:7px;border:1px solid var(--line);background:var(--panel-2);color:var(--muted);border-radius:8px;cursor:pointer;font-size:12px">대화 기록 지우기</button> <div id="csb-session-project-row" style="display:none">
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:5px">프로젝트</div>
<select id="csb-session-project-sel" onchange="switchProject(this.value||null)" style="width:100%;font-size:12px;padding:5px 8px;background:var(--panel-2);color:var(--text);border:1px solid var(--line);border-radius:8px;font-family:monospace;cursor:pointer"></select>
</div>
<button onclick="codeAiClear()" style="width:100%;padding:7px;border:1px solid var(--line);background:var(--panel-2);color:var(--muted);border-radius:8px;cursor:pointer;font-size:12px">↺ 새 세션</button>
</div>
</div> </div>
</div> </div>
@@ -206,14 +220,32 @@
<div id="code-ai-panel"> <div id="code-ai-panel">
<div class="code-ai-header"> <div class="code-ai-header">
<span id="code-ai-title">💻 Coder AI</span> <span id="code-ai-title">💻 Coder AI</span>
<div class="code-ai-mode-selector"> <div class="code-ai-tech-selector" id="code-ai-tech-selector">
<button class="code-ai-mode-btn active" id="code-ai-mode-code" onclick="setCodeAiMode('code')">💻 Code</button> <button class="code-ai-tech-btn active" data-tech="auto" onclick="setCodeAiTech('auto')">🤖 자동</button>
<button class="code-ai-mode-btn" id="code-ai-mode-ask" onclick="setCodeAiMode('ask')">💬 Ask</button> <button class="code-ai-tech-btn" data-tech="html" onclick="setCodeAiTech('html')">🌐 HTML</button>
<button class="code-ai-tech-btn" data-tech="python" onclick="setCodeAiTech('python')">🐍 Python</button>
<button class="code-ai-tech-btn" data-tech="node" onclick="setCodeAiTech('node')">🟢 Node</button>
</div> </div>
</div> </div>
<div id="code-ai-messages"> <div id="code-ai-messages">
<div class="code-ai-msg thinking">코드를 선택하고 "AI에게 전송"을 누르거나, 아래에 질문을 입력하세요.</div> <div class="code-ai-msg thinking">코드를 선택하고 "AI에게 전송"을 누르거나, 아래에 질문을 입력하세요.</div>
</div> </div>
<!-- 세션 상태 바 -->
<div id="code-ai-session-bar" style="display:flex;align-items:center;justify-content:space-between;padding:4px 10px;border-top:1px solid var(--line);background:var(--panel-2);flex-shrink:0;gap:6px">
<div style="display:flex;align-items:center;gap:5px;min-width:0;overflow:hidden;flex:1">
<span style="font-size:10px;color:var(--muted);flex-shrink:0">세션</span>
<span id="code-ai-session-bar-project" style="font-size:11px;color:var(--text);font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">(전체)</span>
<span id="code-ai-session-bar-count" style="font-size:10px;color:var(--muted);flex-shrink:0"></span>
</div>
<!-- 컨텍스트 사용량 -->
<div id="code-ai-ctx-wrap" title="컨텍스트 사용량 (추정)" style="display:none;flex-shrink:0;display:flex;align-items:center;gap:4px;cursor:default">
<span id="code-ai-ctx-label" style="font-size:10px;color:var(--muted);white-space:nowrap"></span>
<div style="width:60px;height:5px;background:rgba(255,255,255,.1);border-radius:3px;overflow:hidden;flex-shrink:0">
<div id="code-ai-ctx-fill" style="height:100%;width:0%;border-radius:3px;transition:width .4s,background .4s"></div>
</div>
</div>
<button onclick="codeAiClear()" title="새 세션 시작" style="flex-shrink:0;background:none;border:1px solid var(--line);border-radius:6px;color:var(--muted);cursor:pointer;padding:2px 8px;font-size:11px;line-height:1.6;white-space:nowrap">↺ 새 세션</button>
</div>
<div class="code-ai-input-row"> <div class="code-ai-input-row">
<textarea id="code-ai-input" placeholder="AI에게 질문..." rows="1" <textarea id="code-ai-input" placeholder="AI에게 질문..." rows="1"
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();codeAiSend();}"></textarea> onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();codeAiSend();}"></textarea>
@@ -231,6 +263,7 @@
<button onclick="toggleCodeTerminal()" title="닫기" style="background:none;border:none;color:#3d6b4f;cursor:pointer;font-size:14px;padding:0 8px;line-height:1;flex-shrink:0">&times;</button> <button onclick="toggleCodeTerminal()" title="닫기" style="background:none;border:none;color:#3d6b4f;cursor:pointer;font-size:14px;padding:0 8px;line-height:1;flex-shrink:0">&times;</button>
</div> </div>
<div id="code-term-container" style="flex:1;min-height:0;position:relative;overflow:hidden"></div> <div id="code-term-container" style="flex:1;min-height:0;position:relative;overflow:hidden"></div>
<div id="code-terminal-panel" style="position:relative;height:0"></div>
</div> </div>
</div> </div>
+33
View File
@@ -2108,6 +2108,7 @@ async function codeAiSend() {
// 모델이 "기존 파일이 있다"는 맥락은 유지하되 매 턴 파일 전체를 반복하지 않음. // 모델이 "기존 파일이 있다"는 맥락은 유지하되 매 턴 파일 전체를 반복하지 않음.
const historyEntry = currentFile ? `[파일: ${_clientBareFileName(currentFile.name)}] ${message}` : message; const historyEntry = currentFile ? `[파일: ${_clientBareFileName(currentFile.name)}] ${message}` : message;
codeAiHistory.push({ role: 'user', content: historyEntry }); codeAiHistory.push({ role: 'user', content: historyEntry });
try { updateSessionPanel(); } catch (_) {}
const thinkEl = codeAiAppend('thinking', '생각 중...'); const thinkEl = codeAiAppend('thinking', '생각 중...');
@@ -2463,6 +2464,7 @@ async function codeAiSend() {
codeAiHistory.push({ role: 'assistant', content: reply || '(응답 없음)' }); codeAiHistory.push({ role: 'assistant', content: reply || '(응답 없음)' });
_autoSaveChatSession(); _autoSaveChatSession();
if (activeProjectName) saveProjectSession(activeProjectName); if (activeProjectName) saveProjectSession(activeProjectName);
try { updateSessionPanel(); } catch (_) {}
thinkEl.classList.remove('streaming'); // Stop cursor animation thinkEl.classList.remove('streaming'); // Stop cursor animation
if (!reply) { thinkEl.textContent = '(응답 없음)'; } if (!reply) { thinkEl.textContent = '(응답 없음)'; }
else codeAiRenderMarkdown(thinkEl, reply); else codeAiRenderMarkdown(thinkEl, reply);
@@ -3891,11 +3893,42 @@ function setCodeSbTab(tab) {
} }
function _codeAiCtxMax() {
const m = (codeAiModel || '').toLowerCase();
if (!m || m.includes('claude')) return 200000;
// Common Ollama context windows
if (m.includes('llama3') || m.includes('llama-3') || m.includes('qwen')) return 128000;
if (m.includes('mistral') || m.includes('mixtral')) return 32000;
if (m.includes('gemma')) return 8192;
return 32000; // safe default for unknown local models
}
function updateSessionPanel() { function updateSessionPanel() {
const barProj = document.getElementById('code-ai-session-bar-project'); const barProj = document.getElementById('code-ai-session-bar-project');
const barCount = document.getElementById('code-ai-session-bar-count'); const barCount = document.getElementById('code-ai-session-bar-count');
if (barProj) barProj.textContent = activeProjectName || '(전체)'; if (barProj) barProj.textContent = activeProjectName || '(전체)';
if (barCount) barCount.textContent = codeAiHistory.length ? `${codeAiHistory.length}개` : ''; if (barCount) barCount.textContent = codeAiHistory.length ? `${codeAiHistory.length}개` : '';
// Context usage estimate
const ctxWrap = document.getElementById('code-ai-ctx-wrap');
const ctxLabel = document.getElementById('code-ai-ctx-label');
const ctxFill = document.getElementById('code-ai-ctx-fill');
if (ctxWrap && ctxLabel && ctxFill) {
const totalChars = codeAiHistory.reduce((s, m) => s + (m.content || '').length, 0);
if (totalChars === 0) {
ctxWrap.style.display = 'none';
} else {
const estTokens = Math.round(totalChars / 4);
const maxTokens = _codeAiCtxMax();
const pct = Math.min(100, Math.round(estTokens / maxTokens * 100));
const kStr = estTokens >= 1000 ? `~${(estTokens/1000).toFixed(1)}k` : `~${estTokens}`;
const maxKStr = maxTokens >= 1000 ? `${(maxTokens/1000).toFixed(0)}k` : `${maxTokens}`;
ctxLabel.textContent = `${kStr} / ${maxKStr}`;
ctxFill.style.width = pct + '%';
ctxFill.style.background = pct >= 80 ? '#ef4444' : pct >= 50 ? '#f59e0b' : '#22c55e';
ctxWrap.style.display = 'flex';
}
}
// 프로젝트 선택기 (항상 업데이트) // 프로젝트 선택기 (항상 업데이트)
const sel = document.getElementById('csb-session-project-sel'); const sel = document.getElementById('csb-session-project-sel');
const projRow = document.getElementById('csb-session-project-row'); const projRow = document.getElementById('csb-session-project-row');