fix(ui): restore Shell.jsx to v0.9.0-beta.1 state
All checks were successful
Stable Release / stable (push) Successful in 1m38s
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:
@@ -6,21 +6,18 @@ import { WebglAddon } from '@xterm/addon-webgl'
|
||||
import { SearchAddon } from '@xterm/addon-search'
|
||||
import { Unicode11Addon } from '@xterm/addon-unicode11'
|
||||
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 { useI18n } from '../i18n'
|
||||
import mermaid from 'mermaid'
|
||||
import FileEditor from './FileEditor'
|
||||
|
||||
mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose', fontFamily: 'var(--font-mono)' })
|
||||
|
||||
const AI_TAB_ID = 0
|
||||
const MAX_TABS = 7
|
||||
const MAX_PANES = 4
|
||||
const SHELL_MAX_TOKENS = 100000
|
||||
const SHELL_AI_COMMANDS = ['/clear', '/help', '/model', '/model change']
|
||||
const TABS_STORAGE_KEY = 'muyue_shell_tabs'
|
||||
const LAYOUT_STORAGE_KEY = 'muyue_shell_layout'
|
||||
const TERMINAL_BUFFER_KEY = 'muyue_terminal_buffers'
|
||||
|
||||
function renderContent(text) {
|
||||
@@ -480,107 +477,6 @@ export default function Shell({ api, isSudo }) {
|
||||
const _streamRafRef = 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(() => {
|
||||
_streamRafRef.current = null
|
||||
const pending = _streamPendingRef.current
|
||||
@@ -922,22 +818,6 @@ export default function Shell({ api, isSudo }) {
|
||||
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.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
|
||||
</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 && (
|
||||
<div className="shell-new-tab-wrapper">
|
||||
<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 className={`shell-xterm-wrapper ${splitLayout ? 'has-splits' : ''}`}>
|
||||
{editingFile && (
|
||||
<FileEditor api={api} filePath={editingFile} onClose={() => setEditingFile(null)} />
|
||||
)}
|
||||
{!editingFile && (
|
||||
splitLayout ? (
|
||||
<SplitPaneRenderer
|
||||
node={splitLayout}
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
showSearch={showSearch}
|
||||
searchText={searchText}
|
||||
searchInputRef={searchInputRef}
|
||||
handleSearchChange={handleSearchChange}
|
||||
handleSearchNext={handleSearchNext}
|
||||
handleSearchPrev={handleSearchPrev}
|
||||
handleCloseSearch={handleCloseSearch}
|
||||
removePane={removePane}
|
||||
onLayoutChange={setSplitLayout}
|
||||
<div className="shell-xterm-wrapper">
|
||||
{showSearch && (
|
||||
<div className="shell-search-bar">
|
||||
<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..."
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{showSearch && (
|
||||
<div className="shell-search-bar">
|
||||
<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' : ''}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
<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' : ''}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1941,158 +1775,3 @@ const ShellAIMessage = memo(function ShellAIMessage({ msg, sendToTerminal, termi
|
||||
</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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user