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 { 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
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user