Compare commits

...

7 Commits

Author SHA1 Message Date
Augustin
bf8c0fd380 fix(terminal): improve terminal dimensions and fit timing
All checks were successful
Beta Release / beta (push) Successful in 47s
Use min-height:0 on xterm-wrapper (flex child) instead of height:100%
to properly fill available space in flex layout. Add delayed fit()
calls after initialization to let the layout stabilize before
calculating terminal cell dimensions.

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-24 20:35:49 +02:00
Augustin
08dc1fd53b fix(terminal): detect shell tab visibility via MutationObserver
All checks were successful
Beta Release / beta (push) Successful in 49s
Shell is always mounted inside a display:none parent when the app
loads on a different tab. Added MutationObserver on the wrapper to
detect when the shell tab becomes visible and initialize/fit all
pending terminals at that moment. Removed attempt limit so retries
continue until the tab is actually shown.

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-24 20:28:02 +02:00
Augustin
13e937a11b fix(terminal): init all tabs on load, fix excessive zoom
All checks were successful
Beta Release / beta (push) Successful in 46s
Use visibility:hidden instead of display:none for inactive terminal tabs
so xterm containers retain their dimensions. This allows all terminals
to initialize independently and prevents fitAddon from miscalculating
cell sizes on zero-height containers.

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-24 20:13:21 +02:00
Augustin
3cf701b002 fix(terminal): improve tab visibility checks and positioning
All checks were successful
Beta Release / beta (push) Successful in 48s
- Add null check for container before accessing offsetHeight
- Validate activeTabRef during initialization and fit operations
- Check for display:none as visibility indicator
- Simplify useEffect dependency array
- Use absolute positioning for terminal wrapper/instance

💘 Generated with Crush

Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
2026-04-24 17:59:48 +02:00
Augustin
3a09e0e0c2 fix(ui): adjust global CSS styles
All checks were successful
Beta Release / beta (push) Successful in 45s
💘 Generated with Crush

Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
2026-04-24 17:38:21 +02:00
Augustin
47fa2e01bb fix(terminal): use display:none instead of visibility for tab hiding
All checks were successful
Beta Release / beta (push) Successful in 49s
Replace visibility-based hiding with display property for reliable tab
detection. Use offsetParent and offsetHeight checks instead of style
properties to properly detect hidden terminals.

💘 Generated with Crush

Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
2026-04-24 17:23:54 +02:00
Augustin
401292ec5b feat(ui): refactor copy state to Set and add helper functions
All checks were successful
Beta Release / beta (push) Successful in 46s
- Change copiedIdx (number) to copiedSet (Set) for tracking multiple copied items
- Add copyCmd function to handle clipboard and timeout cleanup
- Add relativeTime function for displaying relative timestamps

💘 Generated with Crush

Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
2026-04-24 17:04:38 +02:00
3 changed files with 108 additions and 41 deletions

View File

@@ -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 => {

View File

@@ -399,10 +399,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 +425,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)
@@ -840,11 +888,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>

View File

@@ -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;