diff --git a/web-ui/code.html b/web-ui/code.html index d8aae9b..e5ff92a 100644 --- a/web-ui/code.html +++ b/web-ui/code.html @@ -4,10 +4,14 @@ πŸ’» μ½”λ“œ 에디터 + - + @@ -227,6 +231,11 @@
λ‘œλ”© 쀑...
+ +
+ + +
@@ -276,6 +285,29 @@
μƒˆ 터미널 νƒ­ μ—΄λ©΄ 이 경둜둜 μžλ™ 이동
+
+
에디터 폰트
+ +
+
+
폰트 μ›¨μ΄νŠΈ
+ +
폰트 크기
@@ -321,7 +353,7 @@
-
+
πŸ€–
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; }