π€
diff --git a/web-ui/code.js b/web-ui/code.js
index 78c8251..c698c60 100644
--- a/web-ui/code.js
+++ b/web-ui/code.js
@@ -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(/(? {
- 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 = 'μλ² μ°κ²° μ€...';
+ 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('')) { 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();
diff --git a/web-ui/fonts/D2Coding.ttf b/web-ui/fonts/D2Coding.ttf
new file mode 100644
index 0000000..34f6c8c
Binary files /dev/null and b/web-ui/fonts/D2Coding.ttf differ
diff --git a/web-ui/fonts/D2CodingBold.ttf b/web-ui/fonts/D2CodingBold.ttf
new file mode 100644
index 0000000..bd658b3
Binary files /dev/null and b/web-ui/fonts/D2CodingBold.ttf differ
diff --git a/web-ui/styles.css b/web-ui/styles.css
index c49906a..b2899e6 100644
--- a/web-ui/styles.css
+++ b/web-ui/styles.css
@@ -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; }