feat: enhance virtual monitor command/python playback
This commit is contained in:
parent
757e1adaae
commit
8755688c8e
@ -43,6 +43,15 @@ class TerminalOperator:
|
||||
self._toolbox: Optional[ToolboxContainer] = None
|
||||
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:
|
||||
"""
|
||||
自动检测可用的Python命令
|
||||
@ -137,6 +146,8 @@ class TerminalOperator:
|
||||
Returns:
|
||||
执行结果字典
|
||||
"""
|
||||
# 每次执行前重置工具容器,防止上一条命令的输出/状态干扰
|
||||
self._reset_toolbox()
|
||||
# 替换命令中的python3为实际可用的命令
|
||||
if "python3" in command and self.python_cmd != "python3":
|
||||
command = command.replace("python3", self.python_cmd)
|
||||
@ -297,6 +308,9 @@ class TerminalOperator:
|
||||
"""
|
||||
timeout = timeout or CODE_EXECUTION_TIMEOUT
|
||||
|
||||
# 强制重置工具容器,避免上一段代码仍在运行时输出混入
|
||||
self._reset_toolbox()
|
||||
|
||||
# 创建临时Python文件
|
||||
temp_file = self.project_path / ".temp_code.py"
|
||||
|
||||
|
||||
@ -89,6 +89,32 @@
|
||||
</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-header">
|
||||
<span class="traffic-dot red"></span>
|
||||
@ -218,6 +244,15 @@ const terminalTabs = ref<HTMLElement | null>(null);
|
||||
const terminalTabList = ref<HTMLElement | null>(null);
|
||||
const terminalAddButton = 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 readerTitle = 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,
|
||||
apps: {
|
||||
browser: new URL('../../icons/globe.svg', import.meta.url).href,
|
||||
terminal: new URL('../../icons/terminal.svg', import.meta.url).href,
|
||||
command: new URL('../../icons/laptop.svg', import.meta.url).href,
|
||||
terminal: 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,
|
||||
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
|
||||
}
|
||||
};
|
||||
@ -291,6 +326,15 @@ const mountDirector = async () => {
|
||||
terminalTabList: terminalTabList.value!,
|
||||
terminalAddButton: terminalAddButton.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!,
|
||||
readerTitle: readerTitle.value!,
|
||||
readerLines: readerLines.value!,
|
||||
|
||||
@ -44,6 +44,15 @@ export interface MonitorElements {
|
||||
terminalTabList: HTMLElement;
|
||||
terminalAddButton: HTMLElement;
|
||||
terminalBody: HTMLElement;
|
||||
commandWindow: HTMLElement;
|
||||
commandTitle: HTMLElement;
|
||||
commandInput: HTMLElement;
|
||||
commandOutput: HTMLElement;
|
||||
pythonWindow: HTMLElement;
|
||||
pythonTitle: HTMLElement;
|
||||
pythonBody: HTMLElement;
|
||||
pythonInput: HTMLElement;
|
||||
pythonOutput: HTMLElement;
|
||||
readerWindow: HTMLElement;
|
||||
readerTitle: HTMLElement;
|
||||
readerLines: HTMLElement;
|
||||
@ -129,9 +138,9 @@ const DESKTOP_APPS: Array<{ id: string; label: string; assetKey: string }> = [
|
||||
{ id: 'browser', label: '浏览器', assetKey: 'browser' },
|
||||
{ id: 'terminal', label: '终端', assetKey: 'terminal' },
|
||||
{ id: 'command', label: '命令行', assetKey: 'command' },
|
||||
{ id: 'python', label: 'Python', assetKey: 'python' },
|
||||
{ id: 'memory', label: '记忆', assetKey: 'memory' },
|
||||
{ id: 'todo', label: '看板', assetKey: 'todo' },
|
||||
{ id: 'reader', label: '阅读器', assetKey: 'reader' },
|
||||
{ id: 'subagent', label: '子代理', assetKey: 'subagent' }
|
||||
];
|
||||
|
||||
@ -190,6 +199,7 @@ export class MonitorDirector implements MonitorDriver {
|
||||
private folderIcons = new Map<string, HTMLElement>();
|
||||
// 用于控制编辑器动画全局加速(根据本次补丁的总体改动量动态调整)
|
||||
private editorSpeedBoost = 1;
|
||||
private pythonRunToken = 0;
|
||||
private pendingDesktopFolders = new Set<string>();
|
||||
private fileIcons = new Map<string, HTMLElement>();
|
||||
private browserResultMap = new Map<string, HTMLLIElement>();
|
||||
@ -218,6 +228,7 @@ export class MonitorDirector implements MonitorDriver {
|
||||
placeholder: false
|
||||
};
|
||||
private editorSnapshots = new Map<string, string[]>();
|
||||
private commandCurrentText = '';
|
||||
private progressBubbleTimer: number | null = null;
|
||||
private progressBubbleBase: string | null = null;
|
||||
private progressSceneName: string | null = null;
|
||||
@ -226,9 +237,27 @@ export class MonitorDirector implements MonitorDriver {
|
||||
private waitingBubbleTimer: number | null = null;
|
||||
private waitingBubbleBase: string | null = null;
|
||||
private progressBubbleActive = false;
|
||||
// 当实际执行进度快于动画播放时,用于压制“正在 xxx”提示
|
||||
private playbackLagging = false;
|
||||
// 记录最近一次 Python 执行的ID,用于丢弃过期动画/结果
|
||||
private latestPythonExecutionId: string | number | null = null;
|
||||
private lastTerminalSessionId: string | null = null;
|
||||
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) {
|
||||
if (!runtime || typeof runtime.setStatus !== 'function') {
|
||||
return;
|
||||
@ -256,7 +285,7 @@ export class MonitorDirector implements MonitorDriver {
|
||||
(window as any).__TERMINAL_MENU_DEBUG_BUILD = '2025-12-13-1';
|
||||
terminalMenuDebug('constructor:init', { build: (window as any).__TERMINAL_MENU_DEBUG_BUILD });
|
||||
const resizeHandler = () => {
|
||||
this.screenRect = this.elements.screen.getBoundingClientRect();
|
||||
this.refreshScreenRect();
|
||||
this.layoutFloatingWindows();
|
||||
};
|
||||
window.addEventListener('resize', resizeHandler, { passive: true });
|
||||
@ -502,11 +531,19 @@ export class MonitorDirector implements MonitorDriver {
|
||||
if (progressLabel && !isPlaybackPhase) {
|
||||
ensureProgressBubble();
|
||||
}
|
||||
|
||||
// 若进入播放阶段且有执行结果已完成,压制“正在…”提示
|
||||
if (isPlaybackPhase) {
|
||||
this.playbackLagging = true;
|
||||
} else {
|
||||
this.playbackLagging = false;
|
||||
}
|
||||
|
||||
const wrappedRuntime: MonitorSceneRuntime = {
|
||||
...runtime,
|
||||
waitForResult: async (id?: string | number | null) => {
|
||||
const waitFn = runtime.waitForResult || (() => Promise.resolve(null));
|
||||
if (!isPlaybackPhase) {
|
||||
if (!isPlaybackPhase && !this.playbackLagging) {
|
||||
ensureProgressBubble();
|
||||
}
|
||||
const waitKey = id ?? payload?.executionId ?? payload?.id;
|
||||
@ -514,6 +551,7 @@ export class MonitorDirector implements MonitorDriver {
|
||||
try {
|
||||
const result = await waitFn(id);
|
||||
progressDebug('playScene:waitForResult:resolved', { scene: name, id: waitKey });
|
||||
this.playbackLagging = false;
|
||||
return result;
|
||||
} finally {
|
||||
clearProgressBubble();
|
||||
@ -527,6 +565,7 @@ export class MonitorDirector implements MonitorDriver {
|
||||
monitorLifecycleDebug('playScene:end', {
|
||||
scene: name
|
||||
});
|
||||
this.playbackLagging = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1189,6 +1228,157 @@ export class MonitorDirector implements MonitorDriver {
|
||||
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) {
|
||||
await this.movePointerToApp('terminal');
|
||||
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.editorWindow, { x: 0.62, y: 0.58 });
|
||||
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.memoryWindow, { x: 0.28, 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.applySceneStatus(runtime, 'runCommand', '正在执行命令');
|
||||
const { sessionId } = await this.ensureTerminalSessionReady(payload, { focusPrompt: true, createIfMissing: true });
|
||||
const command = payload?.arguments?.command || 'echo "Hello"';
|
||||
await this.typeSessionCommand(sessionId, command);
|
||||
const command = payload?.arguments?.command || payload?.result?.command || 'echo \"Hello\"';
|
||||
const reuse = this.isWindowVisible(this.elements.commandWindow);
|
||||
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 output = completion?.result?.output || completion?.result?.stdout || '命令执行完成';
|
||||
const lines = this.sanitizeTerminalOutput(
|
||||
@ -2111,16 +2312,47 @@ export class MonitorDirector implements MonitorDriver {
|
||||
? output.map(String)
|
||||
: [String(output || '')]
|
||||
);
|
||||
this.appendTerminalOutputs(sessionId, command, lines.length ? lines : ['命令执行完成']);
|
||||
this.appendCommandOutput(lines.length ? lines : ['命令执行完成']);
|
||||
await sleep(500);
|
||||
};
|
||||
|
||||
this.sceneHandlers.runPython = async (payload, runtime) => {
|
||||
this.applySceneStatus(runtime, 'runPython', '正在执行 Python');
|
||||
const { sessionId } = await this.ensureTerminalSessionReady(payload, { focusPrompt: true, createIfMissing: true });
|
||||
const code = payload?.arguments?.code || 'print("Hello")';
|
||||
await this.typeSessionCommand(sessionId, code);
|
||||
const runId =
|
||||
payload?.executionId ||
|
||||
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);
|
||||
if (runToken !== this.pythonRunToken) {
|
||||
// 有新的 Python 运行已启动,本次结果丢弃
|
||||
return;
|
||||
}
|
||||
const output = completion?.result?.output || completion?.result?.stdout || '>>> 执行完成';
|
||||
const lines = this.sanitizeTerminalOutput(
|
||||
typeof output === 'string'
|
||||
@ -2129,7 +2361,7 @@ export class MonitorDirector implements MonitorDriver {
|
||||
? output.map(String)
|
||||
: [String(output || '')]
|
||||
);
|
||||
this.appendTerminalOutputs(sessionId, code, lines.length ? lines : ['>>> 执行完成']);
|
||||
this.appendPythonOutput('输出', lines.join('\n') || '>>> 执行完成');
|
||||
await sleep(500);
|
||||
};
|
||||
|
||||
@ -2771,6 +3003,7 @@ export class MonitorDirector implements MonitorDriver {
|
||||
};
|
||||
|
||||
private async movePointerToApp(appId: string) {
|
||||
this.refreshScreenRect();
|
||||
const icon = this.appIcons.get(appId);
|
||||
if (icon) {
|
||||
await this.movePointerToElement(icon);
|
||||
@ -2780,6 +3013,7 @@ export class MonitorDirector implements MonitorDriver {
|
||||
}
|
||||
|
||||
private async movePointerToDesktop() {
|
||||
this.refreshScreenRect();
|
||||
return this.movePointerToElement(this.elements.desktopGrid, { offsetX: 160, offsetY: 120, duration: 700 });
|
||||
}
|
||||
|
||||
@ -2787,6 +3021,7 @@ export class MonitorDirector implements MonitorDriver {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
this.refreshScreenRect();
|
||||
this.raiseWindowForTarget(target);
|
||||
if (!this.progressBubbleBase) {
|
||||
this.dismissBubble(true);
|
||||
|
||||
@ -831,6 +831,9 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
|
||||
// 工具准备中事件 - 实时显示
|
||||
ctx.socket.on('tool_preparing', (data) => {
|
||||
if (ctx.dropToolEvents) {
|
||||
return;
|
||||
}
|
||||
socketLog('工具准备中:', data.name);
|
||||
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
||||
socketLog('跳过tool_preparing(对话不匹配)', data.conversation_id);
|
||||
@ -876,6 +879,9 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
|
||||
// 工具状态更新事件 - 实时显示详细状态
|
||||
ctx.socket.on('tool_status', (data) => {
|
||||
if (ctx.dropToolEvents) {
|
||||
return;
|
||||
}
|
||||
socketLog('工具状态:', data);
|
||||
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
||||
socketLog('跳过tool_status(对话不匹配)', data.conversation_id);
|
||||
@ -899,6 +905,9 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
|
||||
// 工具开始(从准备转为执行)
|
||||
ctx.socket.on('tool_start', (data) => {
|
||||
if (ctx.dropToolEvents) {
|
||||
return;
|
||||
}
|
||||
socketLog('工具开始执行:', data.name);
|
||||
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
||||
socketLog('跳过tool_start(对话不匹配)', data.conversation_id);
|
||||
@ -956,6 +965,9 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
|
||||
// 更新action(工具完成)
|
||||
ctx.socket.on('update_action', (data) => {
|
||||
if (ctx.dropToolEvents) {
|
||||
return;
|
||||
}
|
||||
socketLog('更新action:', data.id, 'status:', data.status);
|
||||
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
||||
socketLog('跳过update_action(对话不匹配)', data.conversation_id);
|
||||
@ -1047,6 +1059,9 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
});
|
||||
|
||||
ctx.socket.on('append_payload', (data) => {
|
||||
if (ctx.dropToolEvents) {
|
||||
return;
|
||||
}
|
||||
socketLog('收到append_payload事件:', data);
|
||||
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
||||
socketLog('跳过append_payload(对话不匹配)', data.conversation_id);
|
||||
@ -1064,6 +1079,9 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
});
|
||||
|
||||
ctx.socket.on('modify_payload', (data) => {
|
||||
if (ctx.dropToolEvents) {
|
||||
return;
|
||||
}
|
||||
socketLog('收到modify_payload事件:', data);
|
||||
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
||||
socketLog('跳过modify_payload(对话不匹配)', data.conversation_id);
|
||||
@ -1084,6 +1102,13 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
ctx.socket.on('stop_requested', (data) => {
|
||||
socketLog('停止请求已接收:', data.message);
|
||||
// 可以显示提示信息
|
||||
try {
|
||||
if (typeof ctx.clearPendingTools === 'function') {
|
||||
ctx.clearPendingTools('socket:stop_requested');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('清理未完成工具失败', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 任务停止
|
||||
|
||||
@ -251,6 +251,158 @@
|
||||
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 {
|
||||
width: 360px;
|
||||
height: 320px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user