-
- {t('config.resetConfirm')}
+
+ Cette action supprimera toute votre configuration et relancera l'application.
+
+
+
+
+ {showResetModal && (
+
setShowResetModal(false)}>
+
e.stopPropagation()}>
+
+
+ {t('config.resetConfig')}
-
-
-
+
+
+ {t('config.resetConfirm')}
+
+
+ Cette action est irréversible. Toute votre configuration (profil, clés API, préférences) sera supprimée.
+
+
+
+
+
- ) : (
-
- )}
-
+
+ )}
>
)
}
diff --git a/web/src/components/Shell.jsx b/web/src/components/Shell.jsx
index 2a695d2..859d4a0 100644
--- a/web/src/components/Shell.jsx
+++ b/web/src/components/Shell.jsx
@@ -1,15 +1,15 @@
-import { useState, useRef, useEffect, useCallback, useMemo } 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, Bot } from 'lucide-react'
+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 AI_TAB_ID = 0
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 = []
@@ -132,7 +132,7 @@ function createTerminal(container, settings = {}) {
const theme = getTheme(settings.theme || 'default')
const term = new XTerm({
cursorBlink: true,
- fontSize: settings.fontSize || 14,
+ fontSize: settings.fontSize || 12,
fontFamily: settings.fontFamily || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
theme,
allowTransparency: false,
@@ -201,7 +201,7 @@ export default function Shell({ api }) {
const { t } = useI18n()
const tabsRef = useRef({})
const nextIdRef = useRef(1)
- const settingsRef = useRef({ fontSize: 14, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'default' })
+ const settingsRef = useRef({ fontSize: 12, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'default' })
const savedTabs = (() => {
try {
@@ -217,15 +217,13 @@ export default function Shell({ api }) {
})()
const [tabs, setTabs] = useState(savedTabs || [
- { id: AI_TAB_ID, name: 'AI Terminal', type: 'ai', shell: '', connected: false, ai: true },
{ id: 1, name: 'Local Shell', type: 'local', shell: '', connected: false },
])
const [activeTab, setActiveTab] = useState(() => {
if (savedTabs) {
- const aiTab = savedTabs.find(t => t.ai)
- return aiTab ? aiTab.id : savedTabs[0].id
+ return savedTabs[0]?.id || 1
}
- return AI_TAB_ID
+ return 1
})
const [sshConnections, setSshConnections] = useState([])
const [systemTerminals, setSystemTerminals] = useState([])
@@ -234,7 +232,7 @@ export default function Shell({ api }) {
const [editingTab, setEditingTab] = useState(null)
const [editName, setEditName] = useState('')
const [terminalSettings, setTerminalSettings] = useState({
- fontSize: 14,
+ fontSize: 12,
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
theme: 'default',
})
@@ -253,7 +251,6 @@ export default function Shell({ api }) {
const [analyzing, setAnalyzing] = useState(false)
const [showAnalysis, setShowAnalysis] = useState(false)
const [analysisContent, setAnalysisContent] = useState('')
- const [renderTick, setRenderTick] = useState(0)
const aiMessagesRef = useRef(null)
const aiLoadedRef = useRef(false)
@@ -261,12 +258,6 @@ export default function Shell({ api }) {
aiMessagesRef.current?.scrollTo(0, aiMessagesRef.current.scrollHeight)
}, [aiMessages])
- useEffect(() => {
- const ms = aiLoading ? 1000 : 5000
- const iv = setInterval(() => setRenderTick(t => t + 1), ms)
- return () => clearInterval(iv)
- }, [aiLoading])
-
useEffect(() => {
api.getShellAnalysis?.().then(d => {
if (d?.analysis) setAnalysisContent(d.analysis)
@@ -305,7 +296,7 @@ export default function Shell({ api }) {
api.getConfig().then(d => {
if (d.terminal) {
setTerminalSettings({
- fontSize: d.terminal.font_size || 14,
+ 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',
})
@@ -346,11 +337,58 @@ export default function Shell({ api }) {
const ws = connectWebSocket(term, fitAddon, initPayload)
+ // Restore saved terminal buffer
+ try {
+ const savedBuffers = JSON.parse(localStorage.getItem(TERMINAL_BUFFER_KEY) || '{}')
+ if (savedBuffers[tabId]) {
+ term.write(savedBuffers[tabId])
+ }
+ } catch {}
+
+ 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(localStorage.getItem(TERMINAL_BUFFER_KEY) || '{}')
+ savedBuffers[tabId] = lines.join('\n')
+ localStorage.setItem(TERMINAL_BUFFER_KEY, JSON.stringify(savedBuffers))
+ } catch {}
+ }
+
+ const bufferSaveInterval = setInterval(saveBuffer, 5000)
+
+ // Detect clear command to wipe saved buffer
+ let inputBuffer = ''
+ term.onData((data) => {
+ if (data === '\r') {
+ const cmd = inputBuffer.replace(/[\x1b\x00-\x1f]/g, '').trim().toLowerCase()
+ if (cmd === 'clear') {
+ try {
+ const savedBuffers = JSON.parse(localStorage.getItem(TERMINAL_BUFFER_KEY) || '{}')
+ delete savedBuffers[tabId]
+ localStorage.setItem(TERMINAL_BUFFER_KEY, JSON.stringify(savedBuffers))
+ } catch {}
+ }
+ inputBuffer = ''
+ } else if (data === '\x7f' || data === '\b') {
+ inputBuffer = inputBuffer.slice(0, -1)
+ } else if (data === '\x03') {
+ inputBuffer = ''
+ } else if (data.length === 1 && data.charCodeAt(0) >= 32) {
+ inputBuffer += data
+ }
+ })
+
ws.onopen = () => {
setTabs(prev => prev.map(t => t.id === tabId ? { ...t, connected: true } : t))
}
ws.onclose = () => {
+ saveBuffer()
setTabs(prev => prev.map(t => t.id === tabId ? { ...t, connected: false } : t))
}
@@ -369,7 +407,7 @@ export default function Shell({ api }) {
resizeObserver.observe(container)
window.addEventListener('resize', onResize)
- tabsRef.current[tabId] = { term, fitAddon, ws, resizeObserver, onResize }
+ tabsRef.current[tabId] = { term, fitAddon, ws, resizeObserver, onResize, bufferSaveInterval, saveBuffer }
}, [])
useEffect(() => {
@@ -444,7 +482,7 @@ export default function Shell({ api }) {
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, ai: t.ai || false, host: t.host, port: t.port, user: t.user, key_path: t.key_path })))); return next })
+ 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)
}
@@ -462,7 +500,7 @@ export default function Shell({ api }) {
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, ai: t.ai || false, host: t.host, port: t.port, user: t.user, key_path: t.key_path })))); return next })
+ 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)
}
@@ -470,10 +508,12 @@ export default function Shell({ api }) {
const closeTab = (tabId, e) => {
if (e) e.stopPropagation()
const tab = tabs.find(t => t.id === tabId)
- if (!tab || tab.ai || tabs.length <= 1) return
+ if (!tab || tabs.length <= 1) return
if (tabsRef.current[tabId]) {
- const { ws, resizeObserver, onResize, term } = tabsRef.current[tabId]
+ const { ws, resizeObserver, onResize, term, bufferSaveInterval, saveBuffer } = tabsRef.current[tabId]
+ if (saveBuffer) saveBuffer()
+ if (bufferSaveInterval) clearInterval(bufferSaveInterval)
window.removeEventListener('resize', onResize)
resizeObserver.disconnect()
ws.close()
@@ -527,19 +567,16 @@ export default function Shell({ api }) {
}
const sendToTerminal = useCallback((code) => {
- const aiEntry = tabsRef.current[AI_TAB_ID]
- if (aiEntry?.ws && aiEntry.ws.readyState === WebSocket.OPEN) {
- aiEntry.ws.send(JSON.stringify({ type: 'input', data: code + '\r' }))
+ const entry = tabsRef.current[activeTab]
+ if (entry?.ws && entry.ws.readyState === WebSocket.OPEN) {
+ entry.ws.send(JSON.stringify({ type: 'input', data: code + '\r' }))
}
- }, [])
+ }, [activeTab])
const focusAiTerminal = useCallback(() => {
- setActiveTab(AI_TAB_ID)
- setTimeout(() => {
- const entry = tabsRef.current[AI_TAB_ID]
- if (entry) entry.term.focus()
- }, 150)
- }, [])
+ const entry = tabsRef.current[activeTab]
+ if (entry) entry.term.focus()
+ }, [activeTab])
const handleAiSend = async () => {
if (!aiInput.trim() || aiLoading || aiAtLimit) return
@@ -596,21 +633,7 @@ export default function Shell({ api }) {
setAiLoading(false)
}
- useEffect(() => {
- const handler = (e) => {
- const msg = e.detail?.message
- if (!msg) return
- setAiInput(msg)
- setActiveTab(AI_TAB_ID)
- setTimeout(() => {
- handleAiSendDirect(msg)
- }, 100)
- }
- window.addEventListener('ask-ai-terminal', handler)
- return () => window.removeEventListener('ask-ai-terminal', handler)
- }, [])
-
- const handleAiSendDirect = async (text) => {
+ const handleAiSendDirect = useCallback(async (text) => {
if (!text || aiLoading || aiAtLimit) return
setAiInput('')
@@ -652,7 +675,20 @@ export default function Shell({ api }) {
setAiMessages(prev => [...prev.filter(m => !m._streaming), { role: 'assistant', content: `Erreur: ${err.message}` }])
}
setAiLoading(false)
- }
+ }, [api, t, aiLoading, aiAtLimit])
+
+ useEffect(() => {
+ const handler = (e) => {
+ const msg = e.detail?.message
+ if (!msg) return
+ setAiInput(msg)
+ setTimeout(() => {
+ handleAiSendDirect(msg)
+ }, 100)
+ }
+ window.addEventListener('ask-ai-terminal', handler)
+ return () => window.removeEventListener('ask-ai-terminal', handler)
+ }, [handleAiSendDirect])
const handleAnalyze = async () => {
setAnalyzing(true)
@@ -681,14 +717,13 @@ export default function Shell({ api }) {
{tabs.map((tab, i) => (
setActiveTab(tab.id)}
- onDoubleClick={(e) => !tab.ai && startRename(tab.id, e)}
+ onDoubleClick={(e) => startRename(tab.id, e)}
>
- {tab.ai && }
- {!tab.ai && tab.type === 'ssh' && }
- {!tab.ai && tab.type === 'local' && }
+ {tab.type === 'ssh' && }
+ {tab.type === 'local' && }
{editingTab === tab.id ? (
{tab.name}
)}
{i + 1}
- {!tab.ai && tabs.length > 1 && (
+ {tabs.length > 1 && (
{aiMessages.map((msg, i) => (
-
+
))}
{aiLoading &&
}
@@ -915,7 +950,7 @@ export default function Shell({ api }) {
)
}
-function ShellAIMessage({ msg, sendToTerminal, renderTick }) {
+function ShellAIMessage({ msg, sendToTerminal }) {
const role = msg.role === 'user' ? 'user' : msg.role === 'system' ? 'system' : 'assistant'
const content = msg.content || ''
@@ -934,7 +969,7 @@ function ShellAIMessage({ msg, sendToTerminal, renderTick }) {
{parts.map((part, i) => {
if (part.type === 'code') {
return (
-
+
{part.lang &&
{part.lang}
}
{part.content}
@@ -948,7 +983,7 @@ function ShellAIMessage({ msg, sendToTerminal, renderTick }) {
)
}
- return
+ return
})}
)
diff --git a/web/src/components/Studio.jsx b/web/src/components/Studio.jsx
index 915ffda..c35e2a1 100644
--- a/web/src/components/Studio.jsx
+++ b/web/src/components/Studio.jsx
@@ -309,7 +309,6 @@ export default function Studio({ api }) {
const [tokenInfo, setTokenInfo] = useState({ used: 0, max: 100000, summarizeAt: 80000 })
const [contextCollapsed, setContextCollapsed] = useState(false)
const [messagesCollapsed, setMessagesCollapsed] = useState(false)
- const [renderTick, setRenderTick] = useState(0)
const messagesEnd = useRef(null)
const feedRef = useRef(null)
const textareaRef = useRef(null)
@@ -342,12 +341,6 @@ export default function Studio({ api }) {
messagesEnd.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages, streaming, streamThinking, streamToolCalls])
- useEffect(() => {
- const ms = loading ? 1000 : 5000
- const iv = setInterval(() => setRenderTick(t => t + 1), ms)
- return () => clearInterval(iv)
- }, [loading])
-
useEffect(() => {
const onTab = (e) => {
if (e.key !== 'Tab') return
@@ -648,7 +641,7 @@ export default function Studio({ api }) {
return (
<>
{messages.slice(0, visibleCount).map(msg => (
-
+
))}