refactor: 工作空间去掉手动命令输入,终止时自动反馈 AI
- 移除工作空间终端的命令输入框和执行按钮 - 用户只能通过终止按钮中断卡住的 AI 命令 - 终止后自动通过 killTool IPC 终止进程,结果自动回传 AI - 新增 workspace:killTool IPC 和 terminateCurrentToolCommand - 新增终端底部状态提示(等待/执行中) - 修复工作空间面板顶部对齐(90px → 92px)
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user