fix: AI terminal init, Shift+Tab nav, code block rendering, command filtering

- Fix AI terminal not initializing (wait for shell col visibility, remove offsetHeight guard)
- Add Shift+Tab to cycle between shell terminals
- Handle unclosed code blocks in renderContent (Shell + Studio)
- Filter irrelevant commands from history (short/non-alpha backend + expanded frontend exclude list)

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-23 23:24:43 +02:00
parent c39203cc4b
commit 20237c022f
4 changed files with 53 additions and 8 deletions

View File

@@ -8,6 +8,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"time" "time"
@@ -553,10 +554,11 @@ func (s *Server) handleRecentCommands(w http.ResponseWriter, r *http.Request) {
shell = "zsh" shell = "zsh"
} }
lines := strings.Split(string(data), "\n") lines := strings.Split(string(data), "\n")
start := len(lines) - 25 start := len(lines) - 50
if start < 0 { if start < 0 {
start = 0 start = 0
} }
for i := len(lines) - 1; i >= start; i-- { for i := len(lines) - 1; i >= start; i-- {
line := strings.TrimSpace(lines[i]) line := strings.TrimSpace(lines[i])
if line == "" || strings.HasPrefix(line, "#") { if line == "" || strings.HasPrefix(line, "#") {
@@ -573,6 +575,15 @@ func (s *Server) handleRecentCommands(w http.ResponseWriter, r *http.Request) {
if line == "" { if line == "" {
continue continue
} }
base := strings.Fields(line)[0]
if len(base) < 2 {
continue
}
if !regexp.MustCompile(`^[a-zA-Z@./]`).MatchString(base) {
continue
}
entries = append(entries, cmdEntry{Cmd: line, Shell: shell}) entries = append(entries, cmdEntry{Cmd: line, Shell: shell})
} }
} }

View File

@@ -93,13 +93,14 @@ export default function Dashboard({ api, refreshRef }) {
const minimax = (quota || []).find(p => p.name === 'minimax') const minimax = (quota || []).find(p => p.name === 'minimax')
const zai = (quota || []).find(p => p.name === 'zai') const zai = (quota || []).find(p => p.name === 'zai')
const EXCLUDE_CMDS = ['ls', 'cd', 'pwd', 'clear', 'exit', 'history'] const EXCLUDE_CMDS = ['ls', 'cd', 'pwd', 'clear', 'exit', 'history', 'cat', 'echo', 'grep', 'export', 'alias', 'unalias', 'set', 'unset', 'source', '.', 'fg', 'bg', 'jobs', 'wait', 'true', 'false', 'yes', 'sleep', 'date', 'whoami', 'id', 'uname', 'hostname', 'uptime', 'df', 'free', 'top', 'htop', 'nano', 'vi', 'vim', 'less', 'more', 'tail', 'head', 'man', 'info', 'which', 'whereis', 'type', 'command', 'hash', 'builtin', 'help']
const topCmds = (() => { const topCmds = (() => {
const counts = {} const counts = {}
for (const c of recentCmds) { for (const c of recentCmds) {
const base = c.cmd.split(/\s+/)[0] const base = c.cmd.split(/\s+/)[0]
if (EXCLUDE_CMDS.includes(base) || !base) continue if (!base || base.length < 2 || EXCLUDE_CMDS.includes(base)) continue
if (!/^[a-zA-Z@.\/]/.test(base)) continue
counts[base] = (counts[base] || 0) + 1 counts[base] = (counts[base] || 0) + 1
} }
return Object.entries(counts) return Object.entries(counts)

View File

@@ -28,7 +28,16 @@ function renderContent(text) {
lastIndex = match.index + full.length lastIndex = match.index + full.length
} }
if (lastIndex < text.length) { if (lastIndex < text.length) {
parts.push({ type: 'text', content: text.slice(lastIndex) }) const remaining = text.slice(lastIndex)
const openBlock = remaining.match(/```(\w*)\n?([\s\S]*)$/)
if (openBlock) {
if (openBlock.index > 0) {
parts.push({ type: 'text', content: remaining.slice(0, openBlock.index) })
}
parts.push({ type: 'code', lang: openBlock[1] || '', content: openBlock[2] || '' })
} else {
parts.push({ type: 'text', content: remaining })
}
} }
return parts return parts
} }
@@ -308,7 +317,7 @@ export default function Shell({ api }) {
if (tabsRef.current[tabId]) return if (tabsRef.current[tabId]) return
const container = document.getElementById(`terminal-${tabId}`) const container = document.getElementById(`terminal-${tabId}`)
if (!container || container.offsetHeight === 0) return if (!container) return
const s = settingsRef.current const s = settingsRef.current
const { term, fitAddon } = createTerminal(container, { const { term, fitAddon } = createTerminal(container, {
@@ -368,7 +377,12 @@ export default function Shell({ api }) {
if (!tab) return if (!tab) return
const tryInit = (attempt) => { const tryInit = (attempt) => {
if (attempt > 10) return if (attempt > 20) return
const shellCol = document.querySelector('.shell-terminal-col')
if (!shellCol || shellCol.offsetParent === null) {
setTimeout(() => tryInit(attempt + 1), 150)
return
}
const container = document.getElementById(`terminal-${tab.id}`) const container = document.getElementById(`terminal-${tab.id}`)
if (!container || container.offsetHeight === 0) { if (!container || container.offsetHeight === 0) {
setTimeout(() => tryInit(attempt + 1), 100) setTimeout(() => tryInit(attempt + 1), 100)
@@ -404,7 +418,17 @@ export default function Shell({ api }) {
useEffect(() => { useEffect(() => {
const onKey = (e) => { const onKey = (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
if (!e.altKey) return if (!e.altKey && !(e.key === 'Tab' && e.shiftKey)) return
if (e.key === 'Tab' && e.shiftKey) {
const shellTab = document.querySelector('.shell-layout')
if (!shellTab || shellTab.closest('.tab-hidden')) return
e.preventDefault()
const idx = tabs.findIndex(t => t.id === activeTab)
const next = (idx + 1) % tabs.length
setActiveTab(tabs[next].id)
return
}
const num = parseInt(e.key) const num = parseInt(e.key)
if (num >= 1 && num <= tabs.length) { if (num >= 1 && num <= tabs.length) {

View File

@@ -47,7 +47,16 @@ function renderContent(text) {
lastIndex = match.index + full.length lastIndex = match.index + full.length
} }
if (lastIndex < text.length) { if (lastIndex < text.length) {
parts.push({ type: 'text', content: text.slice(lastIndex) }) const remaining = text.slice(lastIndex)
const openBlock = remaining.match(/```(\w*)\n?([\s\S]*)$/)
if (openBlock) {
if (openBlock.index > 0) {
parts.push({ type: 'text', content: remaining.slice(0, openBlock.index) })
}
parts.push({ type: 'code', lang: openBlock[1] || '', content: openBlock[2] || '' })
} else {
parts.push({ type: 'text', content: remaining })
}
} }
return parts return parts
} }