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
}
function formatText(text) {
let html = text
.replace(/&/g, '&').replace(//g, '>')
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, '
')
html = html
.replace(/
\s*
/g, '
')
.replace(/
\s*( {
ws.send(JSON.stringify(initPayload))
const dims = fitAddon.proposeDimensions()
if (dims) {
ws.send(JSON.stringify({ type: 'resize', rows: dims.rows, cols: dims.cols }))
}
if (onStateChange) onStateChange(true)
})
let firstMessage = true
ws.addEventListener('message', (event) => {
if (firstMessage) {
firstMessage = false
if (onFirstMessage) onFirstMessage()
}
try {
const msg = JSON.parse(event.data)
if (msg.type === 'output') {
term.write(msg.data)
} else if (msg.type === 'error') {
term.write(`\r\n\x1b[31mError: ${msg.data}\x1b[0m\r\n`)
}
} catch {
term.write(event.data)
}
})
ws.addEventListener('close', () => {
term.write('\r\n\x1b[33m— Connection closed —\x1b[0m\r\n')
if (onStateChange) onStateChange(false)
})
ws.addEventListener('error', () => {
term.write('\r\n\x1b[31m— Connection error —\x1b[0m\r\n')
if (onStateChange) onStateChange(false)
})
term.onResize(({ rows, cols }) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'resize', rows, cols }))
}
})
return ws
}
export default function Shell({ api }) {
const { t } = useI18n()
const tabsRef = useRef({})
const nextIdRef = useRef(1)
const settingsRef = useRef({ fontSize: 12, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'default' })
const pendingCommandsRef = useRef({})
const savedTabs = (() => {
try {
const raw = localStorage.getItem(TABS_STORAGE_KEY)
if (raw) {
const parsed = JSON.parse(raw)
if (Array.isArray(parsed) && parsed.length > 0) {
return parsed.map(t => ({ ...t, connected: false }))
}
}
} catch {}
return null
})()
const [tabs, setTabs] = useState(savedTabs || [
{ id: 1, name: 'Local Shell', type: 'local', shell: '', connected: false },
])
const [activeTab, setActiveTab] = useState(() => {
if (savedTabs) {
return savedTabs[0]?.id || 1
}
return 1
})
const activeTabRef = useRef(activeTab)
useEffect(() => { activeTabRef.current = activeTab }, [activeTab])
const [sshConnections, setSshConnections] = useState([])
const [systemTerminals, setSystemTerminals] = useState([])
const [showMenu, setShowMenu] = useState(false)
const [showSshModal, setShowSshModal] = useState(false)
const [editingTab, setEditingTab] = useState(null)
const [editName, setEditName] = useState('')
const [terminalSettings, setTerminalSettings] = useState({
fontSize: 12,
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
theme: 'default',
})
useEffect(() => { settingsRef.current = terminalSettings }, [terminalSettings])
const [sshForm, setSshForm] = useState({
name: '', host: '', port: 22, user: '', key_path: '',
})
const [aiMessages, setAiMessages] = useState([])
const [aiInput, setAiInput] = useState('')
const [aiLoading, setAiLoading] = useState(false)
const [aiTokens, setAiTokens] = useState(0)
const [aiAtLimit, setAiAtLimit] = useState(false)
const [analyzing, setAnalyzing] = useState(false)
const [showAnalysis, setShowAnalysis] = useState(false)
const [analysisContent, setAnalysisContent] = useState('')
const aiMessagesRef = useRef(null)
const aiLoadedRef = useRef(false)
const aiLoadingRef = useRef(false)
useEffect(() => {
aiMessagesRef.current?.scrollTo(0, aiMessagesRef.current.scrollHeight)
}, [aiMessages])
useEffect(() => {
api.getShellAnalysis?.().then(d => {
if (d?.analysis) setAnalysisContent(d.analysis)
}).catch(() => {
const stored = localStorage.getItem('shell_analysis')
if (stored) setAnalysisContent(stored)
})
}, [])
useEffect(() => {
if (aiLoadedRef.current) return
aiLoadedRef.current = true
api.getShellChatHistory().then(d => {
if (d.messages && d.messages.length > 0) {
setAiMessages(d.messages)
} else {
setAiMessages([{ role: 'assistant', content: t('shell.aiWelcome') || 'Système Analyste prêt. Tapez /help pour les commandes.' }])
}
setAiTokens(d.tokens || 0)
setAiAtLimit(d.at_limit || false)
}).catch(() => {
setAiMessages([{ role: 'assistant', content: 'Système Analyste prêt.' }])
})
}, [])
useEffect(() => {
const maxId = tabs.reduce((max, t) => Math.max(max, t.id), 0)
nextIdRef.current = maxId + 1
}, [])
useEffect(() => {
api.getTerminalSessions().then(d => {
setSshConnections(d.ssh || [])
setSystemTerminals(d.system || [])
}).catch(() => {})
api.getConfig().then(d => {
if (d.terminal) {
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',
})
}
}).catch(() => {})
}, [])
const initTerminal = useCallback((tabId, tab) => {
if (tabsRef.current[tabId]) return
const container = document.getElementById(`terminal-${tabId}`)
if (!container) return
const s = settingsRef.current
const { term, fitAddon } = createTerminal(container, {
fontSize: s.fontSize,
fontFamily: s.fontFamily,
theme: s.theme,
})
let initPayload
if (tab.type === 'ssh') {
initPayload = {
type: 'ssh',
data: JSON.stringify({
host: tab.host,
port: tab.port || 22,
user: tab.user || 'root',
key_path: tab.key_path || '',
}),
}
} else {
initPayload = {
type: 'shell',
data: tab.shell || '',
}
}
let disposed = false
const saveBuffer = () => {
try {
const buf = term.buffer.active
const lines = []
for (let i = 0; i < buf.length; i++) {
const line = buf.getLine(i)
if (line) lines.push(line.translateToString(true))
}
const savedBuffers = JSON.parse(sessionStorage.getItem(TERMINAL_BUFFER_KEY) || '{}')
savedBuffers[tabId] = lines.join('\n')
sessionStorage.setItem(TERMINAL_BUFFER_KEY, JSON.stringify(savedBuffers))
} catch (e) { console.warn('[Shell] Buffer save failed:', e) }
}
const onWsState = (connected) => {
if (disposed) return
if (!connected) saveBuffer()
setTabs(prev => prev.map(t => t.id === tabId ? { ...t, connected } : t))
}
const restoreBuffer = () => {
try {
const savedBuffers = JSON.parse(sessionStorage.getItem(TERMINAL_BUFFER_KEY) || '{}')
if (savedBuffers[tabId]) {
term.write('\x1b[90m— session restaurée —\x1b[0m\r\n')
term.write(savedBuffers[tabId])
}
} catch (e) { console.warn('[Shell] Buffer restore failed:', e) }
}
const ws = connectWebSocket(term, fitAddon, initPayload, onWsState, restoreBuffer)
const clearBufferOnClear = () => {
try {
const buf = term.buffer.active
const lineY = buf.length - 1
const line = buf.getLine(lineY)
if (line) {
const text = line.translateToString(true).trim().toLowerCase()
if (text === 'clear' || text === '$ clear' || text.endsWith(' clear')) {
const savedBuffers = JSON.parse(sessionStorage.getItem(TERMINAL_BUFFER_KEY) || '{}')
delete savedBuffers[tabId]
sessionStorage.setItem(TERMINAL_BUFFER_KEY, JSON.stringify(savedBuffers))
}
}
} catch (e) { console.warn('[Shell] Clear detection failed:', e) }
}
term.onData((data) => {
if (data === '\r') clearBufferOnClear()
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'input', data }))
}
})
const onResize = () => {
fitAddon.fit()
}
const resizeObserver = new ResizeObserver(onResize)
resizeObserver.observe(container)
window.addEventListener('resize', onResize)
const bufferSaveInterval = setInterval(() => { if (!disposed) saveBuffer() }, 5000)
console.log(`[Shell] initTerminal tab=${tabId} type=${tab.type} name="${tab.name}" shell="${tab.shell || '(default)'}"`)
tabsRef.current[tabId] = { term, fitAddon, ws, resizeObserver, onResize, bufferSaveInterval, saveBuffer, disposed: () => disposed }
tabsRef.current[tabId]._markDisposed = () => { disposed = true }
console.log(`[Shell] initTerminal tab=${tabId} done, tabsRef keys:`, Object.keys(tabsRef.current))
const pending = pendingCommandsRef.current[tabId]
if (pending && pending.length > 0) {
console.log(`[Shell] Flushing ${pending.length} pending commands for tab ${tabId}`)
for (const cmd of pending) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'input', data: cmd + '\r' }))
}
}
delete pendingCommandsRef.current[tabId]
}
}, [])
const initPendingTabs = useCallback(() => {
for (const tab of tabsRef.current._tabList || []) {
if (!tabsRef.current[tab.id]) {
const container = document.getElementById(`terminal-${tab.id}`)
if (container && container.offsetHeight > 0) {
initTerminal(tab.id, tab)
}
}
}
requestAnimationFrame(() => {
for (const tab of tabsRef.current._tabList || []) {
const entry = tabsRef.current[tab.id]
if (entry) entry.fitAddon.fit()
}
setTimeout(() => {
for (const tab of tabsRef.current._tabList || []) {
const entry = tabsRef.current[tab.id]
if (entry) entry.fitAddon.fit()
}
}, 150)
})
}, [initTerminal])
useEffect(() => {
tabsRef.current._tabList = tabs
}, [tabs])
useEffect(() => {
let cancelled = false
const pending = []
const tryInitTab = (tab, attempt) => {
if (cancelled) return
const shellCol = document.querySelector('.shell-terminal-col')
if (!shellCol || shellCol.offsetParent === null) {
pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 200))
return
}
const container = document.getElementById(`terminal-${tab.id}`)
if (!container) {
pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 100))
return
}
if (container.offsetHeight === 0) {
pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 100))
return
}
if (!tabsRef.current[tab.id]) {
initTerminal(tab.id, tab)
}
requestAnimationFrame(() => {
if (cancelled) return
const entry = tabsRef.current[tab.id]
if (entry) {
entry.fitAddon.fit()
setTimeout(() => { if (!cancelled) entry.fitAddon.fit() }, 100)
}
})
if (!tabsRef.current[tab.id]) {
tryInitTab(tab, 0)
}
}
const wrapper = document.querySelector('.shell-layout')?.parentElement
let observer
if (wrapper) {
observer = new MutationObserver(() => {
if (!wrapper.classList.contains('tab-hidden') && wrapper.offsetParent !== null) {
initPendingTabs()
}
})
observer.observe(wrapper, { attributes: true, attributeFilter: ['class'] })
}
return () => {
cancelled = true
pending.forEach(clearTimeout)
observer?.disconnect()
}
}, [tabs, initTerminal, initPendingTabs])
useEffect(() => {
const entry = tabsRef.current[activeTab]
if (entry) {
requestAnimationFrame(() => {
if (activeTabRef.current === activeTab) {
entry.fitAddon.fit()
}
})
}
}, [activeTab])
useEffect(() => {
const iv = setInterval(() => {
const wrapper = document.querySelector('.shell-layout')?.parentElement
if (wrapper && wrapper.classList.contains('tab-hidden')) return
const entry = tabsRef.current[activeTabRef.current]
if (entry) {
entry.fitAddon.fit()
}
}, 2000)
return () => clearInterval(iv)
}, [tabs])
useEffect(() => {
return () => {
for (const [tabId, entry] of Object.entries(tabsRef.current)) {
entry._markDisposed?.()
if (entry.bufferSaveInterval) clearInterval(entry.bufferSaveInterval)
window.removeEventListener('resize', entry.onResize)
entry.resizeObserver?.disconnect()
entry.ws?.close()
entry.term?.dispose()
}
tabsRef.current = {}
}
}, [])
useEffect(() => {
const onKey = (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
if (!e.altKey && !(e.key === 'Tab' && e.shiftKey)) return
if (e.key === 'Tab' && e.shiftKey) {
const shellTab = document.querySelector('.shell-layout')
if (!shellTab || shellTab.closest('.tab-hidden')) return
e.preventDefault()
const idx = tabs.findIndex(t => t.id === activeTab)
const next = (idx + 1) % tabs.length
setActiveTab(tabs[next].id)
return
}
const num = parseInt(e.key)
if (num >= 1 && num <= tabs.length) {
e.preventDefault()
setActiveTab(tabs[num - 1].id)
}
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [tabs])
const addLocalTab = (shell, name) => {
if (tabs.length >= MAX_TABS) return
const id = nextIdRef.current++
const newTab = { id, name: name || `${t('shell.localShell')} ${tabs.length}`, type: 'local', shell: shell || '', connected: false }
setTabs(prev => { const next = [...prev, newTab]; localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(next.map(t => ({ id: t.id, name: t.name, type: t.type, shell: t.shell, host: t.host, port: t.port, user: t.user, key_path: t.key_path })))); return next })
setActiveTab(id)
setShowMenu(false)
}
const addSSHTab = (conn) => {
if (tabs.length >= MAX_TABS) return
const id = nextIdRef.current++
const newTab = {
id,
name: conn.name || `${conn.user}@${conn.host}`,
type: 'ssh',
host: conn.host,
port: conn.port || 22,
user: conn.user || 'root',
key_path: conn.key_path || '',
connected: false,
}
setTabs(prev => { const next = [...prev, newTab]; localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(next.map(t => ({ id: t.id, name: t.name, type: t.type, shell: t.shell, host: t.host, port: t.port, user: t.user, key_path: t.key_path })))); return next })
setActiveTab(id)
setShowMenu(false)
}
const closeTab = (tabId, e) => {
if (e) e.stopPropagation()
const entry = tabsRef.current[tabId]
if (entry) {
entry._markDisposed?.()
entry.saveBuffer?.()
if (entry.bufferSaveInterval) clearInterval(entry.bufferSaveInterval)
window.removeEventListener('resize', entry.onResize)
entry.resizeObserver.disconnect()
entry.ws.close()
entry.term.dispose()
delete tabsRef.current[tabId]
}
try {
const savedBuffers = JSON.parse(sessionStorage.getItem(TERMINAL_BUFFER_KEY) || '{}')
delete savedBuffers[tabId]
sessionStorage.setItem(TERMINAL_BUFFER_KEY, JSON.stringify(savedBuffers))
} catch (e) { console.warn('[Shell] Buffer cleanup failed:', e) }
setTabs(prev => {
if (prev.length <= 1) return prev
const next = prev.filter(t => t.id !== tabId)
if (activeTab === tabId && next.length > 0) {
setActiveTab(next[next.length - 1].id)
}
return next
})
}
const startRename = (tabId, e) => {
if (e) e.stopPropagation()
const tab = tabs.find(t => t.id === tabId)
setEditingTab(tabId)
setEditName(tab.name)
}
const finishRename = () => {
if (editName.trim() && editingTab) {
setTabs(prev => prev.map(t => t.id === editingTab ? { ...t, name: editName.trim() } : t))
}
setEditingTab(null)
setEditName('')
}
const saveSSHConnection = async () => {
if (!sshForm.name.trim() || !sshForm.host.trim()) return
try {
await api.addSSHConnection(sshForm)
setSshConnections(prev => [...prev, { ...sshForm }])
setSshForm({ name: '', host: '', port: 22, user: '', key_path: '' })
setShowSshModal(false)
} catch (err) {
console.error(err)
}
}
const deleteSSHConnection = async (name) => {
try {
await api.deleteSSHConnection(name)
setSshConnections(prev => prev.filter(c => c.name !== name))
} catch (err) {
console.error(err)
}
}
const sendToTerminal = useCallback((code, tabId) => {
const targetId = tabId || activeTabRef.current
const entry = tabsRef.current[targetId]
if (!entry) {
console.warn(`[Shell] sendToTerminal: tab ${targetId} not ready. Queueing. tabsRef:`, Object.keys(tabsRef.current), 'activeTab:', activeTabRef.current, 'requested:', tabId)
if (!pendingCommandsRef.current[targetId]) pendingCommandsRef.current[targetId] = []
pendingCommandsRef.current[targetId].push(code)
return
}
if (!entry.ws || entry.ws.readyState !== WebSocket.OPEN) {
console.warn(`[Shell] sendToTerminal: WS not open for tab ${targetId} (state=${entry.ws?.readyState}). Queueing.`)
if (!pendingCommandsRef.current[targetId]) pendingCommandsRef.current[targetId] = []
pendingCommandsRef.current[targetId].push(code)
return
}
console.log(`[Shell] sendToTerminal: tab ${targetId} ← ${code.length} chars`)
entry.ws.send(JSON.stringify({ type: 'input', data: code + '\r' }))
}, [])
const focusAiTerminal = useCallback(() => {
const entry = tabsRef.current[activeTabRef.current]
if (entry) entry.term.focus()
}, [])
const _sendAiMessage = useCallback(async (text, fromEvent = false) => {
if (!text || !text.trim() || aiLoadingRef.current || aiAtLimit) return
const trimmed = text.trim()
aiLoadingRef.current = true
if (!fromEvent) {
setAiInput('')
focusAiTerminal()
}
if (trimmed === '/clear') {
try {
await api.clearShellChat()
setAiMessages([{ role: 'assistant', content: t('shell.aiWelcome') || 'Contexte effacé. Prêt.' }])
setAiTokens(0)
setAiAtLimit(false)
} catch {}
aiLoadingRef.current = false
return
}
if (trimmed === '/help') {
setAiMessages(prev => [...prev,
{ role: 'user', content: trimmed },
{ role: 'assistant', content: 'Commandes disponibles:\n• /clear — Effacer la conversation\n• /help — Afficher l\'aide\n\nJe ne peux pas exécuter de code. Les blocs de code proposés peuvent être copiés ou envoyés directement au terminal actif.' }
])
aiLoadingRef.current = false
return
}
const currentTab = activeTabRef.current
console.log(`[Shell] _sendAiMessage: activeTab=${currentTab}, fromEvent=${fromEvent}, text="${trimmed.slice(0, 50)}"`)
setAiMessages(prev => [...prev, { role: 'user', content: trimmed, _tabId: currentTab }])
setAiLoading(true)
try {
let accumulated = ''
await api.sendShellChat(trimmed, {}, true, (partial) => {
accumulated = partial
setAiMessages(prev => {
const filtered = prev.filter(m => !m._streaming)
return [...filtered, { role: 'assistant', content: partial, _streaming: true, _tabId: currentTab }]
})
})
setAiMessages(prev => {
const filtered = prev.filter(m => !m._streaming)
return [...filtered, { role: 'assistant', content: accumulated, _tabId: currentTab }]
})
api.getShellChatHistory().then(d => {
setAiTokens(d.tokens || 0)
setAiAtLimit(d.at_limit || false)
}).catch(() => {})
} catch (err) {
if (err.message?.includes('context limit')) {
setAiAtLimit(true)
}
setAiMessages(prev => [...prev.filter(m => !m._streaming), { role: 'assistant', content: `Erreur: ${err.message}` }])
}
setAiLoading(false)
aiLoadingRef.current = false
}, [api, t, aiAtLimit, focusAiTerminal])
const handleAiSend = () => _sendAiMessage(aiInput, false)
useEffect(() => {
const handler = (e) => {
const msg = e.detail?.message
if (!msg) return
setAiInput(msg)
setTimeout(() => _sendAiMessage(msg, true), 100)
}
window.addEventListener('ask-ai-terminal', handler)
return () => window.removeEventListener('ask-ai-terminal', handler)
}, [_sendAiMessage])
const handleAnalyze = async () => {
setAnalyzing(true)
setAiMessages(prev => [...prev, { role: 'system', content: 'Analyse du système en cours...' }])
try {
const d = await api.analyzeSystem()
if (d.analysis) {
setAnalysisContent(d.analysis)
localStorage.setItem('shell_analysis', d.analysis)
}
setAiMessages(prev => [...prev.filter(m => m.content !== 'Analyse du système en cours...'), {
role: 'system',
content: 'Analyse système terminée et sauvegardée. Le contexte système est maintenant disponible.'
}])
} catch (err) {
setAiMessages(prev => prev.filter(m => m.content !== 'Analyse du système en cours...'))
}
setAnalyzing(false)
}
return (
{tabs.map((tab, i) => (
setActiveTab(tab.id)}
onDoubleClick={(e) => startRename(tab.id, e)}
>
{tab.type === 'ssh' && }
{tab.type === 'local' && }
{editingTab === tab.id ? (
setEditName(e.target.value)}
onBlur={finishRename}
onKeyDown={e => { if (e.key === 'Enter') finishRename(); if (e.key === 'Escape') setEditingTab(null) }}
autoFocus
onClick={e => e.stopPropagation()}
/>
) : (
{tab.name}
)}
{i + 1}
{tabs.length > 1 && (
)}
))}
{tabs.length < MAX_TABS && (
{showMenu && (
<>
setShowMenu(false)} />
{t('shell.systemTerminals')}
{systemTerminals.map(st => (
))}
{t('shell.savedConnections')}
{sshConnections.length === 0 && (
{t('shell.noConnections')}
)}
{sshConnections.map(conn => (
))}
>
)}
)}
Analyste Système
= SHELL_MAX_TOKENS * 0.8 ? 'warn' : ''}`}
style={{ width: `${Math.min(100, (aiTokens / SHELL_MAX_TOKENS) * 100)}%` }}
/>
{Math.round(aiTokens / 1000)}k/{Math.round(SHELL_MAX_TOKENS / 1000)}k
{aiMessages.map((msg, i) => (
))}
{aiLoading &&
}
setAiInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleAiSend()}
placeholder={aiAtLimit ? '/clear pour continuer' : t('shell.askAi')}
disabled={aiAtLimit && aiInput !== '/clear'}
/>
{showAnalysis && analysisContent && (
setShowAnalysis(false)}>
e.stopPropagation()}>
Analyse Système
{renderContent(analysisContent).map((part, i) =>
part.type === 'code' ? (
{part.lang &&
{part.lang}
}
{part.content}
) : (
)
)}
)}
{showSshModal && (
setShowSshModal(false)}>
e.stopPropagation()}>
{t('shell.addConnection')}
)}
)
}
function ShellAIMessage({ msg, sendToTerminal, terminalTabId }) {
const role = msg.role === 'user' ? 'user' : msg.role === 'system' ? 'system' : 'assistant'
const content = msg.content || ''
if (role === 'user') {
return
{content}
}
if (role === 'system') {
return
{content}
}
const parts = renderContent(content)
return (
{parts.map((part, i) => {
if (part.type === 'code') {
return (
{part.lang &&
{part.lang}
}
{part.content}
)
}
return
})}
)
}