diff --git a/web/src/components/Shell.jsx b/web/src/components/Shell.jsx index 6897709..637b7d4 100644 --- a/web/src/components/Shell.jsx +++ b/web/src/components/Shell.jsx @@ -1,73 +1,69 @@ import { useState, useRef, useEffect, useCallback } from 'react' -import { Terminal as XTerm } from '@xterm/xterm' -import { FitAddon } from '@xterm/addon-fit' -import { WebLinksAddon } from '@xterm/addon-web-links' -import { Plus, X, Monitor, Globe, ChevronDown, Pencil, Trash2, Search, Copy, Send, Eye } from 'lucide-react' -import '@xterm/xterm/css/xterm.css' -import { useI18n } from '../i18n' - -const MAX_TABS = 7 -const SHELL_MAX_TOKENS = 100000 -const TABS_STORAGE_KEY = 'muyue_shell_tabs' -const TERMINAL_BUFFER_KEY = 'muyue_terminal_buffers' - -function renderContent(text) { - const parts = [] - const codeBlockRegex = /(```[\s\S]*?```)/g - let match - let lastIndex = 0 - while ((match = codeBlockRegex.exec(text)) !== null) { - if (match.index > lastIndex) { - parts.push({ type: 'text', content: text.slice(lastIndex, match.index) }) - } - const full = match[1] - const firstNewline = full.indexOf('\n') - const lang = firstNewline > -1 ? full.slice(3, firstNewline).trim() : '' - const code = firstNewline > -1 ? full.slice(firstNewline + 1, -3) : full.slice(3, -3) - parts.push({ type: 'code', lang, content: code }) - lastIndex = match.index + full.length - } - if (lastIndex < text.length) { - const remaining = text.slice(lastIndex) - const openBlock = remaining.match(/```(\w*)\n?([\s\S]*)$/) - if (openBlock) { - if (openBlock.index > 0) { - parts.push({ type: 'text', content: remaining.slice(0, openBlock.index) }) - } - parts.push({ type: 'code', lang: openBlock[1] || '', content: openBlock[2] || '' }) - } else { - parts.push({ type: 'text', content: remaining }) - } - } - return parts +impo +// === Style thème système pour xterm === +function getCSSVariable(varName) { + if (typeof document === 'undefined') return null; + return getComputedStyle(document.documentElement).getPropertyValue(varName).trim() || null; } -function formatText(text) { - let html = text - .replace(/&/g, '&').replace(//g, '>') +function parseHexColor(hex) { + if (!hex || hex.startsWith('var(')) return null; + hex = hex.replace('#', ''); + if (hex.length === 3) hex = hex.split('').map(c => c + c).join(''); + if (hex.length !== 6) return null; + const r = parseInt(hex.slice(0, 2), 16); + const g = parseInt(hex.slice(2, 4), 16); + const b = parseInt(hex.slice(4, 6), 16); + return { r, g, b }; +} - html = html - .replace(/\*\*(.+?)\*\*/g, '$1') - .replace(/`([^`]+)`/g, '$1') - .replace(/^### (.+)$/gm, '

$1

') - .replace(/^## (.+)$/gm, '

$1

') - .replace(/^# (.+)$/gm, '

$1

') - .replace(/^\s*[-*] (.+)$/gm, '
• $1
') - .replace(/^\s*(\d+)[.)] (.+)$/gm, '
$1 $2
') - .replace(/\n/g, '
') +function toRgbString(hex) { + const c = parseHexColor(hex); + if (!c) return '#000000'; + return `#${c.r.toString(16).padStart(2, '0')}${c.g.toString(16).padStart(2, '0')}${c.b.toString(16).padStart(2, '0')}`; +} - html = html - .replace(/\s*/g, '
') - .replace(/\s*( { @@ -291,7 +290,7 @@ export default function Shell({ api }) { const [terminalSettings, setTerminalSettings] = useState({ fontSize: 12, fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace", - theme: 'default', + theme: 'system', }) useEffect(() => { settingsRef.current = terminalSettings }, [terminalSettings]) @@ -356,7 +355,7 @@ export default function Shell({ api }) { setTerminalSettings({ fontSize: d.terminal.font_size || 12, fontFamily: d.terminal.font_family || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace", - theme: d.terminal.theme || 'default', + theme: d.terminal.theme || 'system', }) } }).catch(() => {}) diff --git a/web/src/styles/global.css b/web/src/styles/global.css index 63d34f6..1443ab8 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -1058,3 +1058,76 @@ input::placeholder { color: var(--text-disabled); } word-break: break-word; background: var(--bg); } + +/* === XTerm Custom Styling === */ +/* Styles for xterm.js integrated with Muyue theme */ +.shell-xterm-instance .xterm { + padding: 4px 8px; +} + +.shell-xterm-instance .xterm-viewport { + background-color: var(--bg-base) !important; +} + +.shell-xterm-instance .xterm-screen { + background-color: var(--bg-base); +} + +/* Scrollbar styling for xterm */ +.shell-xterm-instance .xterm-viewport::-webkit-scrollbar { + width: 8px; +} + +.shell-xterm-instance .xterm-viewport::-webkit-scrollbar-track { + background: var(--bg-surface); +} + +.shell-xterm-instance .xterm-viewport::-webkit-scrollbar-thumb { + background: var(--accent-dim); + border-radius: 4px; +} + +.shell-xterm-instance .xterm-viewport::-webkit-scrollbar-thumb:hover { + background: var(--accent-dark); +} + +/* Selection styling */ +.shell-xterm-instance .xterm-selection { + background: var(--accent-dim) !important; +} + +/* Focus ring styling */ +.shell-xterm-instance .xterm:focus .xterm-helper-text-container { + box-shadow: none; +} + +/* Ensure consistent font rendering */ +.shell-xterm-instance .xterm .xterm-char-measure-element { + font-family: var(--font-mono) !important; +} + +/* Bell animation styling */ +.shell-xterm-instance .xterm-bell { + animation: xterm-bell-flash 0.3s ease-out; +} + +@keyframes xterm-bell-flash { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 0; } +} + +/* Cursor styling */ +.shell-xterm-instance .xterm-cursor { + outline: none !important; +} + +/* Link styling for web links addon */ +.shell-xterm-instance .xterm-link { + color: var(--accent-light) !important; + text-decoration: underline; +} + +.shell-xterm-instance .xterm-link:hover { + color: var(--accent-muted) !important; +}