refactor: 工作空间改为单一终端,移除多 Tab 机制
- 移除新建终端按钮和 Tab 栏,只保留唯一默认终端 - AI 命令始终发往唯一终端,避免多终端时目标不确定 - 删除 createTerminalSession/closeTerminalTab/renderTerminalTabs 等多 Tab 相关代码
This commit is contained in:
@@ -31,8 +31,7 @@ interface FileNode {
|
|||||||
|
|
||||||
let panelVisible = true; // 常驻显示,不可关闭
|
let panelVisible = true; // 常驻显示,不可关闭
|
||||||
let activeTab: 'terminal' | 'files' = 'terminal';
|
let activeTab: 'terminal' | 'files' = 'terminal';
|
||||||
let terminalSessions: TerminalSession[] = [];
|
let terminal: TerminalSession | null = null; // 唯一终端
|
||||||
let activeTerminalId = '';
|
|
||||||
let workspaceDir = '';
|
let workspaceDir = '';
|
||||||
let currentFileDir = '';
|
let currentFileDir = '';
|
||||||
let fileTree: FileNode[] = [];
|
let fileTree: FileNode[] = [];
|
||||||
@@ -196,8 +195,14 @@ export async function initWorkspacePanel(): Promise<void> {
|
|||||||
// 绑定 UI 事件
|
// 绑定 UI 事件
|
||||||
bindEvents();
|
bindEvents();
|
||||||
|
|
||||||
// 创建默认终端
|
// 创建唯一终端
|
||||||
createTerminalSession('终端 1');
|
terminal = {
|
||||||
|
id: genId(),
|
||||||
|
title: '终端',
|
||||||
|
lines: [],
|
||||||
|
running: false,
|
||||||
|
autoScroll: true
|
||||||
|
};
|
||||||
|
|
||||||
logInfo('工作空间面板已初始化');
|
logInfo('工作空间面板已初始化');
|
||||||
}
|
}
|
||||||
@@ -213,12 +218,6 @@ function bindEvents(): void {
|
|||||||
// 清空按钮
|
// 清空按钮
|
||||||
document.querySelector('#wsClearBtn')?.addEventListener('click', clearTerminal);
|
document.querySelector('#wsClearBtn')?.addEventListener('click', clearTerminal);
|
||||||
|
|
||||||
// 新建终端 Tab
|
|
||||||
document.querySelector('#wsNewTabBtn')?.addEventListener('click', () => {
|
|
||||||
const num = terminalSessions.length + 1;
|
|
||||||
createTerminalSession(`终端 ${num}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 文件刷新
|
// 文件刷新
|
||||||
document.querySelector('#wsFileRefreshBtn')?.addEventListener('click', () => {
|
document.querySelector('#wsFileRefreshBtn')?.addEventListener('click', () => {
|
||||||
loadFiles(currentFileDir || workspaceDir);
|
loadFiles(currentFileDir || workspaceDir);
|
||||||
@@ -250,22 +249,8 @@ function switchTab(tab: 'terminal' | 'files'): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── 终端管理 ──
|
// ── 终端管理 ──
|
||||||
function createTerminalSession(title: string): string {
|
|
||||||
const session: TerminalSession = {
|
|
||||||
id: genId(),
|
|
||||||
title,
|
|
||||||
lines: [],
|
|
||||||
running: false,
|
|
||||||
autoScroll: true
|
|
||||||
};
|
|
||||||
terminalSessions.push(session);
|
|
||||||
activeTerminalId = session.id;
|
|
||||||
renderPanel();
|
|
||||||
return session.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActiveSession(): TerminalSession | undefined {
|
function getActiveSession(): TerminalSession | undefined {
|
||||||
return terminalSessions.find(s => s.id === activeTerminalId);
|
return terminal ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** AI Tool Calling 实时输出 → 流式追加到活跃终端 */
|
/** AI Tool Calling 实时输出 → 流式追加到活跃终端 */
|
||||||
@@ -318,11 +303,11 @@ function handleToolDone(command: string, exitCode: number | null): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function appendTerminalOutput(sessionId: string, type: 'stdout' | 'stderr', data: string): void {
|
function appendTerminalOutput(sessionId: string, type: 'stdout' | 'stderr', data: string): void {
|
||||||
const session = terminalSessions.find(s => s.id === sessionId);
|
if (!terminal || terminal.id !== sessionId) {
|
||||||
if (!session) {
|
logDebug('工作空间输出丢失', `未匹配终端: ${sessionId}`);
|
||||||
logDebug('工作空间输出丢失', `未找到会话: ${sessionId}`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const session = terminal;
|
||||||
|
|
||||||
// 按行分割,处理 \r 进度更新
|
// 按行分割,处理 \r 进度更新
|
||||||
const lines = data.split('\n');
|
const lines = data.split('\n');
|
||||||
@@ -354,11 +339,11 @@ function appendTerminalOutput(sessionId: string, type: 'stdout' | 'stderr', data
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleProcessExit(sessionId: string, code: number | null): void {
|
function handleProcessExit(sessionId: string, code: number | null): void {
|
||||||
const session = terminalSessions.find(s => s.id === sessionId);
|
if (!terminal || terminal.id !== sessionId) {
|
||||||
if (!session) {
|
logDebug('工作空间退出信号丢失', `未匹配终端: ${sessionId}`);
|
||||||
logDebug('工作空间退出信号丢失', `未找到会话: ${sessionId}`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const session = terminal;
|
||||||
|
|
||||||
session.running = false;
|
session.running = false;
|
||||||
const exitMsg = code === 0 ? '✓ 进程正常退出' : `✗ 进程退出 (code: ${code})`;
|
const exitMsg = code === 0 ? '✓ 进程正常退出' : `✗ 进程退出 (code: ${code})`;
|
||||||
@@ -408,21 +393,6 @@ function clearTerminal(): void {
|
|||||||
renderTerminal();
|
renderTerminal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeTerminalTab(sessionId: string): void {
|
|
||||||
if (terminalSessions.length <= 1) return; // 至少保留一个
|
|
||||||
|
|
||||||
const session = terminalSessions.find(s => s.id === sessionId);
|
|
||||||
if (session?.running) {
|
|
||||||
window.metonaDesktop?.workspace.kill(sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
terminalSessions = terminalSessions.filter(s => s.id !== sessionId);
|
|
||||||
if (activeTerminalId === sessionId) {
|
|
||||||
activeTerminalId = terminalSessions[0].id;
|
|
||||||
}
|
|
||||||
renderPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 文件浏览器 ──
|
// ── 文件浏览器 ──
|
||||||
async function loadFiles(dirPath: string): Promise<void> {
|
async function loadFiles(dirPath: string): Promise<void> {
|
||||||
const bridge = window.metonaDesktop;
|
const bridge = window.metonaDesktop;
|
||||||
@@ -489,7 +459,6 @@ export function renderPanel(): void {
|
|||||||
|
|
||||||
if (activeTab === 'terminal') {
|
if (activeTab === 'terminal') {
|
||||||
renderTerminal();
|
renderTerminal();
|
||||||
renderTerminalTabs();
|
|
||||||
} else {
|
} else {
|
||||||
renderFileList();
|
renderFileList();
|
||||||
}
|
}
|
||||||
@@ -532,41 +501,6 @@ function renderTerminal(): void {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTerminalTabs(): void {
|
|
||||||
const tabBar = document.querySelector('#wsTermTabBar') as HTMLElement;
|
|
||||||
if (!tabBar) return;
|
|
||||||
|
|
||||||
tabBar.innerHTML = terminalSessions.map(s => {
|
|
||||||
const active = s.id === activeTerminalId;
|
|
||||||
const running = s.running ? ' ●' : '';
|
|
||||||
const closeBtn = terminalSessions.length > 1
|
|
||||||
? `<button class="ws-tab-close" data-close="${s.id}" title="关闭">✕</button>`
|
|
||||||
: '';
|
|
||||||
return `<div class="ws-tab ${active ? 'active' : ''}" data-session="${s.id}">
|
|
||||||
<span class="ws-tab-title">${escapeHtml(s.title)}${running}</span>
|
|
||||||
${closeBtn}
|
|
||||||
</div>`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
// 绑定 tab 点击事件
|
|
||||||
tabBar.querySelectorAll('.ws-tab').forEach(tab => {
|
|
||||||
tab.addEventListener('click', (e) => {
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
const closeBtn = target.closest('.ws-tab-close') as HTMLElement;
|
|
||||||
if (closeBtn) {
|
|
||||||
e.stopPropagation();
|
|
||||||
closeTerminalTab(closeBtn.dataset.close!);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const sessionId = (tab as HTMLElement).dataset.session;
|
|
||||||
if (sessionId) {
|
|
||||||
activeTerminalId = sessionId;
|
|
||||||
renderPanel();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderFileList(): void {
|
function renderFileList(): void {
|
||||||
const container = document.querySelector('#wsFileList') as HTMLElement;
|
const container = document.querySelector('#wsFileList') as HTMLElement;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|||||||
@@ -179,7 +179,6 @@
|
|||||||
|
|
||||||
<!-- 终端 Tab -->
|
<!-- 终端 Tab -->
|
||||||
<div class="ws-content" id="wsTerminalContent">
|
<div class="ws-content" id="wsTerminalContent">
|
||||||
<div class="ws-term-tab-bar" id="wsTermTabBar"></div>
|
|
||||||
<div class="ws-term-output" id="wsTermOutput"></div>
|
<div class="ws-term-output" id="wsTermOutput"></div>
|
||||||
<div class="ws-term-toolbar">
|
<div class="ws-term-toolbar">
|
||||||
<span class="ws-term-hint" id="wsTermHint">等待 AI 执行命令...</span>
|
<span class="ws-term-hint" id="wsTermHint">等待 AI 执行命令...</span>
|
||||||
@@ -195,11 +194,6 @@
|
|||||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="ws-toolbar-btn" id="wsNewTabBtn" title="新建终端">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user