feat: add agent panel, file panel, context menu, fix agy cursor, and bump to v1.1.0
Build and Release / Build & Release DEB Package (push) Successful in 3m27s

- Agent panel: dedicated panel per tab with compact icon-only agent tabs (agy with Google colors, codex with ChatGPT icon, claude)
- Agent lifecycle: auto-close tab and panel when agent process exits without leaving a shell
- Fix cursor jumping: disable convertEol and ensure precise viewport size calculation
- File browser panel: single instance per tab, directory navigation, hidden files toggle
- Context menu: terminal right-click menu with split options, agent launcher, and close pane
- Terminal shortcuts: Ctrl+Shift+C / Ctrl+Shift+V copy and paste support
- Test suite: 35 automated end-to-end tests covering all new features
This commit is contained in:
2026-09-25 01:45:51 +08:00
parent 2db388c2f2
commit 3589e5c927
17 changed files with 3215 additions and 29 deletions
+93 -4
View File
@@ -3,6 +3,7 @@ import { TabManager } from './tab-manager.js';
import { WorkspaceManager } from './workspace-manager.js';
import { CommandPalette } from './command-palette.js';
import { SettingsModal } from './settings.js';
import { ContextMenu } from './context-menu.js';
class TerminalApp {
constructor() {
@@ -10,6 +11,7 @@ class TerminalApp {
this.initTheme();
this.initTabManager();
this.initWorkspaceManager();
this.initContextMenu();
this.initModals();
this.initSearch();
this.initWindowControls();
@@ -59,6 +61,10 @@ class TerminalApp {
if (activeTerm && this.statusShell) {
this.statusShell.textContent = activeTerm.title || 'bash';
}
this.updateSplitButtonsState(activeTerm);
},
onContextMenu: (e, term) => {
this.contextMenu?.show(e, term);
},
});
@@ -75,12 +81,43 @@ class TerminalApp {
});
}
updateSplitButtonsState(activeTerm) {
const isAgent = !!(
activeTerm?.isAgent ||
activeTerm?.element?.closest?.('.agent-panel') ||
activeTerm?.terminalWrapper?.closest?.('.agent-panel')
);
const btnH = document.getElementById('btn-split-h');
const btnV = document.getElementById('btn-split-v');
if (btnH) {
btnH.disabled = isAgent;
btnH.classList.toggle('disabled', isAgent);
btnH.title = isAgent ? 'Agent 面板不可分屏' : '水平分屏 (Ctrl+Shift+E)';
}
if (btnV) {
btnV.disabled = isAgent;
btnV.classList.toggle('disabled', isAgent);
btnV.title = isAgent ? 'Agent 面板不可分屏' : '垂直分屏 (Ctrl+Shift+O)';
}
}
initWorkspaceManager() {
this.workspaceManager = new WorkspaceManager({
tabManager: this.tabManager,
});
}
initContextMenu() {
this.contextMenu = new ContextMenu({
tabManager: this.tabManager,
workspaceManager: this.workspaceManager,
});
window.addEventListener('terminal-contextmenu', (e) => {
this.contextMenu?.show(e.detail.event, e.detail.termView);
});
}
initModals() {
this.commandPalette = new CommandPalette({
overlay: this.commandPaletteOverlay,
@@ -109,12 +146,20 @@ class TerminalApp {
document.getElementById('btn-split-h')?.addEventListener('click', () => {
const tab = this.tabManager.getActiveTab();
if (tab) tab.splitTree.splitCurrent('horizontal');
if (tab && !tab.splitTree.activeTerminal?.isAgent) {
tab.splitTree.splitCurrent('horizontal');
}
});
document.getElementById('btn-split-v')?.addEventListener('click', () => {
const tab = this.tabManager.getActiveTab();
if (tab) tab.splitTree.splitCurrent('vertical');
if (tab && !tab.splitTree.activeTerminal?.isAgent) {
tab.splitTree.splitCurrent('vertical');
}
});
document.getElementById('btn-file-browser')?.addEventListener('click', () => {
this.toggleFileBrowser();
});
document.getElementById('btn-search')?.addEventListener('click', () => {
@@ -176,6 +221,12 @@ class TerminalApp {
}
}
toggleFileBrowser(initialDir = null) {
const tab = this.tabManager?.getActiveTab();
if (!tab || !tab.splitTree) return null;
return tab.splitTree.toggleFilePanel(initialDir);
}
initWindowControls() {
document.getElementById('win-min')?.addEventListener('click', () => {
window.terminalAPI.minimize();
@@ -241,6 +292,32 @@ class TerminalApp {
return;
}
const isEditingText = (el) => {
if (!el) return false;
const tag = el.tagName?.toLowerCase();
return (tag === 'input' || tag === 'textarea') && !el.classList.contains('xterm-helper-textarea');
};
// Ctrl+Shift+C: 复制当前选中终端文本
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'c') {
if (isEditingText(e.target)) return;
e.preventDefault();
e.stopPropagation();
const activeTerm = this.tabManager.getActiveTerminal();
if (activeTerm) activeTerm.copySelection();
return;
}
// Ctrl+Shift+V: 粘贴剪贴板文本到当前终端
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'v') {
if (isEditingText(e.target)) return;
e.preventDefault();
e.stopPropagation();
const activeTerm = this.tabManager.getActiveTerminal();
if (activeTerm) activeTerm.pasteFromClipboard();
return;
}
// Ctrl+Shift+T: 新建当前工作区标签页
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 't') {
e.preventDefault();
@@ -273,7 +350,9 @@ class TerminalApp {
e.preventDefault();
e.stopPropagation();
const tab = this.tabManager.getActiveTab();
if (tab) tab.splitTree.splitCurrent('horizontal');
if (tab && !tab.splitTree.activeTerminal?.isAgent) {
tab.splitTree.splitCurrent('horizontal');
}
return;
}
@@ -282,7 +361,9 @@ class TerminalApp {
e.preventDefault();
e.stopPropagation();
const tab = this.tabManager.getActiveTab();
if (tab) tab.splitTree.splitCurrent('vertical');
if (tab && !tab.splitTree.activeTerminal?.isAgent) {
tab.splitTree.splitCurrent('vertical');
}
return;
}
@@ -294,6 +375,14 @@ class TerminalApp {
return;
}
// Ctrl+Shift+U 或 Alt+F: 切换文件浏览面板
if ((e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'u') || (e.altKey && !e.ctrlKey && !e.shiftKey && e.key.toLowerCase() === 'f')) {
e.preventDefault();
e.stopPropagation();
this.toggleFileBrowser();
return;
}
// Ctrl+,: 设置
if (e.ctrlKey && e.key === ',') {
e.preventDefault();