Compare commits
10 Commits
v0.3.5-bet
...
v0.3.5-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbf623b98b | ||
|
|
b85ebb8e54 | ||
|
|
7cc206dc20 | ||
|
|
bf8c0fd380 | ||
|
|
08dc1fd53b | ||
|
|
13e937a11b | ||
|
|
3cf701b002 | ||
|
|
3a09e0e0c2 | ||
|
|
47fa2e01bb | ||
|
|
401292ec5b |
@@ -92,6 +92,8 @@ export default function App() {
|
||||
{ keys: `${layout.keys.shift}+${layout.keys.enter}`, desc: t('statusbar.newLine') },
|
||||
],
|
||||
shell: [
|
||||
{ keys: `${layout.keys.ctrl}+${layout.keys.shift}+C`, desc: t('statusbar.copy') },
|
||||
{ keys: `${layout.keys.ctrl}+${layout.keys.shift}+V`, desc: t('statusbar.paste') },
|
||||
{ keys: layout.keys.enter, desc: t('statusbar.runCommand') },
|
||||
{ keys: `${layout.keys.up}/${layout.keys.down}`, desc: t('statusbar.commandHistory') },
|
||||
],
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function Dashboard({ api, refreshRef }) {
|
||||
const [recentCmds, setRecentCmds] = useState([])
|
||||
const [processes, setProcesses] = useState([])
|
||||
const [metrics, setMetrics] = useState(null)
|
||||
const [copiedIdx, setCopiedIdx] = useState(-1)
|
||||
const [copiedSet, setCopiedSet] = useState(new Set())
|
||||
const cpuRef = useRef([])
|
||||
const memRef = useRef([])
|
||||
const netRxRef = useRef([])
|
||||
@@ -109,6 +109,23 @@ export default function Dashboard({ api, refreshRef }) {
|
||||
.map(([cmd, count]) => ({ cmd, count }))
|
||||
})()
|
||||
|
||||
const maxCount = topCmds.length > 0 ? topCmds[0].count : 1
|
||||
|
||||
const copyCmd = (cmd, key) => {
|
||||
navigator.clipboard.writeText(cmd)
|
||||
setCopiedSet(prev => new Set(prev).add(key))
|
||||
setTimeout(() => setCopiedSet(prev => { const next = new Set(prev); next.delete(key); return next }), 1500)
|
||||
}
|
||||
|
||||
const relativeTime = (ts) => {
|
||||
if (!ts) return ''
|
||||
const diff = Math.floor((Date.now() - new Date(ts).getTime()) / 1000)
|
||||
if (diff < 60) return `${diff}s`
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}m`
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h`
|
||||
return `${Math.floor(diff / 86400)}d`
|
||||
}
|
||||
|
||||
const recentUnique = (() => {
|
||||
const seen = new Set()
|
||||
return recentCmds.filter(c => {
|
||||
|
||||
@@ -143,6 +143,32 @@ function createTerminal(container, settings = {}) {
|
||||
const webLinksAddon = new WebLinksAddon()
|
||||
term.loadAddon(fitAddon)
|
||||
term.loadAddon(webLinksAddon)
|
||||
|
||||
term.attachCustomKeyEventHandler((e) => {
|
||||
if (e.type !== 'keydown') return true
|
||||
const ctrl = e.ctrlKey || e.metaKey
|
||||
const shift = e.shiftKey
|
||||
|
||||
if (ctrl && shift && e.key === 'C') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const selection = term.getSelection()
|
||||
if (selection) navigator.clipboard.writeText(selection)
|
||||
return false
|
||||
}
|
||||
|
||||
if (ctrl && shift && e.key === 'V') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
navigator.clipboard.readText().then(text => {
|
||||
if (text) term.paste(text)
|
||||
}).catch(() => {})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
term.open(container)
|
||||
fitAddon.fit()
|
||||
|
||||
@@ -399,10 +425,7 @@ export default function Shell({ api }) {
|
||||
})
|
||||
|
||||
const onResize = () => {
|
||||
const el = document.getElementById(`terminal-${tabId}`)
|
||||
if (el && el.style.visibility !== 'hidden' && el.style.position !== 'absolute') {
|
||||
fitAddon.fit()
|
||||
}
|
||||
fitAddon.fit()
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(onResize)
|
||||
@@ -428,54 +451,105 @@ export default function Shell({ api }) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const tab = tabs.find(t => t.id === activeTab)
|
||||
if (!tab) return
|
||||
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 tryInit = (attempt) => {
|
||||
if (cancelled || attempt > 20) return
|
||||
const tryInitTab = (tab, attempt) => {
|
||||
if (cancelled) return
|
||||
const shellCol = document.querySelector('.shell-terminal-col')
|
||||
if (!shellCol) {
|
||||
pending.push(setTimeout(() => tryInit(attempt + 1), 150))
|
||||
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(() => tryInit(attempt + 1), 100))
|
||||
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)
|
||||
}
|
||||
if (activeTab === tab.id) {
|
||||
requestAnimationFrame(() => {
|
||||
if (cancelled) return
|
||||
const entry = tabsRef.current[tab.id]
|
||||
if (entry) entry.fitAddon.fit()
|
||||
})
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
tryInit(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()
|
||||
}
|
||||
}, [activeTab, tabs, initTerminal])
|
||||
}, [tabs, initTerminal, initPendingTabs])
|
||||
|
||||
useEffect(() => {
|
||||
const entry = tabsRef.current[activeTab]
|
||||
if (entry) {
|
||||
requestAnimationFrame(() => {
|
||||
if (activeTabRef.current === activeTab) {
|
||||
entry.fitAddon.fit()
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [activeTab])
|
||||
|
||||
useEffect(() => {
|
||||
const iv = setInterval(() => {
|
||||
for (const tab of tabs) {
|
||||
const entry = tabsRef.current[tab.id]
|
||||
if (entry) {
|
||||
const el = document.getElementById(`terminal-${tab.id}`)
|
||||
if (el && el.style.visibility !== 'hidden') {
|
||||
entry.fitAddon.fit()
|
||||
}
|
||||
}
|
||||
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)
|
||||
@@ -645,7 +719,7 @@ export default function Shell({ api }) {
|
||||
|
||||
if (!fromEvent) {
|
||||
setAiInput('')
|
||||
focusAiTerminal()
|
||||
setTimeout(() => focusAiTerminal(), 0)
|
||||
}
|
||||
|
||||
if (trimmed === '/clear') {
|
||||
@@ -840,11 +914,7 @@ export default function Shell({ api }) {
|
||||
<div
|
||||
key={tab.id}
|
||||
id={`terminal-${tab.id}`}
|
||||
className="shell-xterm-instance"
|
||||
style={activeTab === tab.id
|
||||
? { visibility: 'visible', pointerEvents: 'auto' }
|
||||
: { visibility: 'hidden', pointerEvents: 'none' }
|
||||
}
|
||||
className={`shell-xterm-instance${activeTab === tab.id ? ' active' : ''}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -893,7 +963,7 @@ export default function Shell({ api }) {
|
||||
<input
|
||||
value={aiInput}
|
||||
onChange={e => setAiInput(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleAiSend()}
|
||||
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); handleAiSend() } }}
|
||||
placeholder={aiAtLimit ? '/clear pour continuer' : t('shell.askAi')}
|
||||
disabled={aiAtLimit && aiInput !== '/clear'}
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,8 @@ const en = {
|
||||
switchWindow: 'Switch window',
|
||||
sendMessage: 'Send message',
|
||||
newLine: 'New line',
|
||||
copy: 'Copy',
|
||||
paste: 'Paste',
|
||||
runCommand: 'Run command',
|
||||
commandHistory: 'Command history',
|
||||
},
|
||||
|
||||
@@ -16,6 +16,8 @@ const fr = {
|
||||
switchWindow: 'Changer de fen\u00eatre',
|
||||
sendMessage: 'Envoyer le message',
|
||||
newLine: 'Nouvelle ligne',
|
||||
copy: 'Copier',
|
||||
paste: 'Coller',
|
||||
runCommand: 'Ex\u00e9cuter',
|
||||
commandHistory: 'Historique',
|
||||
},
|
||||
|
||||
@@ -155,7 +155,7 @@ input::placeholder { color: var(--text-disabled); }
|
||||
.header-clock { font-family: var(--font-mono); font-size: 12px; color: var(--accent); font-weight: 600; }
|
||||
|
||||
.content { flex: 1; overflow: hidden; position: relative; }
|
||||
.content > div { height: 100%; }
|
||||
.content > div { position: absolute; inset: 0; overflow: hidden; }
|
||||
.tab-hidden { display: none; }
|
||||
|
||||
.statusbar {
|
||||
@@ -276,8 +276,8 @@ input::placeholder { color: var(--text-disabled); }
|
||||
.sidebar-tab:hover { background: var(--bg-card); color: var(--text-primary); }
|
||||
.sidebar-tab.active { background: var(--accent); color: #fff; font-weight: 600; }
|
||||
|
||||
.shell-layout { display: flex; height: 100%; }
|
||||
.shell-terminal-col { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; }
|
||||
.shell-layout { display: flex; height: 100%; overflow: hidden; }
|
||||
.shell-terminal-col { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; }
|
||||
|
||||
.shell-tabs-bar {
|
||||
display: flex; align-items: center; background: var(--bg-surface);
|
||||
@@ -382,12 +382,18 @@ input::placeholder { color: var(--text-disabled); }
|
||||
}
|
||||
.shell-menu-divider { height: 1px; background: var(--border); margin: 4px 6px; }
|
||||
|
||||
.shell-xterm-wrapper { flex: 1; background: var(--bg); overflow: hidden; position: relative; }
|
||||
.shell-xterm-wrapper { flex: 1; min-height: 0; background: var(--bg); overflow: hidden; position: relative; }
|
||||
.shell-xterm-instance {
|
||||
position: absolute; inset: 0; padding: 4px;
|
||||
display: block !important;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.shell-xterm-instance .xterm { height: 100%; padding: 4px; }
|
||||
.shell-xterm-instance.active {
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.shell-xterm-instance .xterm { height: 100%; }
|
||||
|
||||
.connection-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
|
||||
.connection-dot.on { background: var(--success); box-shadow: 0 0 6px var(--success); }
|
||||
@@ -396,7 +402,7 @@ input::placeholder { color: var(--text-disabled); }
|
||||
.shell-tab.ai-tab .shell-tab-name { color: var(--accent); }
|
||||
.shell-tab.ai-tab { border-bottom-color: var(--accent); }
|
||||
|
||||
.shell-ai-col { width: 320px; border-left: 1px solid var(--border); background: var(--bg-surface); display: flex; flex-direction: column; flex-shrink: 0; }
|
||||
.shell-ai-col { width: 320px; max-width: 320px; border-left: 1px solid var(--border); background: var(--bg-surface); display: flex; flex-direction: column; flex-shrink: 0; overflow: hidden; }
|
||||
.ai-panel-header { padding: 12px 16px; border-bottom: 1px solid var(--border); font-weight: 700; font-size: 13px; color: var(--accent); display: flex; align-items: center; justify-content: space-between; }
|
||||
.shell-analyze-btn {
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
|
||||
Reference in New Issue
Block a user