refactor: 工作空间去掉手动命令输入,终止时自动反馈 AI
- 移除工作空间终端的命令输入框和执行按钮 - 用户只能通过终止按钮中断卡住的 AI 命令 - 终止后自动通过 killTool IPC 终止进程,结果自动回传 AI - 新增 workspace:killTool IPC 和 terminateCurrentToolCommand - 新增终端底部状态提示(等待/执行中) - 修复工作空间面板顶部对齐(90px → 92px)
This commit is contained in:
+8
-1
@@ -22,7 +22,7 @@ import {
|
|||||||
handleRunCommand
|
handleRunCommand
|
||||||
} from './tool-handlers.js';
|
} from './tool-handlers.js';
|
||||||
import { getAllowedDirs, getBlockedDirs, setAllowedDirs } from './tool-security.js';
|
import { getAllowedDirs, getBlockedDirs, setAllowedDirs } from './tool-security.js';
|
||||||
import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir, execWorkspaceCommand } from './workspace.js';
|
import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir, execWorkspaceCommand, terminateCurrentToolCommand } from './workspace.js';
|
||||||
|
|
||||||
export function setupIPC(): void {
|
export function setupIPC(): void {
|
||||||
ipcMain.handle('dialog:openFile', async (_, options?: { filters?: Array<{ name: string; extensions: string[] }> }) => {
|
ipcMain.handle('dialog:openFile', async (_, options?: { filters?: Array<{ name: string; extensions: string[] }> }) => {
|
||||||
@@ -177,4 +177,11 @@ export function setupIPC(): void {
|
|||||||
sendLog('info', `🔧 workspace:execTool`, `${command.slice(0, 200)} | cwd: ${cwd || '默认'}`);
|
sendLog('info', `🔧 workspace:execTool`, `${command.slice(0, 200)} | cwd: ${cwd || '默认'}`);
|
||||||
return execWorkspaceCommand(command, cwd);
|
return execWorkspaceCommand(command, cwd);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 终止当前 AI 工具命令
|
||||||
|
ipcMain.handle('workspace:killTool', async () => {
|
||||||
|
const killed = terminateCurrentToolCommand();
|
||||||
|
sendLog('info', `🔧 workspace:killTool`, killed ? '已终止' : '无正在运行的工具命令');
|
||||||
|
return { killed };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ contextBridge.exposeInMainWorld('metonaDesktop', {
|
|||||||
},
|
},
|
||||||
/** 无超时命令执行,用于 AI Tool Calling */
|
/** 无超时命令执行,用于 AI Tool Calling */
|
||||||
execTool: (command: string, cwd?: string) => ipcRenderer.invoke('workspace:execTool', command, cwd),
|
execTool: (command: string, cwd?: string) => ipcRenderer.invoke('workspace:execTool', command, cwd),
|
||||||
|
/** 终止当前 AI 工具命令 */
|
||||||
|
killTool: () => ipcRenderer.invoke('workspace:killTool'),
|
||||||
/** AI Tool Calling 命令输出 → 同步到工作空间终端 */
|
/** AI Tool Calling 命令输出 → 同步到工作空间终端 */
|
||||||
onToolOutput: (callback: (data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => void) => {
|
onToolOutput: (callback: (data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => void) => {
|
||||||
ipcRenderer.on('workspace:toolOutput', (_: unknown, data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => callback(data));
|
ipcRenderer.on('workspace:toolOutput', (_: unknown, data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => callback(data));
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export const DEFAULT_WORKSPACE_DIR = path.join(app.getPath('userData'), 'workspa
|
|||||||
/** 活跃进程 Map */
|
/** 活跃进程 Map */
|
||||||
const activeProcesses = new Map<string, ChildProcess>();
|
const activeProcesses = new Map<string, ChildProcess>();
|
||||||
|
|
||||||
|
/** 当前 AI 工具命令进程 ID(用于用户手动终止) */
|
||||||
|
let _currentToolProcessId: string | null = null;
|
||||||
|
|
||||||
/** 确保工作空间目录存在 */
|
/** 确保工作空间目录存在 */
|
||||||
export function ensureWorkspaceDir(): void {
|
export function ensureWorkspaceDir(): void {
|
||||||
if (!fs.existsSync(DEFAULT_WORKSPACE_DIR)) {
|
if (!fs.existsSync(DEFAULT_WORKSPACE_DIR)) {
|
||||||
@@ -253,6 +256,7 @@ export function execWorkspaceCommand(
|
|||||||
});
|
});
|
||||||
|
|
||||||
activeProcesses.set(cmdId, proc);
|
activeProcesses.set(cmdId, proc);
|
||||||
|
_currentToolProcessId = cmdId;
|
||||||
|
|
||||||
proc.stdout.on('data', (data: Buffer) => {
|
proc.stdout.on('data', (data: Buffer) => {
|
||||||
if (stdoutBuf.length < MAX_OUTPUT) {
|
if (stdoutBuf.length < MAX_OUTPUT) {
|
||||||
@@ -280,6 +284,7 @@ export function execWorkspaceCommand(
|
|||||||
|
|
||||||
proc.on('close', (code) => {
|
proc.on('close', (code) => {
|
||||||
activeProcesses.delete(cmdId);
|
activeProcesses.delete(cmdId);
|
||||||
|
if (_currentToolProcessId === cmdId) _currentToolProcessId = null;
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
sendLog(
|
sendLog(
|
||||||
code === 0 ? 'success' : 'warn',
|
code === 0 ? 'success' : 'warn',
|
||||||
@@ -299,6 +304,7 @@ export function execWorkspaceCommand(
|
|||||||
|
|
||||||
proc.on('error', (err) => {
|
proc.on('error', (err) => {
|
||||||
activeProcesses.delete(cmdId);
|
activeProcesses.delete(cmdId);
|
||||||
|
if (_currentToolProcessId === cmdId) _currentToolProcessId = null;
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
sendLog('error', `🔧 execWorkspaceCommand 失败`, `${cmdId} | ${err.message}`);
|
sendLog('error', `🔧 execWorkspaceCommand 失败`, `${cmdId} | ${err.message}`);
|
||||||
resolve({
|
resolve({
|
||||||
@@ -334,3 +340,14 @@ export function execWorkspaceCommand(
|
|||||||
export function terminateWorkspaceCommand(commandId: string): boolean {
|
export function terminateWorkspaceCommand(commandId: string): boolean {
|
||||||
return killProcess(commandId);
|
return killProcess(commandId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 终止当前正在运行的 AI 工具命令
|
||||||
|
* @returns 是否成功终止
|
||||||
|
*/
|
||||||
|
export function terminateCurrentToolCommand(): boolean {
|
||||||
|
if (!_currentToolProcessId) return false;
|
||||||
|
const id = _currentToolProcessId;
|
||||||
|
_currentToolProcessId = null;
|
||||||
|
return killProcess(id);
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,17 @@ let fileTree: FileNode[] = [];
|
|||||||
let previewFile: { name: string; content: string; path: string } | null = null;
|
let previewFile: { name: string; content: string; path: string } | null = null;
|
||||||
let _counter = 0;
|
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 {
|
function genId(): string {
|
||||||
return `ws_${Date.now()}_${++_counter}`;
|
return `ws_${Date.now()}_${++_counter}`;
|
||||||
}
|
}
|
||||||
@@ -191,28 +202,6 @@ function bindEvents(): void {
|
|||||||
document.querySelector('#wsTabTerminal')?.addEventListener('click', () => switchTab('terminal'));
|
document.querySelector('#wsTabTerminal')?.addEventListener('click', () => switchTab('terminal'));
|
||||||
document.querySelector('#wsTabFiles')?.addEventListener('click', () => switchTab('files'));
|
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);
|
document.querySelector('#wsStopBtn')?.addEventListener('click', killCurrentProcess);
|
||||||
|
|
||||||
@@ -274,36 +263,16 @@ function getActiveSession(): TerminalSession | undefined {
|
|||||||
return terminalSessions.find(s => s.id === activeTerminalId);
|
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 命令输出 → 追加到活跃终端 */
|
/** AI Tool Calling 命令输出 → 追加到活跃终端 */
|
||||||
function appendToolOutput(command: string, stdout: string, stderr: string, exitCode: number | null): void {
|
function appendToolOutput(command: string, stdout: string, stderr: string, exitCode: number | null): void {
|
||||||
const session = getActiveSession();
|
const session = getActiveSession();
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
|
|
||||||
|
// 记录当前 AI 命令
|
||||||
|
currentAiCommand = command;
|
||||||
|
session.running = true;
|
||||||
|
updateHint();
|
||||||
|
|
||||||
// 显示执行的命令
|
// 显示执行的命令
|
||||||
session.lines.push({ type: 'system', text: `$ ${command} (AI)` });
|
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})`;
|
const exitMsg = exitCode === 0 ? '✓ AI 命令执行成功' : `✗ AI 命令退出 (code: ${exitCode})`;
|
||||||
session.lines.push({ type: 'system', text: exitMsg });
|
session.lines.push({ type: 'system', text: exitMsg });
|
||||||
|
|
||||||
|
// 命令结束
|
||||||
|
session.running = false;
|
||||||
|
currentAiCommand = null;
|
||||||
|
|
||||||
// 限制最大行数
|
// 限制最大行数
|
||||||
if (session.lines.length > 3000) {
|
if (session.lines.length > 3000) {
|
||||||
session.lines = session.lines.slice(-2000);
|
session.lines = session.lines.slice(-2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTerminal();
|
renderTerminal();
|
||||||
|
updateStopBtnState();
|
||||||
|
updateHint();
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendTerminalOutput(sessionId: string, type: 'stdout' | 'stderr', data: string): void {
|
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 {
|
function killCurrentProcess(): void {
|
||||||
const session = getActiveSession();
|
const session = getActiveSession();
|
||||||
if (!session || !session.running) return;
|
if (!session) return;
|
||||||
|
|
||||||
const bridge = window.metonaDesktop;
|
const bridge = window.metonaDesktop;
|
||||||
if (!bridge?.isDesktop) return;
|
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();
|
renderTerminal();
|
||||||
|
updateStopBtnState();
|
||||||
|
updateHint();
|
||||||
|
|
||||||
|
// 通知 AI 命令被用户终止
|
||||||
|
if (cmd && onToolTerminated) {
|
||||||
|
onToolTerminated(cmd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearTerminal(): void {
|
function clearTerminal(): void {
|
||||||
@@ -649,12 +642,23 @@ function renderPreview(): void {
|
|||||||
function updateStopBtnState(): void {
|
function updateStopBtnState(): void {
|
||||||
const stopBtn = document.querySelector('#wsStopBtn') as HTMLButtonElement;
|
const stopBtn = document.querySelector('#wsStopBtn') as HTMLButtonElement;
|
||||||
if (!stopBtn) return;
|
if (!stopBtn) return;
|
||||||
const session = getActiveSession();
|
const running = currentAiCommand !== null;
|
||||||
const running = session?.running || false;
|
|
||||||
stopBtn.disabled = !running;
|
stopBtn.disabled = !running;
|
||||||
stopBtn.classList.toggle('active', 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 {
|
function getFileIcon(name: string): string {
|
||||||
const ext = name.split('.').pop()?.toLowerCase() || '';
|
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 {
|
export function getWorkspaceDirPath(): string {
|
||||||
return workspaceDir;
|
return workspaceDir;
|
||||||
|
|||||||
+19
-22
@@ -182,28 +182,25 @@
|
|||||||
<div class="ws-term-tab-bar" id="wsTermTabBar"></div>
|
<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">
|
||||||
<input class="ws-term-input" id="wsTermInput" type="text" placeholder="输入命令,Enter 执行..." spellcheck="false">
|
<span class="ws-term-hint" id="wsTermHint">等待 AI 执行命令...</span>
|
||||||
<button class="ws-toolbar-btn" id="wsExecBtn" title="执行">
|
<div class="ws-term-toolbar-btns">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<button class="ws-toolbar-btn ws-stop-btn" id="wsStopBtn" title="终止当前命令" disabled>
|
||||||
<polygon points="5 3 19 12 5 21 5 3"/>
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||||
</svg>
|
<rect x="6" y="6" width="12" height="12" rx="1"/>
|
||||||
</button>
|
</svg>
|
||||||
<button class="ws-toolbar-btn ws-stop-btn" id="wsStopBtn" title="停止" disabled>
|
</button>
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
<button class="ws-toolbar-btn" id="wsClearBtn" title="清空">
|
||||||
<rect x="6" y="6" width="12" height="12" rx="1"/>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
</svg>
|
<polyline points="3 6 5 6 21 6"/>
|
||||||
</button>
|
<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"/>
|
||||||
<button class="ws-toolbar-btn" id="wsClearBtn" title="清空">
|
</svg>
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
</button>
|
||||||
<polyline points="3 6 5 6 21 6"/>
|
<button class="ws-toolbar-btn" id="wsNewTabBtn" title="新建终端">
|
||||||
<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 viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
</svg>
|
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||||
</button>
|
</svg>
|
||||||
<button class="ws-toolbar-btn" id="wsNewTabBtn" title="新建终端">
|
</button>
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
</div>
|
||||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2667,7 +2667,7 @@ html, body {
|
|||||||
|
|
||||||
.workspace-panel {
|
.workspace-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 90px; /* header(44px) + model-bar(46px) */
|
top: 92px; /* header(44px) + border(1px) + model-bar(46px) + border(1px) */
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 480px;
|
width: 480px;
|
||||||
@@ -2838,31 +2838,39 @@ html, body {
|
|||||||
.ws-term-toolbar {
|
.ws-term-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-top: 1px solid var(--border-default, rgba(255,255,255,0.08));
|
border-top: 1px solid var(--border-default, rgba(255,255,255,0.08));
|
||||||
background: var(--bg-layer-alt, #2a2a2a);
|
background: var(--bg-layer-alt, #2a2a2a);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ws-term-input {
|
.ws-term-hint {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: var(--bg-layer, #383838);
|
font-size: 11px;
|
||||||
border: 1px solid var(--border-default, rgba(255,255,255,0.08));
|
|
||||||
border-radius: 4px;
|
|
||||||
color: var(--text-primary, #fff);
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-family: 'Cascadia Code', 'Consolas', 'Courier New', monospace;
|
|
||||||
outline: none;
|
|
||||||
transition: border-color 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ws-term-input:focus {
|
|
||||||
border-color: var(--accent, #60CDFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ws-term-input::placeholder {
|
|
||||||
color: var(--text-tertiary, #666);
|
color: var(--text-tertiary, #666);
|
||||||
|
font-family: 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-term-hint.running {
|
||||||
|
color: var(--accent, #60CDFF);
|
||||||
|
animation: hint-pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hint-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.6; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-term-toolbar-btns {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ws-toolbar-btn {
|
.ws-toolbar-btn {
|
||||||
|
|||||||
Vendored
+2
@@ -243,6 +243,8 @@ export interface MetonaDesktopAPI {
|
|||||||
execTool: (command: string, cwd?: string) => Promise<{ success: boolean; stdout: string; stderr: string; exitCode: number | null; duration: number; userTerminated: boolean; truncated: boolean }>;
|
execTool: (command: string, cwd?: string) => Promise<{ success: boolean; stdout: string; stderr: string; exitCode: number | null; duration: number; userTerminated: boolean; truncated: boolean }>;
|
||||||
/** AI Tool Calling 命令输出 → 同步到工作空间终端 */
|
/** AI Tool Calling 命令输出 → 同步到工作空间终端 */
|
||||||
onToolOutput: (callback: (data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => void) => void;
|
onToolOutput: (callback: (data: { command: string; stdout: string; stderr: string; exitCode: number | null }) => void) => void;
|
||||||
|
/** 终止当前 AI 工具命令 */
|
||||||
|
killTool: () => Promise<{ killed: boolean }>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user