|
|
|
@@ -1,73 +1,69 @@
|
|
|
|
import { useState, useRef, useEffect, useCallback } from 'react'
|
|
|
|
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'
|
|
|
|
import { useI18n } from '../i18n'
|
|
|
|
|
|
|
|
// === Style thème système pour xterm ===
|
|
|
|
const MAX_TABS = 7
|
|
|
|
function getCSSVariable(varName) {
|
|
|
|
const SHELL_MAX_TOKENS = 100000
|
|
|
|
if (typeof document === 'undefined') return null;
|
|
|
|
const TABS_STORAGE_KEY = 'muyue_shell_tabs'
|
|
|
|
return getComputedStyle(document.documentElement).getPropertyValue(varName).trim() || null;
|
|
|
|
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
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatText(text) {
|
|
|
|
function parseHexColor(hex) {
|
|
|
|
let html = text
|
|
|
|
if (!hex || hex.startsWith('var(')) return null;
|
|
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
|
|
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
|
|
|
|
function toRgbString(hex) {
|
|
|
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
|
|
const c = parseHexColor(hex);
|
|
|
|
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
|
|
|
|
if (!c) return '#000000';
|
|
|
|
.replace(/^### (.+)$/gm, '<h4 class="msg-h4">$1</h4>')
|
|
|
|
return `#${c.r.toString(16).padStart(2, '0')}${c.g.toString(16).padStart(2, '0')}${c.b.toString(16).padStart(2, '0')}`;
|
|
|
|
.replace(/^## (.+)$/gm, '<h3 class="msg-h3">$1</h3>')
|
|
|
|
}
|
|
|
|
.replace(/^# (.+)$/gm, '<h2 class="msg-h2">$1</h2>')
|
|
|
|
|
|
|
|
.replace(/^\s*[-*] (.+)$/gm, '<div class="msg-bullet">• $1</div>')
|
|
|
|
|
|
|
|
.replace(/^\s*(\d+)[.)] (.+)$/gm, '<div class="msg-step"><span class="msg-step-num">$1</span> $2</div>')
|
|
|
|
|
|
|
|
.replace(/\n/g, '<br/>')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
html = html
|
|
|
|
function buildSystemTheme() {
|
|
|
|
.replace(/<br\/>\s*<br\/>/g, '<br/>')
|
|
|
|
const bg = getCSSVariable('--bg-base') || '#0F0D10';
|
|
|
|
.replace(/<br\/>\s*(<h[234]|<div class="msg-)/g, '$1')
|
|
|
|
const fg = getCSSVariable('--text-primary') || '#EAE0E2';
|
|
|
|
.replace(/(<\/h[234]|<\/div>)\s*<br\/>/g, '$1')
|
|
|
|
const accent = getCSSVariable('--accent-light') || '#FF1A5E';
|
|
|
|
.replace(/\s+on\w+=["'][^"']*["']/gi, '')
|
|
|
|
const accentDim = getCSSVariable('--accent-dim') || '#6B2033';
|
|
|
|
.replace(/javascript:/gi, '')
|
|
|
|
const success = '#00E676';
|
|
|
|
.replace(/data:/gi, '')
|
|
|
|
const warning = '#FFD740';
|
|
|
|
|
|
|
|
const error = getCSSVariable('--accent-bright') || '#FF1744';
|
|
|
|
return html
|
|
|
|
const bgSurface = getCSSVariable('--bg-surface') || bg;
|
|
|
|
|
|
|
|
const bgElevated = getCSSVariable('--bg-elevated') || bgSurface;
|
|
|
|
|
|
|
|
const textSecondary = getCSSVariable('--text-secondary') || fg;
|
|
|
|
|
|
|
|
const textTertiary = getCSSVariable('--text-tertiary') || textSecondary;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
background: toRgbString(bg),
|
|
|
|
|
|
|
|
foreground: toRgbString(fg),
|
|
|
|
|
|
|
|
cursor: toRgbString(accent),
|
|
|
|
|
|
|
|
cursorAccent: toRgbString(bg),
|
|
|
|
|
|
|
|
selectionBackground: `${toRgbString(accentDim)}44`,
|
|
|
|
|
|
|
|
selectionForeground: '#FFFFFF',
|
|
|
|
|
|
|
|
black: toRgbString(bgElevated),
|
|
|
|
|
|
|
|
red: toRgbString(error),
|
|
|
|
|
|
|
|
green: toRgbString(success),
|
|
|
|
|
|
|
|
yellow: toRgbString(warning),
|
|
|
|
|
|
|
|
blue: toRgbString(getCSSVariable('--accent') || '#448AFF'),
|
|
|
|
|
|
|
|
magenta: toRgbString(accent),
|
|
|
|
|
|
|
|
cyan: '#00BCD4',
|
|
|
|
|
|
|
|
white: toRgbString(fg),
|
|
|
|
|
|
|
|
brightBlack: toRgbString(bgSurface),
|
|
|
|
|
|
|
|
brightRed: toRgbString(accent),
|
|
|
|
|
|
|
|
brightGreen: toRgbString(success),
|
|
|
|
|
|
|
|
brightYellow: toRgbString(warning),
|
|
|
|
|
|
|
|
brightBlue: toRgbString(getCSSVariable('--accent-muted') || '#82B1FF'),
|
|
|
|
|
|
|
|
brightMagenta: toRgbString(getCSSVariable('--accent-soft') || '#FF80AB'),
|
|
|
|
|
|
|
|
brightCyan: '#84FFFF',
|
|
|
|
|
|
|
|
brightWhite: '#FFFFFF',
|
|
|
|
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const THEMES = {
|
|
|
|
const THEMES = {
|
|
|
|
|
|
|
|
system: buildSystemTheme(),
|
|
|
|
default: {
|
|
|
|
default: {
|
|
|
|
background: '#0A0A0C', foreground: '#EAE0E2', cursor: '#FF0033',
|
|
|
|
background: '#0A0A0C', foreground: '#EAE0E2', cursor: '#FF0033',
|
|
|
|
cursorAccent: '#0A0A0C', selectionBackground: '#FF003344', selectionForeground: '#ffffff',
|
|
|
|
cursorAccent: '#0A0A0C', selectionBackground: '#FF003344', selectionForeground: '#ffffff',
|
|
|
|
@@ -125,11 +121,14 @@ const THEMES = {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getTheme(themeName) {
|
|
|
|
function getTheme(themeName) {
|
|
|
|
return THEMES[themeName] || THEMES.default
|
|
|
|
if (themeName === 'system' || themeName === 'default') {
|
|
|
|
|
|
|
|
return buildSystemTheme()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return THEMES[themeName] || buildSystemTheme()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createTerminal(container, settings = {}) {
|
|
|
|
function createTerminal(container, settings = {}) {
|
|
|
|
const theme = getTheme(settings.theme || 'default')
|
|
|
|
const theme = getTheme(settings.theme || 'system')
|
|
|
|
const term = new XTerm({
|
|
|
|
const term = new XTerm({
|
|
|
|
cursorBlink: true,
|
|
|
|
cursorBlink: true,
|
|
|
|
fontSize: settings.fontSize || 12,
|
|
|
|
fontSize: settings.fontSize || 12,
|
|
|
|
@@ -237,10 +236,13 @@ function connectWebSocket(term, fitAddon, initPayload, onStateChange, onFirstMes
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function Shell({ api }) {
|
|
|
|
export default function Shell({ api }) {
|
|
|
|
|
|
|
|
const MAX_TABS = 7
|
|
|
|
|
|
|
|
const TABS_STORAGE_KEY = 'muyue_shell_tabs'
|
|
|
|
|
|
|
|
const TERMINAL_BUFFER_KEY = 'muyue_terminal_buffers'
|
|
|
|
const { t } = useI18n()
|
|
|
|
const { t } = useI18n()
|
|
|
|
const tabsRef = useRef({})
|
|
|
|
const tabsRef = useRef({})
|
|
|
|
const nextIdRef = useRef(1)
|
|
|
|
const nextIdRef = useRef(1)
|
|
|
|
const settingsRef = useRef({ fontSize: 12, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'default' })
|
|
|
|
const settingsRef = useRef({ fontSize: 12, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'system' })
|
|
|
|
const pendingCommandsRef = useRef({})
|
|
|
|
const pendingCommandsRef = useRef({})
|
|
|
|
|
|
|
|
|
|
|
|
const [tabs, setTabs] = useState(() => {
|
|
|
|
const [tabs, setTabs] = useState(() => {
|
|
|
|
@@ -271,9 +273,13 @@ export default function Shell({ api }) {
|
|
|
|
]
|
|
|
|
]
|
|
|
|
})
|
|
|
|
})
|
|
|
|
const [activeTab, setActiveTab] = useState(() => {
|
|
|
|
const [activeTab, setActiveTab] = useState(() => {
|
|
|
|
if (savedTabs) {
|
|
|
|
try {
|
|
|
|
return savedTabs[0]?.id || 1
|
|
|
|
const raw = localStorage.getItem(TABS_STORAGE_KEY)
|
|
|
|
}
|
|
|
|
if (raw) {
|
|
|
|
|
|
|
|
const parsed = JSON.parse(raw)
|
|
|
|
|
|
|
|
if (Array.isArray(parsed) && parsed.length > 0) return parsed[0]?.id || 1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch {}
|
|
|
|
return 1
|
|
|
|
return 1
|
|
|
|
})
|
|
|
|
})
|
|
|
|
const activeTabRef = useRef(activeTab)
|
|
|
|
const activeTabRef = useRef(activeTab)
|
|
|
|
@@ -287,7 +293,7 @@ export default function Shell({ api }) {
|
|
|
|
const [terminalSettings, setTerminalSettings] = useState({
|
|
|
|
const [terminalSettings, setTerminalSettings] = useState({
|
|
|
|
fontSize: 12,
|
|
|
|
fontSize: 12,
|
|
|
|
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
|
|
|
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
|
|
|
theme: 'default',
|
|
|
|
theme: 'system',
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { settingsRef.current = terminalSettings }, [terminalSettings])
|
|
|
|
useEffect(() => { settingsRef.current = terminalSettings }, [terminalSettings])
|
|
|
|
@@ -352,7 +358,7 @@ export default function Shell({ api }) {
|
|
|
|
setTerminalSettings({
|
|
|
|
setTerminalSettings({
|
|
|
|
fontSize: d.terminal.font_size || 12,
|
|
|
|
fontSize: d.terminal.font_size || 12,
|
|
|
|
fontFamily: d.terminal.font_family || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
|
|
|
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(() => {})
|
|
|
|
}).catch(() => {})
|
|
|
|
|