feat: enhance virtual monitor command/python playback

This commit is contained in:
JOJO 2025-12-14 17:38:03 +08:00
parent 757e1adaae
commit 8755688c8e
5 changed files with 484 additions and 14 deletions

View File

@ -42,6 +42,15 @@ class TerminalOperator:
print(f"{OUTPUT_FORMATS['info']} 检测到Python命令: {self.python_cmd}") print(f"{OUTPUT_FORMATS['info']} 检测到Python命令: {self.python_cmd}")
self._toolbox: Optional[ToolboxContainer] = None self._toolbox: Optional[ToolboxContainer] = None
self.container_session: Optional["ContainerHandle"] = container_session self.container_session: Optional["ContainerHandle"] = container_session
def _reset_toolbox(self):
"""强制关闭并重建工具终端,保证每次命令/脚本运行独立环境。"""
if self._toolbox:
try:
self._toolbox.shutdown()
except Exception:
pass
self._toolbox = None
def _detect_python_command(self) -> str: def _detect_python_command(self) -> str:
""" """
@ -137,6 +146,8 @@ class TerminalOperator:
Returns: Returns:
执行结果字典 执行结果字典
""" """
# 每次执行前重置工具容器,防止上一条命令的输出/状态干扰
self._reset_toolbox()
# 替换命令中的python3为实际可用的命令 # 替换命令中的python3为实际可用的命令
if "python3" in command and self.python_cmd != "python3": if "python3" in command and self.python_cmd != "python3":
command = command.replace("python3", self.python_cmd) command = command.replace("python3", self.python_cmd)
@ -296,6 +307,9 @@ class TerminalOperator:
执行结果字典 执行结果字典
""" """
timeout = timeout or CODE_EXECUTION_TIMEOUT timeout = timeout or CODE_EXECUTION_TIMEOUT
# 强制重置工具容器,避免上一段代码仍在运行时输出混入
self._reset_toolbox()
# 创建临时Python文件 # 创建临时Python文件
temp_file = self.project_path / ".temp_code.py" temp_file = self.project_path / ".temp_code.py"

View File

@ -89,6 +89,32 @@
</div> </div>
</div> </div>
<div class="window command-window" ref="commandWindow">
<div class="window-header">
<span class="traffic-dot red"></span>
<span class="traffic-dot yellow"></span>
<span class="traffic-dot green"></span>
<span ref="commandTitle">命令行</span>
</div>
<div class="command-body">
<div class="command-input" ref="commandInput"></div>
<div class="command-output" ref="commandOutput"></div>
</div>
</div>
<div class="window python-window" ref="pythonWindow">
<div class="window-header">
<span class="traffic-dot red"></span>
<span class="traffic-dot yellow"></span>
<span class="traffic-dot green"></span>
<span ref="pythonTitle">Python</span>
</div>
<div class="python-body" ref="pythonBody">
<div class="python-input" ref="pythonInput"></div>
<div class="python-output" ref="pythonOutput"></div>
</div>
</div>
<div class="window reader-window" ref="readerWindow"> <div class="window reader-window" ref="readerWindow">
<div class="window-header"> <div class="window-header">
<span class="traffic-dot red"></span> <span class="traffic-dot red"></span>
@ -218,6 +244,15 @@ const terminalTabs = ref<HTMLElement | null>(null);
const terminalTabList = ref<HTMLElement | null>(null); const terminalTabList = ref<HTMLElement | null>(null);
const terminalAddButton = ref<HTMLElement | null>(null); const terminalAddButton = ref<HTMLElement | null>(null);
const terminalBody = ref<HTMLElement | null>(null); const terminalBody = ref<HTMLElement | null>(null);
const commandWindow = ref<HTMLElement | null>(null);
const commandTitle = ref<HTMLElement | null>(null);
const commandInput = ref<HTMLElement | null>(null);
const commandOutput = ref<HTMLElement | null>(null);
const pythonWindow = ref<HTMLElement | null>(null);
const pythonTitle = ref<HTMLElement | null>(null);
const pythonBody = ref<HTMLElement | null>(null);
const pythonInput = ref<HTMLElement | null>(null);
const pythonOutput = ref<HTMLElement | null>(null);
const readerWindow = ref<HTMLElement | null>(null); const readerWindow = ref<HTMLElement | null>(null);
const readerTitle = ref<HTMLElement | null>(null); const readerTitle = ref<HTMLElement | null>(null);
const readerLines = ref<HTMLElement | null>(null); const readerLines = ref<HTMLElement | null>(null);
@ -251,11 +286,11 @@ const assets = {
fileIcon: new URL('../../icons/file.svg', import.meta.url).href, fileIcon: new URL('../../icons/file.svg', import.meta.url).href,
apps: { apps: {
browser: new URL('../../icons/globe.svg', import.meta.url).href, browser: new URL('../../icons/globe.svg', import.meta.url).href,
terminal: new URL('../../icons/terminal.svg', import.meta.url).href, terminal: new URL('../../icons/laptop.svg', import.meta.url).href,
command: new URL('../../icons/laptop.svg', import.meta.url).href, command: new URL('../../icons/terminal.svg', import.meta.url).href,
python: new URL('../../icons/python.svg', import.meta.url).href,
memory: new URL('../../icons/sticky-note.svg', import.meta.url).href, memory: new URL('../../icons/sticky-note.svg', import.meta.url).href,
todo: new URL('../../icons/clipboard.svg', import.meta.url).href, todo: new URL('../../icons/clipboard.svg', import.meta.url).href,
reader: new URL('../../icons/book.svg', import.meta.url).href,
subagent: new URL('../../icons/bot.svg', import.meta.url).href subagent: new URL('../../icons/bot.svg', import.meta.url).href
} }
}; };
@ -291,6 +326,15 @@ const mountDirector = async () => {
terminalTabList: terminalTabList.value!, terminalTabList: terminalTabList.value!,
terminalAddButton: terminalAddButton.value!, terminalAddButton: terminalAddButton.value!,
terminalBody: terminalBody.value!, terminalBody: terminalBody.value!,
commandWindow: commandWindow.value!,
commandTitle: commandTitle.value!,
commandInput: commandInput.value!,
commandOutput: commandOutput.value!,
pythonWindow: pythonWindow.value!,
pythonTitle: pythonTitle.value!,
pythonBody: pythonBody.value!,
pythonInput: pythonInput.value!,
pythonOutput: pythonOutput.value!,
readerWindow: readerWindow.value!, readerWindow: readerWindow.value!,
readerTitle: readerTitle.value!, readerTitle: readerTitle.value!,
readerLines: readerLines.value!, readerLines: readerLines.value!,

View File

@ -44,6 +44,15 @@ export interface MonitorElements {
terminalTabList: HTMLElement; terminalTabList: HTMLElement;
terminalAddButton: HTMLElement; terminalAddButton: HTMLElement;
terminalBody: HTMLElement; terminalBody: HTMLElement;
commandWindow: HTMLElement;
commandTitle: HTMLElement;
commandInput: HTMLElement;
commandOutput: HTMLElement;
pythonWindow: HTMLElement;
pythonTitle: HTMLElement;
pythonBody: HTMLElement;
pythonInput: HTMLElement;
pythonOutput: HTMLElement;
readerWindow: HTMLElement; readerWindow: HTMLElement;
readerTitle: HTMLElement; readerTitle: HTMLElement;
readerLines: HTMLElement; readerLines: HTMLElement;
@ -129,9 +138,9 @@ const DESKTOP_APPS: Array<{ id: string; label: string; assetKey: string }> = [
{ id: 'browser', label: '浏览器', assetKey: 'browser' }, { id: 'browser', label: '浏览器', assetKey: 'browser' },
{ id: 'terminal', label: '终端', assetKey: 'terminal' }, { id: 'terminal', label: '终端', assetKey: 'terminal' },
{ id: 'command', label: '命令行', assetKey: 'command' }, { id: 'command', label: '命令行', assetKey: 'command' },
{ id: 'python', label: 'Python', assetKey: 'python' },
{ id: 'memory', label: '记忆', assetKey: 'memory' }, { id: 'memory', label: '记忆', assetKey: 'memory' },
{ id: 'todo', label: '看板', assetKey: 'todo' }, { id: 'todo', label: '看板', assetKey: 'todo' },
{ id: 'reader', label: '阅读器', assetKey: 'reader' },
{ id: 'subagent', label: '子代理', assetKey: 'subagent' } { id: 'subagent', label: '子代理', assetKey: 'subagent' }
]; ];
@ -190,6 +199,7 @@ export class MonitorDirector implements MonitorDriver {
private folderIcons = new Map<string, HTMLElement>(); private folderIcons = new Map<string, HTMLElement>();
// 用于控制编辑器动画全局加速(根据本次补丁的总体改动量动态调整) // 用于控制编辑器动画全局加速(根据本次补丁的总体改动量动态调整)
private editorSpeedBoost = 1; private editorSpeedBoost = 1;
private pythonRunToken = 0;
private pendingDesktopFolders = new Set<string>(); private pendingDesktopFolders = new Set<string>();
private fileIcons = new Map<string, HTMLElement>(); private fileIcons = new Map<string, HTMLElement>();
private browserResultMap = new Map<string, HTMLLIElement>(); private browserResultMap = new Map<string, HTMLLIElement>();
@ -218,6 +228,7 @@ export class MonitorDirector implements MonitorDriver {
placeholder: false placeholder: false
}; };
private editorSnapshots = new Map<string, string[]>(); private editorSnapshots = new Map<string, string[]>();
private commandCurrentText = '';
private progressBubbleTimer: number | null = null; private progressBubbleTimer: number | null = null;
private progressBubbleBase: string | null = null; private progressBubbleBase: string | null = null;
private progressSceneName: string | null = null; private progressSceneName: string | null = null;
@ -226,9 +237,27 @@ export class MonitorDirector implements MonitorDriver {
private waitingBubbleTimer: number | null = null; private waitingBubbleTimer: number | null = null;
private waitingBubbleBase: string | null = null; private waitingBubbleBase: string | null = null;
private progressBubbleActive = false; private progressBubbleActive = false;
// 当实际执行进度快于动画播放时,用于压制“正在 xxx”提示
private playbackLagging = false;
// 记录最近一次 Python 执行的ID用于丢弃过期动画/结果
private latestPythonExecutionId: string | number | null = null;
private lastTerminalSessionId: string | null = null; private lastTerminalSessionId: string | null = null;
private terminalLastFocusedAt = 0; private terminalLastFocusedAt = 0;
private refreshScreenRect() {
const prev = this.screenRect;
const rect = this.elements.screen.getBoundingClientRect();
this.screenRect = rect;
if (prev && prev.width > 0 && prev.height > 0) {
const relX = this.pointerBase.x / prev.width;
const relY = this.pointerBase.y / prev.height;
this.pointerBase = {
x: relX * rect.width,
y: relY * rect.height
};
}
}
private applySceneStatus(runtime: MonitorSceneRuntime, sceneName: string, fallback: string) { private applySceneStatus(runtime: MonitorSceneRuntime, sceneName: string, fallback: string) {
if (!runtime || typeof runtime.setStatus !== 'function') { if (!runtime || typeof runtime.setStatus !== 'function') {
return; return;
@ -256,7 +285,7 @@ export class MonitorDirector implements MonitorDriver {
(window as any).__TERMINAL_MENU_DEBUG_BUILD = '2025-12-13-1'; (window as any).__TERMINAL_MENU_DEBUG_BUILD = '2025-12-13-1';
terminalMenuDebug('constructor:init', { build: (window as any).__TERMINAL_MENU_DEBUG_BUILD }); terminalMenuDebug('constructor:init', { build: (window as any).__TERMINAL_MENU_DEBUG_BUILD });
const resizeHandler = () => { const resizeHandler = () => {
this.screenRect = this.elements.screen.getBoundingClientRect(); this.refreshScreenRect();
this.layoutFloatingWindows(); this.layoutFloatingWindows();
}; };
window.addEventListener('resize', resizeHandler, { passive: true }); window.addEventListener('resize', resizeHandler, { passive: true });
@ -502,11 +531,19 @@ export class MonitorDirector implements MonitorDriver {
if (progressLabel && !isPlaybackPhase) { if (progressLabel && !isPlaybackPhase) {
ensureProgressBubble(); ensureProgressBubble();
} }
// 若进入播放阶段且有执行结果已完成,压制“正在…”提示
if (isPlaybackPhase) {
this.playbackLagging = true;
} else {
this.playbackLagging = false;
}
const wrappedRuntime: MonitorSceneRuntime = { const wrappedRuntime: MonitorSceneRuntime = {
...runtime, ...runtime,
waitForResult: async (id?: string | number | null) => { waitForResult: async (id?: string | number | null) => {
const waitFn = runtime.waitForResult || (() => Promise.resolve(null)); const waitFn = runtime.waitForResult || (() => Promise.resolve(null));
if (!isPlaybackPhase) { if (!isPlaybackPhase && !this.playbackLagging) {
ensureProgressBubble(); ensureProgressBubble();
} }
const waitKey = id ?? payload?.executionId ?? payload?.id; const waitKey = id ?? payload?.executionId ?? payload?.id;
@ -514,6 +551,7 @@ export class MonitorDirector implements MonitorDriver {
try { try {
const result = await waitFn(id); const result = await waitFn(id);
progressDebug('playScene:waitForResult:resolved', { scene: name, id: waitKey }); progressDebug('playScene:waitForResult:resolved', { scene: name, id: waitKey });
this.playbackLagging = false;
return result; return result;
} finally { } finally {
clearProgressBubble(); clearProgressBubble();
@ -527,6 +565,7 @@ export class MonitorDirector implements MonitorDriver {
monitorLifecycleDebug('playScene:end', { monitorLifecycleDebug('playScene:end', {
scene: name scene: name
}); });
this.playbackLagging = false;
} }
} }
@ -1189,6 +1228,157 @@ export class MonitorDirector implements MonitorDriver {
return top === instance.element; return top === instance.element;
} }
private resetCommandWindow(title = '命令行', options: { clearOutput?: boolean } = {}) {
if (this.elements.commandTitle) {
this.elements.commandTitle.textContent = title;
}
if (this.elements.commandInput) {
this.elements.commandInput.textContent = '';
}
if (options.clearOutput && this.elements.commandOutput) {
this.elements.commandOutput.innerHTML = '';
}
this.commandCurrentText = '';
}
private async revealCommandWindow(title = '命令行', options: { reset?: boolean; focusInput?: boolean } = {}) {
const { reset = true, focusInput = false } = options;
const visible = this.isWindowVisible(this.elements.commandWindow);
if (!visible) {
await this.movePointerToApp('command');
await this.click();
}
if (reset) {
this.resetCommandWindow(title, { clearOutput: true });
} else {
if (this.elements.commandTitle) {
this.elements.commandTitle.textContent = title;
}
}
this.showWindow(this.elements.commandWindow);
if (focusInput) {
await this.focusCommandInput();
}
}
private async focusCommandInput() {
if (!this.elements.commandInput) {
return;
}
await this.movePointerToElement(this.elements.commandInput, { duration: 360 });
await this.click();
}
private async typeCommandText(text: string) {
if (!this.elements.commandInput) {
return;
}
const target = this.elements.commandInput;
await this.focusCommandInput();
const toDelete = this.commandCurrentText;
if (toDelete) {
for (let i = toDelete.length; i > 0; i -= 1) {
target.textContent = toDelete.slice(0, i - 1);
await sleep(18);
}
this.commandCurrentText = '';
}
const chars = Array.from(text);
for (const ch of chars) {
target.textContent = (target.textContent || '') + ch;
await sleep(28);
}
this.commandCurrentText = text;
}
private appendCommandOutput(lines: string[]) {
if (!this.elements.commandOutput) {
return;
}
const frag = document.createDocumentFragment();
lines.forEach(line => {
const row = document.createElement('div');
row.className = 'command-line';
row.textContent = line;
frag.appendChild(row);
});
this.elements.commandOutput.appendChild(frag);
this.elements.commandOutput.scrollTop = this.elements.commandOutput.scrollHeight;
}
private resetPythonWindow(title = 'Python') {
if (this.elements.pythonTitle) {
this.elements.pythonTitle.textContent = title;
}
if (this.elements.pythonInput) {
this.elements.pythonInput.textContent = '';
}
if (this.elements.pythonOutput) {
this.elements.pythonOutput.innerHTML = '';
}
}
private appendPythonOutput(label: string, content: string) {
if (!this.elements.pythonOutput) return;
const wrapper = document.createElement('div');
wrapper.className = 'python-block output';
const title = document.createElement('div');
title.className = 'python-block-title';
title.textContent = label;
const pre = document.createElement('pre');
pre.textContent = content;
wrapper.appendChild(title);
wrapper.appendChild(pre);
this.elements.pythonOutput.innerHTML = '';
this.elements.pythonOutput.appendChild(wrapper);
this.scrollPythonToResult();
}
private async revealPythonWindow(title = 'Python') {
await this.movePointerToApp('python');
await this.click();
this.resetPythonWindow(title);
this.showWindow(this.elements.pythonWindow);
}
private async focusPythonInput() {
if (!this.elements.pythonInput) return;
await this.movePointerToElement(this.elements.pythonInput, { duration: 360 });
await this.click();
}
private async typePythonCode(code: string, options: { deletePrevious?: boolean; animate?: boolean } = {}) {
if (!this.elements.pythonInput) return;
const { deletePrevious = true, animate = true } = options;
const target = this.elements.pythonInput;
if (deletePrevious) {
target.textContent = '';
}
await this.focusPythonInput();
if (!animate) {
target.textContent = code;
return;
}
const chars = Array.from(code);
for (const ch of chars) {
target.textContent = (target.textContent || '') + ch;
await sleep(24);
}
}
private scrollPythonToTop() {
if (this.elements.pythonBody) {
this.elements.pythonBody.scrollTo({ top: 0, behavior: 'smooth' });
}
}
private scrollPythonToResult() {
if (this.elements.pythonBody && this.elements.pythonOutput) {
const top = this.elements.pythonOutput.offsetTop - this.elements.pythonBody.offsetTop;
this.elements.pythonBody.scrollTo({ top, behavior: 'smooth' });
}
}
private async revealTerminalWindow(instance: TerminalShell, title: string) { private async revealTerminalWindow(instance: TerminalShell, title: string) {
await this.movePointerToApp('terminal'); await this.movePointerToApp('terminal');
await this.click(); await this.click();
@ -1675,6 +1865,8 @@ export class MonitorDirector implements MonitorDriver {
this.windowAnchors.set(this.elements.folderWindow, { x: 0.68, y: 0.26 }); this.windowAnchors.set(this.elements.folderWindow, { x: 0.68, y: 0.26 });
this.windowAnchors.set(this.elements.editorWindow, { x: 0.62, y: 0.58 }); this.windowAnchors.set(this.elements.editorWindow, { x: 0.62, y: 0.58 });
this.windowAnchors.set(this.elements.terminalWindow, { x: 0.18, y: 0.42 }); this.windowAnchors.set(this.elements.terminalWindow, { x: 0.18, y: 0.42 });
this.windowAnchors.set(this.elements.commandWindow, { x: 0.2, y: 0.7 });
this.windowAnchors.set(this.elements.pythonWindow, { x: 0.52, y: 0.16 });
this.windowAnchors.set(this.elements.readerWindow, { x: 0.42, y: 0.05 }); this.windowAnchors.set(this.elements.readerWindow, { x: 0.42, y: 0.05 });
this.windowAnchors.set(this.elements.memoryWindow, { x: 0.28, y: 0.32 }); this.windowAnchors.set(this.elements.memoryWindow, { x: 0.28, y: 0.32 });
this.windowAnchors.set(this.elements.todoWindow, { x: 0.5, y: 0.32 }); this.windowAnchors.set(this.elements.todoWindow, { x: 0.5, y: 0.32 });
@ -2099,9 +2291,18 @@ export class MonitorDirector implements MonitorDriver {
this.sceneHandlers.runCommand = async (payload, runtime) => { this.sceneHandlers.runCommand = async (payload, runtime) => {
this.applySceneStatus(runtime, 'runCommand', '正在执行命令'); this.applySceneStatus(runtime, 'runCommand', '正在执行命令');
const { sessionId } = await this.ensureTerminalSessionReady(payload, { focusPrompt: true, createIfMissing: true }); const command = payload?.arguments?.command || payload?.result?.command || 'echo \"Hello\"';
const command = payload?.arguments?.command || 'echo "Hello"'; const reuse = this.isWindowVisible(this.elements.commandWindow);
await this.typeSessionCommand(sessionId, command); if (reuse) {
this.showWindow(this.elements.commandWindow);
await this.focusCommandInput();
if (this.elements.commandOutput) {
this.elements.commandOutput.innerHTML = '';
}
} else {
await this.revealCommandWindow('命令行', { reset: true, focusInput: true });
}
await this.typeCommandText(command);
const completion = await runtime.waitForResult(payload.executionId || payload.id); const completion = await runtime.waitForResult(payload.executionId || payload.id);
const output = completion?.result?.output || completion?.result?.stdout || '命令执行完成'; const output = completion?.result?.output || completion?.result?.stdout || '命令执行完成';
const lines = this.sanitizeTerminalOutput( const lines = this.sanitizeTerminalOutput(
@ -2111,16 +2312,47 @@ export class MonitorDirector implements MonitorDriver {
? output.map(String) ? output.map(String)
: [String(output || '')] : [String(output || '')]
); );
this.appendTerminalOutputs(sessionId, command, lines.length ? lines : ['命令执行完成']); this.appendCommandOutput(lines.length ? lines : ['命令执行完成']);
await sleep(500); await sleep(500);
}; };
this.sceneHandlers.runPython = async (payload, runtime) => { this.sceneHandlers.runPython = async (payload, runtime) => {
this.applySceneStatus(runtime, 'runPython', '正在执行 Python'); this.applySceneStatus(runtime, 'runPython', '正在执行 Python');
const { sessionId } = await this.ensureTerminalSessionReady(payload, { focusPrompt: true, createIfMissing: true }); const runId =
const code = payload?.arguments?.code || 'print("Hello")'; payload?.executionId ||
await this.typeSessionCommand(sessionId, code); payload?.execution_id ||
payload?.id ||
`${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
// 丢弃过期的回放任务
if (this.latestPythonExecutionId && this.latestPythonExecutionId !== runId) {
const older =
typeof runId === 'number' && typeof this.latestPythonExecutionId === 'number'
? runId < this.latestPythonExecutionId
: false;
if (older) {
return;
}
}
this.latestPythonExecutionId = runId;
const code = payload?.arguments?.code || 'print(\"Hello\")';
const codeLines = this.normalizeLines(code);
const animate = codeLines.length <= 15;
const runToken = ++this.pythonRunToken;
const reuse = this.isWindowVisible(this.elements.pythonWindow);
if (reuse) {
this.showWindow(this.elements.pythonWindow);
this.scrollPythonToTop();
await this.focusPythonInput();
this.elements.pythonOutput.innerHTML = '';
} else {
await this.revealPythonWindow('Python');
}
await this.typePythonCode(code, { deletePrevious: true, animate });
const completion = await runtime.waitForResult(payload.executionId || payload.id); const completion = await runtime.waitForResult(payload.executionId || payload.id);
if (runToken !== this.pythonRunToken) {
// 有新的 Python 运行已启动,本次结果丢弃
return;
}
const output = completion?.result?.output || completion?.result?.stdout || '>>> 执行完成'; const output = completion?.result?.output || completion?.result?.stdout || '>>> 执行完成';
const lines = this.sanitizeTerminalOutput( const lines = this.sanitizeTerminalOutput(
typeof output === 'string' typeof output === 'string'
@ -2129,7 +2361,7 @@ export class MonitorDirector implements MonitorDriver {
? output.map(String) ? output.map(String)
: [String(output || '')] : [String(output || '')]
); );
this.appendTerminalOutputs(sessionId, code, lines.length ? lines : ['>>> 执行完成']); this.appendPythonOutput('输出', lines.join('\n') || '>>> 执行完成');
await sleep(500); await sleep(500);
}; };
@ -2771,6 +3003,7 @@ export class MonitorDirector implements MonitorDriver {
}; };
private async movePointerToApp(appId: string) { private async movePointerToApp(appId: string) {
this.refreshScreenRect();
const icon = this.appIcons.get(appId); const icon = this.appIcons.get(appId);
if (icon) { if (icon) {
await this.movePointerToElement(icon); await this.movePointerToElement(icon);
@ -2780,6 +3013,7 @@ export class MonitorDirector implements MonitorDriver {
} }
private async movePointerToDesktop() { private async movePointerToDesktop() {
this.refreshScreenRect();
return this.movePointerToElement(this.elements.desktopGrid, { offsetX: 160, offsetY: 120, duration: 700 }); return this.movePointerToElement(this.elements.desktopGrid, { offsetX: 160, offsetY: 120, duration: 700 });
} }
@ -2787,6 +3021,7 @@ export class MonitorDirector implements MonitorDriver {
if (!target) { if (!target) {
return; return;
} }
this.refreshScreenRect();
this.raiseWindowForTarget(target); this.raiseWindowForTarget(target);
if (!this.progressBubbleBase) { if (!this.progressBubbleBase) {
this.dismissBubble(true); this.dismissBubble(true);

View File

@ -831,6 +831,9 @@ export async function initializeLegacySocket(ctx: any) {
// 工具准备中事件 - 实时显示 // 工具准备中事件 - 实时显示
ctx.socket.on('tool_preparing', (data) => { ctx.socket.on('tool_preparing', (data) => {
if (ctx.dropToolEvents) {
return;
}
socketLog('工具准备中:', data.name); socketLog('工具准备中:', data.name);
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) { if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
socketLog('跳过tool_preparing(对话不匹配)', data.conversation_id); socketLog('跳过tool_preparing(对话不匹配)', data.conversation_id);
@ -876,6 +879,9 @@ export async function initializeLegacySocket(ctx: any) {
// 工具状态更新事件 - 实时显示详细状态 // 工具状态更新事件 - 实时显示详细状态
ctx.socket.on('tool_status', (data) => { ctx.socket.on('tool_status', (data) => {
if (ctx.dropToolEvents) {
return;
}
socketLog('工具状态:', data); socketLog('工具状态:', data);
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) { if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
socketLog('跳过tool_status(对话不匹配)', data.conversation_id); socketLog('跳过tool_status(对话不匹配)', data.conversation_id);
@ -899,6 +905,9 @@ export async function initializeLegacySocket(ctx: any) {
// 工具开始(从准备转为执行) // 工具开始(从准备转为执行)
ctx.socket.on('tool_start', (data) => { ctx.socket.on('tool_start', (data) => {
if (ctx.dropToolEvents) {
return;
}
socketLog('工具开始执行:', data.name); socketLog('工具开始执行:', data.name);
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) { if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
socketLog('跳过tool_start(对话不匹配)', data.conversation_id); socketLog('跳过tool_start(对话不匹配)', data.conversation_id);
@ -956,6 +965,9 @@ export async function initializeLegacySocket(ctx: any) {
// 更新action工具完成 // 更新action工具完成
ctx.socket.on('update_action', (data) => { ctx.socket.on('update_action', (data) => {
if (ctx.dropToolEvents) {
return;
}
socketLog('更新action:', data.id, 'status:', data.status); socketLog('更新action:', data.id, 'status:', data.status);
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) { if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
socketLog('跳过update_action(对话不匹配)', data.conversation_id); socketLog('跳过update_action(对话不匹配)', data.conversation_id);
@ -1047,6 +1059,9 @@ export async function initializeLegacySocket(ctx: any) {
}); });
ctx.socket.on('append_payload', (data) => { ctx.socket.on('append_payload', (data) => {
if (ctx.dropToolEvents) {
return;
}
socketLog('收到append_payload事件:', data); socketLog('收到append_payload事件:', data);
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) { if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
socketLog('跳过append_payload(对话不匹配)', data.conversation_id); socketLog('跳过append_payload(对话不匹配)', data.conversation_id);
@ -1064,6 +1079,9 @@ export async function initializeLegacySocket(ctx: any) {
}); });
ctx.socket.on('modify_payload', (data) => { ctx.socket.on('modify_payload', (data) => {
if (ctx.dropToolEvents) {
return;
}
socketLog('收到modify_payload事件:', data); socketLog('收到modify_payload事件:', data);
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) { if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
socketLog('跳过modify_payload(对话不匹配)', data.conversation_id); socketLog('跳过modify_payload(对话不匹配)', data.conversation_id);
@ -1084,6 +1102,13 @@ export async function initializeLegacySocket(ctx: any) {
ctx.socket.on('stop_requested', (data) => { ctx.socket.on('stop_requested', (data) => {
socketLog('停止请求已接收:', data.message); socketLog('停止请求已接收:', data.message);
// 可以显示提示信息 // 可以显示提示信息
try {
if (typeof ctx.clearPendingTools === 'function') {
ctx.clearPendingTools('socket:stop_requested');
}
} catch (error) {
console.warn('清理未完成工具失败', error);
}
}); });
// 任务停止 // 任务停止

View File

@ -251,6 +251,158 @@
flex-direction: column; flex-direction: column;
} }
.virtual-monitor-surface .command-window {
width: 520px;
height: 260px;
top: 540px;
left: 120px;
display: flex;
flex-direction: column;
background: #f7f9fd;
color: #0f172a;
border: 1px solid rgba(15, 23, 42, 0.08);
box-sizing: border-box;
}
.virtual-monitor-surface .command-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
background: #f7f9fd;
color: #0f172a;
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
font-size: 13px;
border-top: 1px solid rgba(15, 23, 42, 0.06);
padding: 8px 10px 10px;
box-sizing: border-box;
}
.virtual-monitor-surface .command-input {
padding: 12px 14px;
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
background: #ffffff;
white-space: pre-wrap;
overflow: hidden;
border-radius: 12px;
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.08);
min-height: 48px;
display: flex;
align-items: center;
line-height: 1.5;
word-break: break-word;
}
.virtual-monitor-surface .command-output {
padding: 12px 14px;
overflow-y: auto;
background: #eef2f9;
border-radius: 12px;
border: 1px solid rgba(15, 23, 42, 0.06);
flex: 1;
max-height: 100%;
scrollbar-gutter: stable;
box-sizing: border-box;
}
.virtual-monitor-surface .command-line {
line-height: 1.5;
white-space: pre-wrap;
}
.virtual-monitor-surface .command-line.highlight {
color: #16a34a;
}
.virtual-monitor-surface .python-window {
width: 520px;
height: 320px;
top: 120px;
left: 520px;
display: flex;
flex-direction: column;
background: #f9fbff;
color: #0f172a;
border: 1px solid rgba(15, 23, 42, 0.08);
box-sizing: border-box;
}
.virtual-monitor-surface .python-body {
flex: 1;
background: #f1f5fb;
color: #0f172a;
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
font-size: 13px;
padding: 10px 12px;
display: grid;
grid-template-rows: auto 1fr;
gap: 10px;
border-top: 1px solid rgba(15, 23, 42, 0.06);
box-sizing: border-box;
scrollbar-width: none;
min-height: 0; /* 防止子元素撑开导致整体溢出 */
}
.virtual-monitor-surface .python-body::-webkit-scrollbar {
width: 0;
height: 0;
}
.virtual-monitor-surface .python-input {
background: #ffffff;
border: 1px solid rgba(15, 23, 42, 0.08);
border-radius: 12px;
padding: 12px 14px;
min-height: 64px;
max-height: 180px;
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.08);
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
overflow-y: auto;
width: 100%;
box-sizing: border-box;
scrollbar-width: none;
}
.virtual-monitor-surface .python-input::-webkit-scrollbar {
width: 0;
height: 0;
}
.virtual-monitor-surface .python-output {
background: transparent;
border: 0;
border-radius: 0;
padding: 0;
box-shadow: none;
overflow-y: auto;
min-height: 140px;
scrollbar-width: none;
}
.virtual-monitor-surface .python-output::-webkit-scrollbar {
width: 0;
height: 0;
}
.virtual-monitor-surface .python-block {
background: #f6f8fe;
border: 1px solid rgba(15, 23, 42, 0.06);
border-radius: 10px;
padding: 10px 12px;
}
.virtual-monitor-surface .python-block-title {
font-weight: 600;
color: #1d4ed8;
margin-bottom: 6px;
}
.virtual-monitor-surface .python-block pre {
margin: 0;
white-space: pre-wrap;
line-height: 1.55;
}
.virtual-monitor-surface .reader-window { .virtual-monitor-surface .reader-window {
width: 360px; width: 360px;
height: 320px; height: 320px;