From 24b31b0b47db526bb134ba2905b6c9802d42e7e8 Mon Sep 17 00:00:00 2001 From: Augustin Date: Thu, 23 Apr 2026 23:24:43 +0200 Subject: [PATCH] fix: AI terminal init, Shift+Tab nav, code block rendering, command filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- internal/api/handlers_info.go | 13 ++++++++++++- web/src/components/Dashboard.jsx | 5 +++-- web/src/components/Shell.jsx | 32 ++++++++++++++++++++++++++++---- web/src/components/Studio.jsx | 11 ++++++++++- 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/internal/api/handlers_info.go b/internal/api/handlers_info.go index f031df0..46cf8e3 100644 --- a/internal/api/handlers_info.go +++ b/internal/api/handlers_info.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "strings" "time" @@ -553,10 +554,11 @@ func (s *Server) handleRecentCommands(w http.ResponseWriter, r *http.Request) { shell = "zsh" } lines := strings.Split(string(data), "\n") - start := len(lines) - 25 + start := len(lines) - 50 if start < 0 { start = 0 } + for i := len(lines) - 1; i >= start; i-- { line := strings.TrimSpace(lines[i]) if line == "" || strings.HasPrefix(line, "#") { @@ -573,6 +575,15 @@ func (s *Server) handleRecentCommands(w http.ResponseWriter, r *http.Request) { if line == "" { 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}) } } diff --git a/web/src/components/Dashboard.jsx b/web/src/components/Dashboard.jsx index e74080b..cece2be 100644 --- a/web/src/components/Dashboard.jsx +++ b/web/src/components/Dashboard.jsx @@ -93,13 +93,14 @@ export default function Dashboard({ api, refreshRef }) { const minimax = (quota || []).find(p => p.name === 'minimax') 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 counts = {} for (const c of recentCmds) { 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 } return Object.entries(counts) diff --git a/web/src/components/Shell.jsx b/web/src/components/Shell.jsx index dd8a6f5..2a695d2 100644 --- a/web/src/components/Shell.jsx +++ b/web/src/components/Shell.jsx @@ -28,7 +28,16 @@ function renderContent(text) { lastIndex = match.index + full.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 } @@ -308,7 +317,7 @@ export default function Shell({ api }) { if (tabsRef.current[tabId]) return const container = document.getElementById(`terminal-${tabId}`) - if (!container || container.offsetHeight === 0) return + if (!container) return const s = settingsRef.current const { term, fitAddon } = createTerminal(container, { @@ -368,7 +377,12 @@ export default function Shell({ api }) { if (!tab) return 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}`) if (!container || container.offsetHeight === 0) { setTimeout(() => tryInit(attempt + 1), 100) @@ -404,7 +418,17 @@ export default function Shell({ api }) { useEffect(() => { const onKey = (e) => { 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) if (num >= 1 && num <= tabs.length) { diff --git a/web/src/components/Studio.jsx b/web/src/components/Studio.jsx index 11d48fc..915ffda 100644 --- a/web/src/components/Studio.jsx +++ b/web/src/components/Studio.jsx @@ -47,7 +47,16 @@ function renderContent(text) { lastIndex = match.index + full.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 }