fix: render monitor desktop immediately on load

This commit is contained in:
JOJO 2025-12-16 00:39:36 +08:00
parent fce6fb0eb8
commit fc88a22272
2 changed files with 137 additions and 13 deletions

View File

@ -228,13 +228,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'; import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useMonitorStore } from '@/stores/monitor'; import { useMonitorStore } from '@/stores/monitor';
import { MonitorDirector, type MonitorElements } from '@/components/chat/monitor/MonitorDirector'; import { MonitorDirector, type MonitorElements } from '@/components/chat/monitor/MonitorDirector';
const monitorStore = useMonitorStore(); const monitorStore = useMonitorStore();
const { statusLabel, progressIndicator } = storeToRefs(monitorStore); const { statusLabel, progressIndicator, lastTreeSnapshot } = storeToRefs(monitorStore);
const currentProgressLabel = computed(() => progressIndicator.value?.label || ''); const currentProgressLabel = computed(() => progressIndicator.value?.label || '');
const screenEl = ref<HTMLElement | null>(null); const screenEl = ref<HTMLElement | null>(null);
@ -384,12 +384,27 @@ const mountDirector = async () => {
const instance = new MonitorDirector(elements, assets); const instance = new MonitorDirector(elements, assets);
director.value = instance; director.value = instance;
monitorStore.registerDriver(instance); monitorStore.registerDriver(instance);
//
if (Array.isArray(monitorStore.lastTreeSnapshot) && monitorStore.lastTreeSnapshot.length) {
instance.setDesktopRoots(monitorStore.lastTreeSnapshot, { immediate: true });
}
}; };
onMounted(() => { onMounted(() => {
mountDirector(); mountDirector();
}); });
// /
watch(
lastTreeSnapshot,
roots => {
if (director.value && Array.isArray(roots)) {
director.value.setDesktopRoots(roots, { immediate: true });
}
},
{ immediate: true }
);
onBeforeUnmount(() => { onBeforeUnmount(() => {
monitorStore.unregisterDriver(); monitorStore.unregisterDriver();
director.value?.destroy(); director.value?.destroy();

View File

@ -195,6 +195,8 @@ export class MonitorDirector implements MonitorDriver {
private desktopRoots: string[] = []; private desktopRoots: string[] = [];
private screenRect: DOMRect; private screenRect: DOMRect;
private pointerBase = { x: 60, y: 120 }; private pointerBase = { x: 60, y: 120 };
private pendingPointerTransform: { x: number; y: number; duration: number } | null = null;
private screenObserver: ResizeObserver | null = null;
private bubbleTimer: number | null = null; private bubbleTimer: number | null = null;
private windowAnchors = new Map<HTMLElement, { x: number; y: number }>(); private windowAnchors = new Map<HTMLElement, { x: number; y: number }>();
private destroyFns: Array<() => void> = []; private destroyFns: Array<() => void> = [];
@ -254,6 +256,10 @@ export class MonitorDirector implements MonitorDriver {
private refreshScreenRect() { private refreshScreenRect() {
const prev = this.screenRect; const prev = this.screenRect;
const rect = this.elements.screen.getBoundingClientRect(); const rect = this.elements.screen.getBoundingClientRect();
// 隐藏状态下 rect 可能为 0避免把指针归零
if (rect.width < 1 || rect.height < 1) {
return;
}
this.screenRect = rect; this.screenRect = rect;
if (prev && prev.width > 0 && prev.height > 0) { if (prev && prev.width > 0 && prev.height > 0) {
const relX = this.pointerBase.x / prev.width; const relX = this.pointerBase.x / prev.width;
@ -285,6 +291,7 @@ export class MonitorDirector implements MonitorDriver {
this.setupScenes(); this.setupScenes();
this.bindTerminalInteractions(); this.bindTerminalInteractions();
this.populateDesktop(); this.populateDesktop();
this.setupScreenObserver();
this.extractionAnchor = this.windowAnchors.get(this.elements.extractionWindow) || this.extractionAnchor; this.extractionAnchor = this.windowAnchors.get(this.elements.extractionWindow) || this.extractionAnchor;
this.prepareExtractionTemplate(); this.prepareExtractionTemplate();
this.layoutFloatingWindows(); this.layoutFloatingWindows();
@ -294,6 +301,7 @@ export class MonitorDirector implements MonitorDriver {
const resizeHandler = () => { const resizeHandler = () => {
this.refreshScreenRect(); this.refreshScreenRect();
this.layoutFloatingWindows(); this.layoutFloatingWindows();
this.flushPendingPointerTransform();
}; };
window.addEventListener('resize', resizeHandler, { passive: true }); window.addEventListener('resize', resizeHandler, { passive: true });
this.destroyFns.push(() => window.removeEventListener('resize', resizeHandler)); this.destroyFns.push(() => window.removeEventListener('resize', resizeHandler));
@ -312,7 +320,10 @@ export class MonitorDirector implements MonitorDriver {
preservePointer?: boolean; preservePointer?: boolean;
preserveWindows?: boolean; preserveWindows?: boolean;
}) { }) {
// 清理挂起的指针位置,避免后续尺寸变化时覆盖当前重置
this.pendingPointerTransform = null;
const preserveWindows = !!options?.preserveWindows; const preserveWindows = !!options?.preserveWindows;
const preservePointer = options?.preservePointer !== false; // 默认保留指针位置
this.cancelManualDrag(); this.cancelManualDrag();
if (!preserveWindows) { if (!preserveWindows) {
this.resetManualPositions(); this.resetManualPositions();
@ -327,7 +338,7 @@ export class MonitorDirector implements MonitorDriver {
} else { } else {
this.stopBubbleTimers(); this.stopBubbleTimers();
} }
if (!options?.preservePointer) { if (!preservePointer) {
this.pointerBase = { x: 60, y: 120 }; this.pointerBase = { x: 60, y: 120 };
this.elements.mousePointer.style.transform = 'translate3d(60px, 120px, 0)'; this.elements.mousePointer.style.transform = 'translate3d(60px, 120px, 0)';
} }
@ -357,6 +368,9 @@ export class MonitorDirector implements MonitorDriver {
} }
if (Array.isArray(options?.desktopRoots) && options?.desktopRoots.length) { if (Array.isArray(options?.desktopRoots) && options?.desktopRoots.length) {
this.setDesktopRoots(options.desktopRoots, { immediate: true }); this.setDesktopRoots(options.desktopRoots, { immediate: true });
} else if (this.desktopRoots.length) {
// 保持当前根目录重新渲染,避免在重置后桌面空白
this.setDesktopRoots(this.desktopRoots, { immediate: true });
} }
this.renderTerminalTabs(); this.renderTerminalTabs();
} }
@ -724,6 +738,10 @@ export class MonitorDirector implements MonitorDriver {
} }
private positionBubble() { private positionBubble() {
// 隐藏状态display: none时不更新气泡与指针避免坐标被重置到原点
if (this.elements.screen.clientWidth < 1 || this.elements.screen.clientHeight < 1) {
return;
}
const bubble = this.elements.speechBubble; const bubble = this.elements.speechBubble;
const bubbleRect = bubble.getBoundingClientRect(); const bubbleRect = bubble.getBoundingClientRect();
const width = bubbleRect.width || 220; const width = bubbleRect.width || 220;
@ -1306,9 +1324,12 @@ export class MonitorDirector implements MonitorDriver {
await this.focusCommandInput(); await this.focusCommandInput();
const toDelete = this.commandCurrentText; const toDelete = this.commandCurrentText;
if (toDelete) { if (toDelete) {
for (let i = toDelete.length; i > 0; i -= 1) { const len = toDelete.length;
const boost = 1 + Math.min(len / 40, 6); // 行越长删除越快
const interval = Math.max(6, Math.floor(18 / boost));
for (let i = len; i > 0; i -= 1) {
target.textContent = toDelete.slice(0, i - 1); target.textContent = toDelete.slice(0, i - 1);
await sleep(18); await sleep(interval);
} }
this.commandCurrentText = ''; this.commandCurrentText = '';
} }
@ -2533,11 +2554,14 @@ export class MonitorDirector implements MonitorDriver {
execution: payload?.executionId || payload?.id, execution: payload?.executionId || payload?.id,
path: rawPath path: rawPath
}); });
const targetEntry = await this.revealFileTarget(rawPath, { spawnDesktopFile: true }); const editorAlreadyVisible = this.isWindowVisible(this.elements.editorWindow);
if (targetEntry?.element) { if (!editorAlreadyVisible) {
await this.openFileMenuAction(targetEntry.element, 'edit'); const targetEntry = await this.revealFileTarget(rawPath, { spawnDesktopFile: true });
} else { if (targetEntry?.element) {
await this.movePointerToDesktop(); await this.openFileMenuAction(targetEntry.element, 'edit');
} else {
await this.movePointerToDesktop();
}
} }
this.openEditorWindow(filename); this.openEditorWindow(filename);
this.renderEditorPlaceholder('正在读取文件内容...'); this.renderEditorPlaceholder('正在读取文件内容...');
@ -3409,6 +3433,9 @@ export class MonitorDirector implements MonitorDriver {
return; return;
} }
this.refreshScreenRect(); this.refreshScreenRect();
if (this.elements.screen.clientWidth < 1 || this.elements.screen.clientHeight < 1) {
return;
}
this.raiseWindowForTarget(target); this.raiseWindowForTarget(target);
if (!this.progressBubbleBase) { if (!this.progressBubbleBase) {
this.dismissBubble(true); this.dismissBubble(true);
@ -3419,6 +3446,9 @@ export class MonitorDirector implements MonitorDriver {
} }
const { offsetX = 0, offsetY = 0, duration = 900 } = options; const { offsetX = 0, offsetY = 0, duration = 900 } = options;
const rect = target.getBoundingClientRect(); const rect = target.getBoundingClientRect();
if (rect.width < 1 && rect.height < 1) {
return;
}
const desiredX = rect.left - this.screenRect.left + rect.width / 2 + offsetX; const desiredX = rect.left - this.screenRect.left + rect.width / 2 + offsetX;
const desiredY = rect.top - this.screenRect.top + rect.height / 2 + offsetY; const desiredY = rect.top - this.screenRect.top + rect.height / 2 + offsetY;
const pointerX = desiredX - POINTER_TIP_OFFSET.x; const pointerX = desiredX - POINTER_TIP_OFFSET.x;
@ -3494,9 +3524,26 @@ export class MonitorDirector implements MonitorDriver {
}; };
} }
private flushPendingPointerTransform() {
if (!this.pendingPointerTransform) {
return;
}
const pending = this.pendingPointerTransform;
this.pendingPointerTransform = null;
this.updatePointerTransform(pending.x, pending.y, pending.duration);
}
private updatePointerTransform(x: number, y: number, duration = 0) { private updatePointerTransform(x: number, y: number, duration = 0) {
const clampedX = Math.min(Math.max(0, x), Math.max(0, this.elements.screen.clientWidth - 10)); if (!Number.isFinite(x) || !Number.isFinite(y)) {
const clampedY = Math.min(Math.max(0, y), Math.max(0, this.elements.screen.clientHeight - 10)); return;
}
const screenWidth = this.elements.screen.clientWidth || this.screenRect?.width || 0;
const screenHeight = this.elements.screen.clientHeight || this.screenRect?.height || 0;
if (screenWidth < 1 || screenHeight < 1) {
return;
}
const clampedX = Math.min(Math.max(0, x), Math.max(0, screenWidth - 10));
const clampedY = Math.min(Math.max(0, y), Math.max(0, screenHeight - 10));
this.elements.mousePointer.style.setProperty('--mouse-duration', `${Math.max(duration, 0)}ms`); this.elements.mousePointer.style.setProperty('--mouse-duration', `${Math.max(duration, 0)}ms`);
this.elements.mousePointer.style.transform = `translate3d(${clampedX}px, ${clampedY}px, 0)`; this.elements.mousePointer.style.transform = `translate3d(${clampedX}px, ${clampedY}px, 0)`;
this.pointerBase = { x: clampedX, y: clampedY }; this.pointerBase = { x: clampedX, y: clampedY };
@ -3511,6 +3558,24 @@ export class MonitorDirector implements MonitorDriver {
}; };
} }
private setupScreenObserver() {
if (typeof ResizeObserver === 'undefined') {
return;
}
this.screenObserver = new ResizeObserver(entries => {
const entry = Array.isArray(entries) ? entries[0] : null;
const width = entry?.contentRect?.width || this.elements.screen.clientWidth;
const height = entry?.contentRect?.height || this.elements.screen.clientHeight;
if (width < 1 || height < 1) {
return;
}
this.refreshScreenRect();
this.flushPendingPointerTransform();
});
this.screenObserver.observe(this.elements.screen);
this.destroyFns.push(() => this.screenObserver?.disconnect());
}
private showWindow(el: HTMLElement) { private showWindow(el: HTMLElement) {
el.classList.remove('closing'); el.classList.remove('closing');
el.classList.add('visible'); el.classList.add('visible');
@ -4230,15 +4295,30 @@ export class MonitorDirector implements MonitorDriver {
private async animateEditorTransition(nextLines: any) { private async animateEditorTransition(nextLines: any) {
const currentLines = this.editorScene.lines.slice(); const currentLines = this.editorScene.lines.slice();
const targetLines = this.sanitizeEditorLines(nextLines); const targetLines = this.sanitizeEditorLines(nextLines);
const lineCount = targetLines.length;
editorDebug('animate:start', { editorDebug('animate:start', {
currentLines: currentLines.length, currentLines: currentLines.length,
targetLines: targetLines.length targetLines: lineCount
}); });
if (!currentLines.length && !targetLines.length) { if (!currentLines.length && !targetLines.length) {
editorDebug('animate:both-empty'); editorDebug('animate:both-empty');
this.renderEditorPlaceholder('(文件当前为空)'); this.renderEditorPlaceholder('(文件当前为空)');
return; return;
} }
// 规则:
// - > 30 行:直接瞬间渲染
if (lineCount > 30) {
editorDebug('animate:skip-gt-30', { lineCount });
this.renderEditorSnapshot(targetLines);
return;
}
// - 9~30 行:逐行动画(不逐字符)
if (lineCount > 8) {
editorDebug('animate:line-fill', { lineCount });
await this.animateEditorLineFill(targetLines);
return;
}
// - ≤8 行:保留原逐字符/行内阈值逻辑
if ( if (
currentLines.length > EDITOR_DIFF_LIMIT || currentLines.length > EDITOR_DIFF_LIMIT ||
targetLines.length > EDITOR_DIFF_LIMIT targetLines.length > EDITOR_DIFF_LIMIT
@ -4296,6 +4376,35 @@ export class MonitorDirector implements MonitorDriver {
this.editorSpeedBoost = 1; this.editorSpeedBoost = 1;
} }
private async animateEditorLineFill(lines: string[]) {
const normalized = this.sanitizeEditorLines(lines);
if (!normalized.length) {
this.renderEditorPlaceholder('(文件当前为空)');
return;
}
const container = this.elements.editorBody;
container.innerHTML = '';
// 先放空行占位,再逐行填充文本,避免逐字符动画
normalized.forEach((_, index) => {
const row = this.buildEditorLineElement('', index, undefined, { prefill: false });
container.appendChild(row);
});
container.scrollTo({ top: 0 });
const delay = 28;
const rows = Array.from(container.children) as HTMLElement[];
for (let i = 0; i < rows.length; i += 1) {
const row = rows[i];
row.textContent = normalized[i] || ' ';
row.classList.add('visible');
this.adjustEditorScrollForLine(row);
await sleep(delay);
}
this.editorScene.lines = normalized.slice();
this.editorScene.placeholder = false;
this.syncEditorIndices();
this.editorSpeedBoost = 1;
}
private buildEditorDiff(before: string[], after: string[]): EditorOperation[] { private buildEditorDiff(before: string[], after: string[]): EditorOperation[] {
const m = before.length; const m = before.length;
const n = after.length; const n = after.length;