fix(ui): restore Shell.jsx to v0.9.0-beta.1 state
All checks were successful
Stable Release / stable (push) Successful in 1m38s

Revert terminal tab to its exact state at v0.9.0-beta.1, removing
split panes, file editor integration, and agent sessions added in
the v0.9.0 UI overhaul.

🤗 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-28 15:54:38 +02:00
parent b7b66634ea
commit 5a39a3a804

View File

@@ -6,21 +6,18 @@ import { WebglAddon } from '@xterm/addon-webgl'
import { SearchAddon } from '@xterm/addon-search' import { SearchAddon } from '@xterm/addon-search'
import { Unicode11Addon } from '@xterm/addon-unicode11' import { Unicode11Addon } from '@xterm/addon-unicode11'
import { ImageAddon } from '@xterm/addon-image' import { ImageAddon } from '@xterm/addon-image'
import { Plus, X, Monitor, Globe, ChevronDown, Pencil, Trash2, Search, Copy, Send, Eye, Bot, Columns, Rows, Maximize2 } from 'lucide-react' import { Plus, X, Monitor, Globe, ChevronDown, Pencil, Trash2, Search, Copy, Send, Eye, Bot } from 'lucide-react'
import '@xterm/xterm/css/xterm.css' import '@xterm/xterm/css/xterm.css'
import { useI18n } from '../i18n' import { useI18n } from '../i18n'
import mermaid from 'mermaid' import mermaid from 'mermaid'
import FileEditor from './FileEditor'
mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose', fontFamily: 'var(--font-mono)' }) mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose', fontFamily: 'var(--font-mono)' })
const AI_TAB_ID = 0 const AI_TAB_ID = 0
const MAX_TABS = 7 const MAX_TABS = 7
const MAX_PANES = 4
const SHELL_MAX_TOKENS = 100000 const SHELL_MAX_TOKENS = 100000
const SHELL_AI_COMMANDS = ['/clear', '/help', '/model', '/model change'] const SHELL_AI_COMMANDS = ['/clear', '/help', '/model', '/model change']
const TABS_STORAGE_KEY = 'muyue_shell_tabs' const TABS_STORAGE_KEY = 'muyue_shell_tabs'
const LAYOUT_STORAGE_KEY = 'muyue_shell_layout'
const TERMINAL_BUFFER_KEY = 'muyue_terminal_buffers' const TERMINAL_BUFFER_KEY = 'muyue_terminal_buffers'
function renderContent(text) { function renderContent(text) {
@@ -480,107 +477,6 @@ export default function Shell({ api, isSudo }) {
const _streamRafRef = useRef(null) const _streamRafRef = useRef(null)
const _streamPendingRef = useRef(null) const _streamPendingRef = useRef(null)
const [splitLayout, setSplitLayout] = useState(() => {
try {
const raw = localStorage.getItem(LAYOUT_STORAGE_KEY)
if (raw) return JSON.parse(raw)
} catch {}
return null
})
const [editingFile, setEditingFile] = useState(null)
const [agentSessions, setAgentSessions] = useState([])
const paneCount = useMemo(() => {
const count = (node) => {
if (!node) return 1
if (node.type === 'leaf') return 1
return count(node.children?.[0]) + count(node.children?.[1])
}
return count(splitLayout)
}, [splitLayout])
const splitPane = useCallback((direction) => {
if (paneCount >= MAX_PANES) return
const activeId = activeTabRef.current
setSplitLayout(prev => {
if (!prev) {
return { type: 'split', direction, ratio: 0.5, activePane: activeId, children: [
{ type: 'leaf', tabId: activeId },
{ type: 'leaf', tabId: null },
]}
}
const clone = JSON.parse(JSON.stringify(prev))
const findAndSplit = (node) => {
if (node.type === 'leaf' && node.tabId === activeId) {
return { type: 'split', direction, ratio: 0.5, activePane: activeId, children: [
{ type: 'leaf', tabId: activeId },
{ type: 'leaf', tabId: null },
]}
}
if (node.children) {
return { ...node, children: node.children.map(findAndSplit) }
}
return node
}
return findAndSplit(clone)
})
}, [paneCount])
const removePane = useCallback((tabId) => {
setSplitLayout(prev => {
if (!prev) return null
if (prev.type === 'leaf') return null
const clone = JSON.parse(JSON.stringify(prev))
const removeFromTree = (node) => {
if (node.type !== 'split') return node
const left = node.children[0]
const right = node.children[1]
const leftIsTarget = left.type === 'leaf' && left.tabId === tabId
const rightIsTarget = right.type === 'leaf' && right.tabId === tabId
if (leftIsTarget) return removeFromTree(right)
if (rightIsTarget) return removeFromTree(left)
return { ...node, children: [removeFromTree(left), removeFromTree(right)] }
}
const result = removeFromTree(clone)
if (result.type === 'leaf' && result.tabId === null) return null
if (result.type === 'split' && (!result.children || result.children.length < 2)) return result.children?.[0] || null
return result
})
}, [])
const assignPaneTab = useCallback((paneLeaf, tabId) => {
setSplitLayout(prev => {
if (!prev) return prev
const clone = JSON.parse(JSON.stringify(prev))
const assign = (node) => {
if (node === paneLeaf) return { ...node, tabId }
if (node.children) return { ...node, children: node.children.map(assign) }
return node
}
return assign(clone)
})
}, [])
useEffect(() => {
if (splitLayout) {
localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(splitLayout))
} else {
localStorage.removeItem(LAYOUT_STORAGE_KEY)
}
}, [splitLayout])
useEffect(() => {
api.getAgentSessions?.().then(d => {
setAgentSessions(d?.sessions || [])
}).catch(() => {})
const iv = setInterval(() => {
api.getAgentSessions?.().then(d => {
setAgentSessions(d?.sessions || [])
}).catch(() => {})
}, 5000)
return () => clearInterval(iv)
}, [])
const _flushStreamUpdate = useCallback(() => { const _flushStreamUpdate = useCallback(() => {
_streamRafRef.current = null _streamRafRef.current = null
const pending = _streamPendingRef.current const pending = _streamPendingRef.current
@@ -922,22 +818,6 @@ export default function Shell({ api, isSudo }) {
return return
} }
if (ctrl && e.shiftKey && e.key === 'D') {
const shellTab = document.querySelector('.shell-layout')
if (!shellTab || shellTab.closest('.tab-hidden')) return
e.preventDefault()
splitPane('vertical')
return
}
if (ctrl && e.shiftKey && e.key === 'H') {
const shellTab = document.querySelector('.shell-layout')
if (!shellTab || shellTab.closest('.tab-hidden')) return
e.preventDefault()
splitPane('horizontal')
return
}
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
if (!e.altKey && !(e.key === 'Tab' && e.shiftKey)) return if (!e.altKey && !(e.key === 'Tab' && e.shiftKey)) return
@@ -1438,27 +1318,6 @@ Sois concret : cite les vraies versions, les vrais chemins, les vrais nombres. L
{zoomLevel > 0 ? '+' : ''}{zoomLevel > 0 ? zoomLevel * 2 : zoomLevel * 2}px {zoomLevel > 0 ? '+' : ''}{zoomLevel > 0 ? zoomLevel * 2 : zoomLevel * 2}px
</span> </span>
)} )}
{paneCount < MAX_PANES && (
<>
<button className="shell-split-btn" onClick={() => splitPane('vertical')} title="Split Vertical (Ctrl+Shift+D)">
<Columns size={14} />
</button>
<button className="shell-split-btn" onClick={() => splitPane('horizontal')} title="Split Horizontal (Ctrl+Shift+H)">
<Rows size={14} />
</button>
</>
)}
{splitLayout && (
<button className="shell-split-btn" onClick={() => setSplitLayout(null)} title="Unsplit">
<Maximize2 size={14} />
</button>
)}
{agentSessions.length > 0 && (
<span className="shell-agent-indicator" title={`${agentSessions.length} agent(s) actif(s)`}>
<Bot size={12} />
<span className="shell-agent-count">{agentSessions.length}</span>
</span>
)}
{tabs.length < MAX_TABS && ( {tabs.length < MAX_TABS && (
<div className="shell-new-tab-wrapper"> <div className="shell-new-tab-wrapper">
<button className="shell-new-tab-btn" onClick={() => setShowMenu(!showMenu)} title={t('shell.newTab')}> <button className="shell-new-tab-btn" onClick={() => setShowMenu(!showMenu)} title={t('shell.newTab')}>
@@ -1525,59 +1384,34 @@ Sois concret : cite les vraies versions, les vrais chemins, les vrais nombres. L
</div> </div>
</div> </div>
<div className={`shell-xterm-wrapper ${splitLayout ? 'has-splits' : ''}`}> <div className="shell-xterm-wrapper">
{editingFile && ( {showSearch && (
<FileEditor api={api} filePath={editingFile} onClose={() => setEditingFile(null)} /> <div className="shell-search-bar">
)} <Search size={14} className="shell-search-icon" />
{!editingFile && ( <input
splitLayout ? ( ref={searchInputRef}
<SplitPaneRenderer className="shell-search-input"
node={splitLayout} value={searchText}
tabs={tabs} onChange={e => handleSearchChange(e.target.value)}
activeTab={activeTab} onKeyDown={e => {
setActiveTab={setActiveTab} if (e.key === 'Enter') { e.shiftKey ? handleSearchPrev() : handleSearchNext() }
showSearch={showSearch} if (e.key === 'Escape') handleCloseSearch()
searchText={searchText} e.stopPropagation()
searchInputRef={searchInputRef} }}
handleSearchChange={handleSearchChange} placeholder="Rechercher..."
handleSearchNext={handleSearchNext}
handleSearchPrev={handleSearchPrev}
handleCloseSearch={handleCloseSearch}
removePane={removePane}
onLayoutChange={setSplitLayout}
/> />
) : ( <button className="shell-search-nav" onClick={handleSearchPrev} title="Précédent (Shift+Entrée)"></button>
<> <button className="shell-search-nav" onClick={handleSearchNext} title="Suivant (Entrée)"></button>
{showSearch && ( <button className="shell-search-close" onClick={handleCloseSearch}><X size={14} /></button>
<div className="shell-search-bar"> </div>
<Search size={14} className="shell-search-icon" />
<input
ref={searchInputRef}
className="shell-search-input"
value={searchText}
onChange={e => handleSearchChange(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') { e.shiftKey ? handleSearchPrev() : handleSearchNext() }
if (e.key === 'Escape') handleCloseSearch()
e.stopPropagation()
}}
placeholder="Rechercher..."
/>
<button className="shell-search-nav" onClick={handleSearchPrev} title="Précédent (Shift+Entrée)"></button>
<button className="shell-search-nav" onClick={handleSearchNext} title="Suivant (Entrée)"></button>
<button className="shell-search-close" onClick={handleCloseSearch}><X size={14} /></button>
</div>
)}
{tabs.map(tab => (
<div
key={tab.id}
id={`terminal-${tab.id}`}
className={`shell-xterm-instance${activeTab === tab.id ? ' active' : ''}`}
/>
))}
</>
)
)} )}
{tabs.map(tab => (
<div
key={tab.id}
id={`terminal-${tab.id}`}
className={`shell-xterm-instance${activeTab === tab.id ? ' active' : ''}`}
/>
))}
</div> </div>
</div> </div>
@@ -1941,158 +1775,3 @@ const ShellAIMessage = memo(function ShellAIMessage({ msg, sendToTerminal, termi
</div> </div>
) )
}) })
function SplitPaneRenderer({ node, tabs, activeTab, setActiveTab, showSearch, searchText, searchInputRef, handleSearchChange, handleSearchNext, handleSearchPrev, handleCloseSearch, removePane, onLayoutChange }) {
if (!node) return null
if (node.type === 'leaf') {
const tabId = node.tabId
const tab = tabId ? tabs.find(t => t.id === tabId) : null
const isActive = activeTab === tabId
if (!tab && tabId !== null) {
const fallbackTab = tabs[0]
if (fallbackTab) {
return (
<div className="split-pane-leaf" onClick={() => setActiveTab(fallbackTab.id)}>
<div id={`terminal-${fallbackTab.id}`} className={`shell-xterm-instance${activeTab === fallbackTab.id ? ' active' : ''}`} />
</div>
)
}
}
if (!tab) {
return (
<div className="split-pane-leaf empty">
<div className="split-pane-empty">
<Monitor size={24} style={{ opacity: 0.3 }} />
<span style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 8 }}>
Select a tab for this pane
</span>
<div style={{ display: 'flex', gap: 4, marginTop: 8 }}>
{tabs.slice(0, 4).map(t => (
<button key={t.id} className="sm ghost" onClick={() => { onLayoutChange(prev => assignLeafTab(prev, node, t.id)) }}>
{t.name}
</button>
))}
</div>
</div>
</div>
)
}
return (
<div className={`split-pane-leaf ${isActive ? 'active' : ''}`} onClick={() => setActiveTab(tabId)}>
<div className="split-pane-header">
<span className="split-pane-title">{tab.name}</span>
<button className="split-pane-close" onClick={(e) => { e.stopPropagation(); removePane(tabId) }}>
<X size={10} />
</button>
</div>
<div className="split-pane-content">
<div id={`terminal-${tabId}`} className="shell-xterm-instance active" />
</div>
</div>
)
}
if (node.type === 'split') {
const dir = node.direction === 'horizontal' ? 'row' : 'column'
return (
<div className={`split-pane-split ${dir}`} style={{ flex: 1 }}>
<div className="split-pane-child" style={{ flex: node.ratio || 0.5, overflow: 'hidden' }}>
<SplitPaneRenderer
node={node.children?.[0]}
tabs={tabs}
activeTab={activeTab}
setActiveTab={setActiveTab}
showSearch={showSearch}
searchText={searchText}
searchInputRef={searchInputRef}
handleSearchChange={handleSearchChange}
handleSearchNext={handleSearchNext}
handleSearchPrev={handleSearchPrev}
handleCloseSearch={handleCloseSearch}
removePane={removePane}
onLayoutChange={onLayoutChange}
/>
</div>
<div
className="split-pane-resizer"
onMouseDown={(e) => {
e.preventDefault()
const parent = e.target.parentElement
const startX = e.clientX
const startY = e.clientY
const startRatio = node.ratio || 0.5
const isVertical = node.direction === 'vertical'
const parentSize = isVertical ? parent.offsetWidth : parent.offsetHeight
const onMouseMove = (me) => {
const delta = isVertical ? (me.clientX - startX) : (me.clientY - startY)
const newRatio = Math.max(0.15, Math.min(0.85, startRatio + delta / parentSize))
onLayoutChange(prev => updateSplitRatio(prev, node, newRatio))
}
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
document.body.style.cursor = ''
document.body.style.userSelect = ''
}
document.body.style.cursor = isVertical ? 'col-resize' : 'row-resize'
document.body.style.userSelect = 'none'
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}}
/>
<div className="split-pane-child" style={{ flex: 1 - (node.ratio || 0.5), overflow: 'hidden' }}>
<SplitPaneRenderer
node={node.children?.[1]}
tabs={tabs}
activeTab={activeTab}
setActiveTab={setActiveTab}
showSearch={showSearch}
searchText={searchText}
searchInputRef={searchInputRef}
handleSearchChange={handleSearchChange}
handleSearchNext={handleSearchNext}
handleSearchPrev={handleSearchPrev}
handleCloseSearch={handleCloseSearch}
removePane={removePane}
onLayoutChange={onLayoutChange}
/>
</div>
</div>
)
}
return null
}
function assignLeafTab(layout, leaf, tabId) {
if (!layout) return layout
if (layout === leaf) return { ...layout, tabId }
if (layout.children) {
return {
...layout,
children: layout.children.map(c => assignLeafTab(c, leaf, tabId)),
}
}
return layout
}
function updateSplitRatio(layout, targetNode, ratio) {
if (layout === targetNode) {
return { ...layout, ratio }
}
if (layout.children) {
return {
...layout,
children: layout.children.map(c => updateSplitRatio(c, targetNode, ratio)),
}
}
return layout
}