Code editor: block flip-flop colors, view option buttons, terminal error improvements, live server log bubble

- Add alternating block background colors (ff-block-even/odd) for functions/classes
- Add ¶빈줄숨김 / ▓블록색상 toggle buttons to file panel sidebar (below file tree)
- Fix terminal error send: filter Windows prompt+command lines, only send last Traceback block, strip pygame header noise
- Show live server log bubble during AI tool calls, fade out on completion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kim
2026-06-09 13:57:27 +09:00
co-authored by Claude Sonnet 4.6
parent 4ead0750b9
commit 9b558be9f4
5 changed files with 320 additions and 45 deletions
+34 -2
View File
@@ -4,10 +4,14 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>💻 코드 에디터</title>
<style>
@font-face { font-family: 'D2Coding'; src: url('/fonts/D2Coding.ttf') format('truetype'); font-weight: 400; font-style: normal; }
@font-face { font-family: 'D2Coding'; src: url('/fonts/D2CodingBold.ttf') format('truetype'); font-weight: 700; font-style: normal; }
</style>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nanum+Gothic:wght@400;700;800&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&family=Fira+Code:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="icon" type="image/png" sizes="64x64" href="cherry_logo.png">
<!-- CodeMirror + xterm (UMD libs loaded before Monaco's AMD loader to avoid anonymous define() clashes) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
@@ -227,6 +231,11 @@
<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 style="display:flex;gap:4px;padding:5px 8px;flex-shrink:0;border-top:1px solid var(--line)">
<button id="code-opt-hideblank" class="csb-term-btn" onclick="csbHideBlankLines(!_hideBlankLines)" title="빈 줄·주석 줄 숨기기" style="font-size:11px;padding:4px 0">¶ 빈줄숨김</button>
<button id="code-opt-blockff" class="csb-term-btn" onclick="csbBlockFlipFlop(!_blockFlipFlop)" title="함수/블록 교대 배경색" style="font-size:11px;padding:4px 0">▓ 블록색상</button>
</div>
</div>
<!-- 하단: 모델 설정 패널 -->
@@ -276,6 +285,29 @@
</div>
<div style="font-size:10px;color:var(--muted);margin-top:4px">새 터미널 탭 열면 이 경로로 자동 이동</div>
</div>
<div>
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">에디터 폰트</div>
<select id="csb-fontfamily" onchange="csbFontFamily(this.value)"
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">
<option value="D2Coding">D2Coding</option>
<option value="JetBrains Mono">JetBrains Mono</option>
<option value="IBM Plex Mono">IBM Plex Mono</option>
<option value="Fira Code">Fira Code</option>
<option value="Nanum Gothic">Nanum Gothic</option>
<option value="monospace">시스템 기본</option>
</select>
</div>
<div>
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">폰트 웨이트</div>
<select id="csb-fontweight" onchange="csbFontWeight(this.value)"
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">
<option value="300">Light (300)</option>
<option value="400" selected>Regular (400)</option>
<option value="500">Medium (500)</option>
<option value="600">Semi Bold (600)</option>
<option value="700">Bold (700)</option>
</select>
</div>
<div>
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">폰트 크기</div>
<div style="display:flex;align-items:center;gap:8px">
@@ -321,7 +353,7 @@
<div id="code-view" class="active">
<!-- File tabs bar -->
<div class="code-tabs-bar">
<div id="code-file-tabs" style="display:flex;align-items:stretch;flex:1"></div>
<div id="code-file-tabs" style="display:flex;flex:1"></div>
<div id="code-ai-model-card" style="display:flex;align-items:center;gap:7px;padding:3px 10px 3px 12px;border-left:1px solid #1a4a35;flex-shrink:0;align-self:stretch">
<span style="font-size:16px;line-height:1">🤖</span>
<div style="display:flex;flex-direction:column;gap:1px">
+256 -41
View File
@@ -127,15 +127,16 @@ function initCodeView() {
theme: isDark ? 'vs-dark' : 'vs',
language: 'plaintext',
automaticLayout: true,
fontSize: 12,
lineHeight: 19,
fontFamily: "'IBM Plex Mono', 'Fira Code', 'Courier New', monospace",
fontSize: 14,
lineHeight: 22,
fontFamily: "'D2Coding', 'D2 Coding', monospace",
minimap: { enabled: true },
scrollBeyondLastLine: false,
wordWrap: 'on',
renderWhitespace: 'selection',
smoothScrolling: true,
cursorBlinking: 'smooth',
mouseWheelZoom: true,
tabSize: 2,
});
monacoEditor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, codeSaveFile);
@@ -195,6 +196,8 @@ function switchCodeFile(id) {
}
renderCodeFileTabs();
codeUpdateRunButton();
if (_hideBlankLines) setTimeout(_applyHideBlankLines, 50);
if (_blockFlipFlop) setTimeout(_applyBlockFlipFlop, 60);
}
function closeCodeFile(id, ev) {
@@ -444,11 +447,18 @@ async function _clientLoadProjectFiles() {
// errors after a run and offer one-click "AI에게 전송" from the terminal.
const _termBuffers = {}; // shellId → string[] (last 300 lines)
const _TERM_BUF_MAX = 300;
const _ANSI_RE = /\x1b\[[0-9;]*[mGKHFABCDJnsuhl]/g;
function _stripTerminal(raw) {
return raw
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '') // OSC (title 등)
.replace(/\x1b\[[\d;?]*[a-zA-Z]/g, '') // CSI (색상, 커서, private mode)
.replace(/\x1b[^[\]]/g, '') // 기타 ESC 시퀀스
.replace(/[\x00-\x09\x0b-\x1f\x7f]/g, '') // 제어문자 (\n 제외)
.replace(/\r/g, '');
}
function _termBufferAppend(shellId, raw) {
if (!shellId) return;
const text = raw.replace(_ANSI_RE, '').replace(/\r/g, '');
const text = _stripTerminal(raw);
if (!text) return;
const lines = text.split('\n');
if (!_termBuffers[shellId]) _termBuffers[shellId] = [];
@@ -484,25 +494,61 @@ function _termDetectAndShowError(shellId) {
function _showTerminalErrorButton(shellId, errorText) {
const btn = document.getElementById('code-term-ai-error-btn');
if (btn) { btn.style.background = '#dc2626'; btn.style.color = '#fff'; btn.dataset.shellId = shellId; }
if (btn) { btn.style.display = ''; btn.style.background = '#dc2626'; btn.style.color = '#fff'; btn.dataset.shellId = shellId; }
}
function _sendTerminalErrorToAI(errorText) {
const input = document.getElementById('code-ai-input');
if (!input) return;
const trimmed = errorText.split('\n').filter(l => l.trim()).slice(-25).join('\n');
// Filter noise lines
const filtered = errorText.split('\n').filter(l => {
const t = l.trim();
if (!t) return false;
if (/^[A-Za-z]:\\.+>/.test(t)) return false; // Windows 프롬프트 (명령어 포함)
if (/^[~/][^\s]*[$#]\s*/.test(t)) return false; // Unix 프롬프트
if (/^Hello from the pygame community/.test(t)) return false;
if (/^https?:\/\/www\.pygame\.org/.test(t)) return false;
return true;
});
// 마지막 Traceback 블록만 추출
let blockStart = 0;
for (let i = filtered.length - 1; i >= 0; i--) {
if (/Traceback \(most recent call last\)/.test(filtered[i])) {
blockStart = i; break;
}
}
const trimmed = filtered.slice(blockStart).slice(-30).join('\n');
if (!trimmed) return;
input.value = '다음 에러를 고쳐줘. edit_file 도구로 파일을 직접 수정해:\n```\n' + trimmed + '\n```';
input.style.height = '';
input.dispatchEvent(new Event('input'));
// Auto-send
codeAiSend();
}
function _getXtermVisibleText(term, maxLines = 80) {
const buf = term.buffer.active;
const lines = [];
const endRow = buf.baseY + buf.cursorY;
const startRow = Math.max(0, endRow - maxLines);
for (let i = startRow; i <= endRow; i++) {
const line = buf.getLine(i);
if (!line) continue;
const t = line.translateToString(true);
if (t.trim()) lines.push(t);
}
return lines.join('\n');
}
function _sendActiveTermErrorToAI() {
const btn = document.getElementById('code-term-ai-error-btn');
const shellId = btn?.dataset.shellId || _lastErrorShellId;
const buf = (shellId && _termBuffers[shellId]) || Object.values(_termBuffers).at(-1);
const text = buf ? buf.slice(-40).join('\n') : _lastErrorText;
const shellId = btn?.dataset.shellId || _lastErrorShellId || activeCodeTermShellId;
const shell = codeTermShells.find(s => s.id === shellId) || codeTermShells.find(s => s.id === activeCodeTermShellId);
const text = (shell?.term ? _getXtermVisibleText(shell.term) : '')
|| (shellId && _termBuffers[shellId] || Object.values(_termBuffers).at(-1))?.slice(-40).join('\n')
|| _lastErrorText;
if (text) _sendTerminalErrorToAI(text);
if (btn) { btn.style.background = '#1a3a28'; btn.style.color = '#22c55e'; }
}
@@ -559,10 +605,9 @@ function handleCompanionMessage(msg) {
if (msg.type === 'shell_ready') {
if (msg.platform) companionPlatform = msg.platform;
if (msg.home) companionHome = msg.home;
s.term.write('\x1b[32m🖥️ 로컬 셸\x1b[0m\r\n');
_companionCdProject(msg.shellId);
}
else if (msg.type === 'shell_output') s.term.write(msg.data);
else if (msg.type === 'shell_output') { s.term.write(msg.data); _termBufferAppend(s.id, msg.data); }
else if (msg.type === 'shell_exit') { s.term.write(`\r\n\x1b[33m[셸 종료 ${msg.code}]\x1b[0m\r\n`); }
else if (msg.type === 'started') s.term.write(`\r\n\x1b[32m▶ ${msg.filename}\x1b[0m\r\n`);
else if (msg.type === 'output') { s.term.write(msg.data.replace(/(?<!\r)\n/g, '\r\n')); _termBufferAppend(s.id, msg.data); }
@@ -596,15 +641,10 @@ function _companionCdProject(shellId) {
const base = (localTermPath || companionHome || '').replace(/[/\\]$/, '');
if (!base) return;
const target = base + sep + activeProjectName;
const cmd = isWin ? `cd /d "${target}"\r` : `cd ${JSON.stringify(target)}\r`;
const cmd = isWin
? `set "PROMPT=$E[A$P$G" && cd /d "${target}" && cls\r`
: `cd ${JSON.stringify(target)} && clear\r`;
companionWs.send(JSON.stringify({ type: 'input', shellId: sid, data: cmd }));
// After cd completes, clear via xterm API so the prompt is always visible
setTimeout(() => {
const shell = codeTermShells.find(s => s.id === sid);
if (shell?.term) shell.term.clear();
if (companionConnected && companionWs)
companionWs.send(JSON.stringify({ type: 'input', shellId: sid, data: '\r' }));
}, 800);
}
function csbLoadCodeDir() {}
@@ -795,7 +835,7 @@ function toggleCodeTerminal() {
if (codeTermOpen) {
panel.style.display = 'flex';
if (btn) btn.classList.add('active');
if (!codeTermShells.length) { if (companionConnected) addLocalShell(); else addCodeTermShell(); }
if (!codeTermShells.length) { requestAnimationFrame(() => { if (companionConnected) addLocalShell(); else addCodeTermShell(); }); }
else { setTimeout(() => { const s = codeTermShells.find(s => s.id === activeCodeTermShellId); if (s?.fitAddon) s.fitAddon.fit(); if (s?.term) s.term.focus(); }, 50); }
} else {
panel.style.display = 'none';
@@ -2547,6 +2587,9 @@ async function _codeAiSendImpl() {
// 파일 작업 키워드가 포함된 항목이 있으면 실행 단계 목록 → 선택지 아님
const fileOpRe = /edit_file|create_file|move_file|delete_file|replace_lines|insert_after|\.[a-zA-Z]{2,4}(?:\s|$|—|:)/;
if (opts.some(o => fileOpRe.test(o.label))) return [];
if (/수정\s*계획|변경\s*계획|개선\s*계획|구현\s*계획/.test(planText)) return [];
const firstPart = planText.slice(0, planText.length * 0.6);
if (opts.every(o => firstPart.includes(o.label))) return [];
return opts;
})();
@@ -2708,8 +2751,9 @@ async function _codeAiSendImpl() {
const dec = new TextDecoder();
let buf = '', reply = '', _preambleBuf = '', _firstToolSeen = false;
let liveTextEl = null, segReply = '';
thinkEl.textContent = '⏳';
thinkEl.textContent = '🤔 분석 중...';
thinkEl.className = 'code-ai-msg thinking';
const _statusMove = (text) => { thinkEl.textContent = text; thinkEl.style.display = ''; const msgs = document.getElementById('code-ai-msgs'); if (msgs) msgs.appendChild(thinkEl); codeAiScrollBottom(); };
// Live streaming state (mirrored into the editor via codeLiveStreamUpdate)
// Pin the diff target to the file that was active when the user hit send —
// that's the file we attached as "[현재 파일: …]" context, so any returned
@@ -2737,7 +2781,7 @@ async function _codeAiSendImpl() {
else if (ev.type === 'done' && ev.reply) { if (!reply) reply = ev.reply; (liveTextEl || thinkEl).textContent = reply; codeAiScrollBottom(); continue; }
// Handle tool events for live editor update + action cards
else if (ev.type === 'tool_call') {
if (!_firstToolSeen) { _firstToolSeen = true; thinkEl.style.display = 'none'; }
_firstToolSeen = true;
// Close current text bubble before showing tool card
if (liveTextEl) {
if (!segReply.trim()) liveTextEl.remove();
@@ -2746,6 +2790,11 @@ async function _codeAiSendImpl() {
}
const toolName = ev.action || ev.name || '';
const toolArgs = ev.args || {};
const _fname = toolArgs.filename || toolArgs.name || toolArgs.path || '';
const _short = _fname ? _fname.split('/').pop() : '';
const _tIcon = _TOOL_ICONS[toolName] || '🔧';
_statusMove(`${_tIcon} ${toolName}${_short ? ': ' + _short : ''} ...`);
_showLogBubble();
liveState.toolCallCount = (liveState.toolCallCount || 0) + 1;
_codeAiToolAction(toolName, toolArgs, null, null);
// ── Client-side file intercept: apply tool changes to local folder ──
@@ -2765,6 +2814,7 @@ async function _codeAiSendImpl() {
const toolResult = ev.result || '';
const toolError = ev.error || false;
_codeAiToolAction(toolName, null, toolResult, toolError);
_statusMove('💭 다음 단계 분석 중...');
// Refresh file list after successful file-modifying tools
if (!toolError && ['create_file', 'replace_lines', 'insert_after', 'delete_lines', 'find_replace', 'edit_file', 'move_file', 'delete_file'].includes(toolName)) {
codeSbRefresh();
@@ -2772,7 +2822,11 @@ async function _codeAiSendImpl() {
}
if (token) {
reply += token; segReply += token;
if (!liveTextEl) { liveTextEl = codeAiAppend('assistant', ''); liveTextEl.classList.add('streaming'); }
if (!liveTextEl) {
liveTextEl = codeAiAppend('assistant', '');
liveTextEl.classList.add('streaming');
if (_firstToolSeen) _statusMove('💬 응답 작성 중...');
}
liveTextEl.textContent = segReply; codeAiScrollBottom();
if (_firstToolSeen) {
try { codeLiveStreamUpdate(segReply, liveState); } catch(e) { console.error('[codeLiveStream] error:', e); liveTextEl.textContent += '\n⚠️ 에디터 스트리밍 오류: ' + (e.message || e); }
@@ -3138,16 +3192,20 @@ async function _codeAiSendImpl() {
try { updateSessionPanel(); } catch (_) {}
thinkEl.classList.remove('streaming');
if (_firstToolSeen) {
thinkEl.style.display = 'none';
thinkEl.remove();
_hideLogBubble();
if (liveTextEl) { liveTextEl.classList.remove('streaming'); codeAiRenderMarkdown(liveTextEl, segReply); }
const doneEl = codeAiAppend('system', '✅ 완료');
doneEl.style.cssText = 'font-weight:600;color:#4ade80;';
} else {
_hideLogBubble();
thinkEl.className = 'code-ai-msg assistant';
if (!reply) { thinkEl.textContent = '(응답 없음)'; thinkEl.style.display = ''; }
else { codeAiRenderMarkdown(thinkEl, reply); thinkEl.style.display = ''; }
}
} catch(e) {
_hideLogBubble();
if (!thinkEl.isConnected) { const msgs = document.getElementById('code-ai-msgs'); if (msgs) msgs.appendChild(thinkEl); }
thinkEl.style.display = '';
thinkEl.className = 'code-ai-msg thinking';
const errMsg = (e.name === 'AbortError' || (e.message || '').includes('aborted'))
@@ -3162,6 +3220,42 @@ async function _codeAiSendImpl() {
}
}
// ── Live server log bubble ────────────────────────────────────────────────
let _logBubble = null;
let _logPollTimer = null;
function _showLogBubble() {
if (_logBubble) return;
const msgs = document.getElementById('code-ai-messages');
if (!msgs) return;
_logBubble = document.createElement('div');
_logBubble.className = 'code-ai-log-bubble';
_logBubble.innerHTML = '<span class="log-live-dot"></span><span class="log-lines">서버 연결 중...</span>';
msgs.appendChild(_logBubble);
codeAiScrollBottom();
_pollLogBubble();
_logPollTimer = setInterval(_pollLogBubble, 800);
}
async function _pollLogBubble() {
if (!_logBubble) return;
try {
const r = await fetch('/api/logs', { headers: authHeaders({}) });
if (!r.ok) return;
const data = await r.json();
const lines = (data.entries || []).slice(-4).map(e => e.msg.slice(0, 90));
const linesEl = _logBubble.querySelector('.log-lines');
if (linesEl) linesEl.textContent = lines.join('\n') || '...';
codeAiScrollBottom();
} catch {}
}
function _hideLogBubble() {
if (_logPollTimer) { clearInterval(_logPollTimer); _logPollTimer = null; }
const b = _logBubble; _logBubble = null;
if (b) { b.style.opacity = '0'; setTimeout(() => b.remove(), 400); }
}
// ── Tool action cards for Cline-style visibility ──────────────────────────
const _TOOL_ICONS = {
read_file: '📖', list_files: '📂', search_files: '🔍',
@@ -5226,24 +5320,126 @@ function csbWordWrap(checked) {
localStorage.setItem('codeEditorWordWrap', checked ? '1' : '0');
}
function csbFontWeight(val) {
localStorage.setItem('codeEditorFontWeight', val);
if (monacoEditor) monacoEditor.updateOptions({ fontWeight: val });
}
let _hideBlankLines = false;
function csbHideBlankLines(checked) {
_hideBlankLines = !!checked;
localStorage.setItem('codeEditorHideBlank', _hideBlankLines ? '1' : '0');
const btn = document.getElementById('code-opt-hideblank');
if (btn) btn.classList.toggle('active', _hideBlankLines);
_applyHideBlankLines();
}
function _applyHideBlankLines() {
if (!monacoEditor) return;
if (!_hideBlankLines) { try { monacoEditor.setHiddenAreas([]); } catch {} return; }
const model = monacoEditor.getModel();
if (!model) return;
const n = model.getLineCount();
const ranges = [];
let start = -1;
let inBlock = false, blockEnd = '';
for (let i = 1; i <= n; i++) {
const l = model.getLineContent(i).trim();
let hide = false;
if (inBlock) {
hide = true;
if (l.includes(blockEnd)) inBlock = false;
} else if (l === '') {
hide = true;
} else if (/^(\/\/|#|--|;)/.test(l)) {
hide = true;
} else if (l.startsWith('/*')) {
hide = true;
if (!l.slice(2).includes('*/')) { inBlock = true; blockEnd = '*/'; }
} else if (l.startsWith('<!--')) {
hide = true;
if (!l.slice(4).includes('-->')) { inBlock = true; blockEnd = '-->'; }
} else if (l.startsWith('"""') || l.startsWith("'''")) {
const d = l.slice(0, 3);
hide = true;
if (!l.slice(3).includes(d)) { inBlock = true; blockEnd = d; }
}
if (hide) { if (start < 0) start = i; }
else { if (start >= 0) { ranges.push({ startLineNumber: start, startColumn: 1, endLineNumber: i - 1, endColumn: 1 }); start = -1; } }
}
if (start >= 0) ranges.push({ startLineNumber: start, startColumn: 1, endLineNumber: n, endColumn: 1 });
try { monacoEditor.setHiddenAreas(ranges); } catch {}
}
let _blockFlipFlop = false;
let _blockFFDecoIds = [];
function csbBlockFlipFlop(checked) {
_blockFlipFlop = !!checked;
localStorage.setItem('codeEditorBlockFF', _blockFlipFlop ? '1' : '0');
const btn = document.getElementById('code-opt-blockff');
if (btn) btn.classList.toggle('active', _blockFlipFlop);
_applyBlockFlipFlop();
}
function _applyBlockFlipFlop() {
if (!monacoEditor) return;
const model = monacoEditor.getModel();
if (!model) return;
_blockFFDecoIds = monacoEditor.deltaDecorations(_blockFFDecoIds, []);
if (!_blockFlipFlop) return;
const n = model.getLineCount();
const decos = [];
// Detect top-level function/class/def declarations
const declRe = /^(export\s+)?(default\s+)?(async\s+)?(function\b|class\b|def\b|async\s+def\b)|^(const|let|var)\s+\w+\s*=\s*(async\s+)?(function|\(|[A-Za-z_$]\w*\s*=>)/;
const boundaries = [];
for (let i = 1; i <= n; i++) {
const line = model.getLineContent(i);
if (line.match(/^(\s*)/)[1].length === 0 && declRe.test(line.trim())) {
boundaries.push(i);
}
}
const ffCls = (idx) => idx % 2 === 0 ? 'ff-block-even' : 'ff-block-odd';
const ffRange = (from, to, idx) => ({ range: { startLineNumber: from, startColumn: 1, endLineNumber: to, endColumn: 1 }, options: { isWholeLine: true, className: ffCls(idx) } });
if (boundaries.length >= 2) {
// Declaration-based: alternate between declaration boundaries
for (let b = 0; b < boundaries.length; b++) {
const from = boundaries[b];
const to = b + 1 < boundaries.length ? boundaries[b + 1] - 1 : n;
decos.push(ffRange(from, to, b));
}
} else {
// Fallback: blank-line-separated groups
let phase = 0, blockStart = -1;
for (let i = 1; i <= n; i++) {
const blank = model.getLineContent(i).trim() === '';
if (!blank && blockStart < 0) { blockStart = i; }
else if (blank && blockStart >= 0) {
decos.push(ffRange(blockStart, i - 1, phase));
blockStart = -1; phase++;
}
}
if (blockStart >= 0) decos.push(ffRange(blockStart, n, phase));
}
_blockFFDecoIds = monacoEditor.deltaDecorations([], decos);
}
const CODE_THEME_GROUPS = [
{ icon: '🌙', label: '다크', themes: [
{ v: 'vs-dark', label: 'VS Dark', bg: '#1e1e1e' },
{ v: 'one-dark', label: 'One Dark', bg: '#282c34' },
{ v: 'nord', label: 'Nord', bg: '#2e3440' },
{ v: 'github-dark', label: 'GitHub Dark', bg: '#0d1117' },
{ v: 'tomorrow-night', label: 'Tomorrow Night',bg: '#1d1f21' },
{ v: 'hc-black', label: 'High Contrast', bg: '#000000' },
{ v: 'github-dark', label: 'GitHub Dark', bg: '#0d1117' },
{ v: 'one-dark', label: 'One Dark', bg: '#282c34' },
{ v: 'dracula', label: 'Dracula', bg: '#282a36' },
{ v: 'nord', label: 'Nord', bg: '#2e3440' },
{ v: 'vs-dark', label: 'VS Dark', bg: '#1e1e1e' },
{ v: 'monokai', label: 'Monokai', bg: '#272822' },
]},
{ icon: '☀️', label: '라이트', themes: [
{ v: 'vs', label: 'VS Light', bg: '#ffffff' },
{ v: 'github-light', label: 'GitHub Light',bg: '#f6f8fa' },
]},
{ icon: '🌿', label: '웜', themes: [
{ v: 'monokai', label: 'Monokai', bg: '#272822' },
{ v: 'dracula', label: 'Dracula', bg: '#282a36' },
{ v: 'solarized-dark', label: 'Solarized Dark', bg: '#002b36' },
{ v: 'solarized-light', label: 'Solarized Light',bg: '#fdf6e3' },
{ v: 'github-light', label: 'GitHub Light', bg: '#f6f8fa' },
{ v: 'vs', label: 'VS Light', bg: '#ffffff' },
]},
];
const CODE_THEMES = CODE_THEME_GROUPS.flatMap(g => g.themes);
@@ -5353,6 +5549,11 @@ function csbMinimap(checked) {
localStorage.setItem('codeEditorMinimap', checked ? '1' : '0');
}
function csbFontFamily(val) {
localStorage.setItem('codeEditorFontFamily', val);
if (monacoEditor) monacoEditor.updateOptions({ fontFamily: val === 'monospace' ? 'monospace' : `'${val}', monospace` });
}
// Restore editor settings from localStorage
function csbRestoreSettings() {
const fs = localStorage.getItem('codeEditorFontSize');
@@ -5360,16 +5561,30 @@ function csbRestoreSettings() {
const th = localStorage.getItem('codeEditorTheme');
const ts = localStorage.getItem('codeEditorTabSize');
const mm = localStorage.getItem('codeEditorMinimap');
const ff = localStorage.getItem('codeEditorFontFamily') || 'D2Coding';
const fw = localStorage.getItem('codeEditorFontWeight') || '400';
const hb = localStorage.getItem('codeEditorHideBlank');
const bff = localStorage.getItem('codeEditorBlockFF');
if (fs) { document.getElementById('csb-fontsize').value = fs; document.getElementById('csb-fontsize-label').textContent = fs; }
if (ww !== null) document.getElementById('csb-wordwrap').checked = ww === '1';
if (ts) document.getElementById('csb-tabsize').value = ts;
if (mm !== null) document.getElementById('csb-minimap').checked = mm === '1';
const ffEl = document.getElementById('csb-fontfamily'); if (ffEl) ffEl.value = ff;
const fwEl = document.getElementById('csb-fontweight'); if (fwEl) fwEl.value = fw;
_hideBlankLines = hb === '1';
_blockFlipFlop = bff === '1';
const hbBtn = document.getElementById('code-opt-hideblank'); if (hbBtn) hbBtn.classList.toggle('active', _hideBlankLines);
const bffBtn = document.getElementById('code-opt-blockff'); if (bffBtn) bffBtn.classList.toggle('active', _blockFlipFlop);
if (monacoEditor) monacoEditor.updateOptions({
fontSize: fs ? parseInt(fs) : 12,
fontSize: fs ? parseInt(fs) : 14,
wordWrap: ww === '0' ? 'off' : 'on',
tabSize: ts ? parseInt(ts) : 2,
minimap: { enabled: mm !== '0' },
fontFamily: ff === 'monospace' ? 'monospace' : `'${ff}', monospace`,
fontWeight: fw,
});
if (_hideBlankLines) _applyHideBlankLines();
if (_blockFlipFlop) _applyBlockFlipFlop();
const activeTheme = th || (document.documentElement.getAttribute('data-theme') !== 'light' ? 'vs-dark' : 'vs');
if (window.monaco) { defineCodeThemes(); monaco.editor.setTheme(activeTheme); }
renderThemeGrid();
Binary file not shown.
Binary file not shown.
+30 -2
View File
@@ -2366,12 +2366,17 @@
#code-view.active { display: flex; }
.code-tabs-bar {
display: flex;
align-items: center;
align-items: flex-start;
background: #0d1f1a;
border-bottom: 2px solid #1a4a35;
flex-shrink: 0;
min-height: 28px;
overflow-x: auto;
}
#code-file-tabs {
flex-wrap: wrap;
max-height: 56px;
overflow: hidden;
align-content: flex-start;
}
.code-file-tab {
display: flex; align-items: center; gap: 4px;
@@ -2988,3 +2993,26 @@
/* ── AI Mode Selector ────────────────────────────────────────────────── */
@keyframes pulse-red { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:0.8;transform:scale(1.04)} }
/* ── Live server log bubble (shown during AI file edits) ────────────── */
.code-ai-log-bubble {
font-family: 'IBM Plex Mono', monospace;
font-size: 10px; line-height: 1.6;
color: #4a7a5c; background: #040e08;
border: 1px solid #1a3a20; border-radius: 8px;
padding: 5px 10px; margin: 2px 0;
white-space: pre-wrap; word-break: break-all;
transition: opacity .4s;
max-height: 90px; overflow: hidden;
}
.log-live-dot {
display: inline-block; width: 6px; height: 6px;
border-radius: 50%; background: #22c55e;
margin-right: 5px; vertical-align: middle;
animation: log-blink 1.2s ease-in-out infinite;
}
@keyframes log-blink { 0%,100%{opacity:1} 50%{opacity:.2} }
/* ── Block flip-flop alternating highlight ───────────────────────────── */
.ff-block-even { background-color: rgba(59,130,246,0.18) !important; }
.ff-block-odd { background-color: rgba(251,146,60,0.18) !important; }