refactor: 工作空间去掉手动命令输入,终止时自动反馈 AI

- 移除工作空间终端的命令输入框和执行按钮
- 用户只能通过终止按钮中断卡住的 AI 命令
- 终止后自动通过 killTool IPC 终止进程,结果自动回传 AI
- 新增 workspace:killTool IPC 和 terminateCurrentToolCommand
- 新增终端底部状态提示(等待/执行中)
- 修复工作空间面板顶部对齐(90px → 92px)
This commit is contained in:
thzxx
2026-04-08 12:57:48 +08:00
parent d5f4112d97
commit d150b53116
7 changed files with 130 additions and 110 deletions
+56 -69
View File
@@ -39,6 +39,17 @@ let fileTree: FileNode[] = [];
let previewFile: { name: string; content: string; path: string } | null = null;
let _counter = 0;
/** 当前正在运行的 AI 命令(用于用户手动终止时通知 AI) */
let currentAiCommand: string | null = null;
/** 终止通知回调(由外部设置) */
let onToolTerminated: ((command: string) => void) | null = null;
/** 设置终止通知回调 */
export function setOnToolTerminated(cb: (command: string) => void): void {
onToolTerminated = cb;
}
function genId(): string {
return `ws_${Date.now()}_${++_counter}`;
}
@@ -191,28 +202,6 @@ function bindEvents(): void {
document.querySelector('#wsTabTerminal')?.addEventListener('click', () => switchTab('terminal'));
document.querySelector('#wsTabFiles')?.addEventListener('click', () => switchTab('files'));
// 终端输入
const termInput = document.querySelector('#wsTermInput') as HTMLInputElement;
termInput?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const cmd = termInput.value.trim();
if (cmd) {
executeCommand(cmd);
termInput.value = '';
}
}
});
// 执行按钮
document.querySelector('#wsExecBtn')?.addEventListener('click', () => {
const cmd = termInput?.value.trim();
if (cmd) {
executeCommand(cmd);
termInput.value = '';
}
});
// 停止按钮
document.querySelector('#wsStopBtn')?.addEventListener('click', killCurrentProcess);
@@ -274,36 +263,16 @@ function getActiveSession(): TerminalSession | undefined {
return terminalSessions.find(s => s.id === activeTerminalId);
}
function executeCommand(command: string): void {
const session = getActiveSession();
if (!session) {
logError('工作空间执行失败', '无活跃终端会话');
return;
}
const bridge = window.metonaDesktop;
if (!bridge?.isDesktop) {
logError('工作空间执行失败', '非桌面环境');
return;
}
// 显示命令
session.lines.push({ type: 'system', text: `$ ${command}` });
session.running = true;
session.autoScroll = true;
renderTerminal();
const cwd = currentFileDir || workspaceDir;
logInfo('工作空间执行命令', `ID: ${session.id} | ${command.slice(0, 100)} | cwd: ${cwd}`);
bridge.workspace.exec({ id: session.id, command, cwd });
}
/** AI Tool Calling 命令输出 → 追加到活跃终端 */
function appendToolOutput(command: string, stdout: string, stderr: string, exitCode: number | null): void {
const session = getActiveSession();
if (!session) return;
// 记录当前 AI 命令
currentAiCommand = command;
session.running = true;
updateHint();
// 显示执行的命令
session.lines.push({ type: 'system', text: `$ ${command} (AI)` });
@@ -325,12 +294,18 @@ function appendToolOutput(command: string, stdout: string, stderr: string, exitC
const exitMsg = exitCode === 0 ? '✓ AI 命令执行成功' : `✗ AI 命令退出 (code: ${exitCode})`;
session.lines.push({ type: 'system', text: exitMsg });
// 命令结束
session.running = false;
currentAiCommand = null;
// 限制最大行数
if (session.lines.length > 3000) {
session.lines = session.lines.slice(-2000);
}
renderTerminal();
updateStopBtnState();
updateHint();
}
function appendTerminalOutput(sessionId: string, type: 'stdout' | 'stderr', data: string): void {
@@ -389,14 +364,32 @@ function handleProcessExit(sessionId: string, code: number | null): void {
function killCurrentProcess(): void {
const session = getActiveSession();
if (!session || !session.running) return;
if (!session) return;
const bridge = window.metonaDesktop;
if (!bridge?.isDesktop) return;
bridge.workspace.kill(session.id);
session.lines.push({ type: 'system', text: '■ 已发送终止信号' });
// 终止用户终端进程(如果有)
if (session.running) {
bridge.workspace.kill(session.id);
}
// 终止当前 AI 工具命令
bridge.workspace.killTool();
const cmd = currentAiCommand;
session.lines.push({ type: 'system', text: '■ 命令已手动终止' });
session.running = false;
currentAiCommand = null;
renderTerminal();
updateStopBtnState();
updateHint();
// 通知 AI 命令被用户终止
if (cmd && onToolTerminated) {
onToolTerminated(cmd);
}
}
function clearTerminal(): void {
@@ -649,12 +642,23 @@ function renderPreview(): void {
function updateStopBtnState(): void {
const stopBtn = document.querySelector('#wsStopBtn') as HTMLButtonElement;
if (!stopBtn) return;
const session = getActiveSession();
const running = session?.running || false;
const running = currentAiCommand !== null;
stopBtn.disabled = !running;
stopBtn.classList.toggle('active', running);
}
function updateHint(): void {
const hint = document.querySelector('#wsTermHint') as HTMLElement;
if (!hint) return;
if (currentAiCommand) {
hint.textContent = `⏳ AI 正在执行: ${currentAiCommand.slice(0, 40)}${currentAiCommand.length > 40 ? '...' : ''}`;
hint.classList.add('running');
} else {
hint.textContent = '等待 AI 执行命令...';
hint.classList.remove('running');
}
}
// ── 工具函数 ──
function getFileIcon(name: string): string {
const ext = name.split('.').pop()?.toLowerCase() || '';
@@ -676,23 +680,6 @@ function formatSize(bytes: number): string {
// ── 外部调用接口 ──
/** 从聊天区域调用:在工作空间执行命令 */
export function execInWorkspace(command: string): void {
// 切换到终端 tab
activeTab = 'terminal';
// 使用当前活跃终端或创建新的
const session = getActiveSession();
if (session?.running) {
// 当前终端有进程在跑,新建一个
const num = terminalSessions.length + 1;
createTerminalSession(`终端 ${num}`);
}
renderPanel();
executeCommand(command);
}
/** 获取工作空间目录 */
export function getWorkspaceDirPath(): string {
return workspaceDir;