From 8b250c5c6b5aba3da0e125f71e359e05af62ac1c Mon Sep 17 00:00:00 2001
From: JOJO <1498581755@qq.com>
Date: Sun, 14 Dec 2025 21:20:29 +0800
Subject: [PATCH] feat: redesign todo window animation and layout
---
.../components/chat/VirtualMonitorSurface.vue | 33 ++-
.../chat/monitor/MonitorDirector.ts | 207 ++++++++++++------
.../components/chat/_virtual-monitor.scss | 120 ++++++++--
3 files changed, 260 insertions(+), 100 deletions(-)
diff --git a/static/src/components/chat/VirtualMonitorSurface.vue b/static/src/components/chat/VirtualMonitorSurface.vue
index 0d0d1e6..f60c726 100644
--- a/static/src/components/chat/VirtualMonitorSurface.vue
+++ b/static/src/components/chat/VirtualMonitorSurface.vue
@@ -138,6 +138,11 @@
+
@@ -145,21 +150,11 @@
- 任务看板
+ 待办事项
-
@@ -256,9 +251,8 @@ const memoryStatus = ref(null);
const memoryCount = ref(null);
const memoryTime = ref(null);
const todoWindow = ref(null);
-const todoBacklog = ref(null);
-const todoDoing = ref(null);
-const todoDone = ref(null);
+const todoSummary = ref(null);
+const todoList = ref(null);
const desktopMenu = ref(null);
const folderMenu = ref(null);
const fileMenu = ref(null);
@@ -338,9 +332,8 @@ const mountDirector = async () => {
memoryCount: memoryCount.value!,
memoryTime: memoryTime.value!,
todoWindow: todoWindow.value!,
- todoBacklog: todoBacklog.value!,
- todoDoing: todoDoing.value!,
- todoDone: todoDone.value!,
+ todoSummary: todoSummary.value!,
+ todoList: todoList.value!,
desktopMenu: desktopMenu.value!,
folderMenu: folderMenu.value!,
fileMenu: fileMenu.value!,
diff --git a/static/src/components/chat/monitor/MonitorDirector.ts b/static/src/components/chat/monitor/MonitorDirector.ts
index ffe1a7f..fb045ae 100644
--- a/static/src/components/chat/monitor/MonitorDirector.ts
+++ b/static/src/components/chat/monitor/MonitorDirector.ts
@@ -63,9 +63,8 @@ export interface MonitorElements {
memoryCount: HTMLElement;
memoryTime: HTMLElement;
todoWindow: HTMLElement;
- todoBacklog: HTMLElement;
- todoDoing: HTMLElement;
- todoDone: HTMLElement;
+ todoSummary: HTMLElement;
+ todoList: HTMLElement;
desktopMenu: HTMLElement;
folderMenu: HTMLElement;
fileMenu: HTMLElement;
@@ -140,7 +139,7 @@ const DESKTOP_APPS: Array<{ id: string; label: string; assetKey: string }> = [
{ id: 'command', label: '命令行', assetKey: 'command' },
{ id: 'python', label: 'Python', assetKey: 'python' },
{ id: 'memory', label: '记忆', assetKey: 'memory' },
- { id: 'todo', label: '看板', assetKey: 'todo' },
+ { id: 'todo', label: '待办事项', assetKey: 'todo' },
{ id: 'subagent', label: '子代理', assetKey: 'subagent' }
];
@@ -344,9 +343,8 @@ export class MonitorDirector implements MonitorDriver {
this.terminalLastFocusedAt = 0;
this.elements.readerLines.innerHTML = '';
this.elements.readerOcr.innerHTML = '';
- this.elements.todoBacklog.innerHTML = '';
- this.elements.todoDoing.innerHTML = '';
- this.elements.todoDone.innerHTML = '';
+ this.elements.todoSummary.textContent = '';
+ this.elements.todoList.innerHTML = '';
this.folderIcons.clear();
this.fileIcons.clear();
}
@@ -2770,34 +2768,56 @@ export class MonitorDirector implements MonitorDriver {
this.sceneHandlers.todoCreate = async (payload, runtime) => {
this.applySceneStatus(runtime, 'todoCreate', '正在更新待办');
- await this.movePointerToApp('todo');
- await this.click({ count: 2 });
- this.showWindow(this.elements.todoWindow);
- const title = payload?.arguments?.title || payload?.arguments?.task || '新的待办';
- this.pushTodoCard('backlog', title);
- await sleep(400);
+ const summary =
+ payload?.arguments?.summary ||
+ payload?.arguments?.overview ||
+ payload?.arguments?.title ||
+ payload?.arguments?.task ||
+ '待办摘要';
+ const tasks = this.normalizeTodoTasks(payload?.arguments?.tasks || summary);
+ await this.ensureTodoWindowVisible();
+ await this.typeTodoSummary(summary);
+ for (const task of tasks) {
+ await this.animateTodoAppend(task);
+ }
+ await sleep(320);
};
this.sceneHandlers.todoUpdate = async (payload, runtime) => {
- await this.sceneHandlers.todoCreate(payload, runtime);
- this.applySceneStatus(runtime, 'todoUpdate', '正在调整进度');
- this.shiftTodoCard('backlog', 'doing');
- await sleep(400);
+ this.applySceneStatus(runtime, 'todoUpdate', '正在调整待办');
+ await this.ensureTodoWindowVisible();
+ const targetText = payload?.arguments?.title || payload?.arguments?.task || null;
+ const completed =
+ payload?.arguments?.completed ?? payload?.arguments?.done ?? payload?.arguments?.checked ?? true;
+ await this.toggleTodoItem(targetText, !!completed);
+ await sleep(260);
};
this.sceneHandlers.todoFinish = async (_payload, runtime) => {
this.applySceneStatus(runtime, 'todoFinish', '正在完成任务');
- this.shiftTodoCard('doing', 'done');
- await sleep(400);
+ const redDot = this.elements.todoWindow.querySelector('.traffic-dot.red') as HTMLElement | null;
+ if (redDot) {
+ await this.movePointerToElement(redDot, { duration: 420 });
+ await this.click();
+ }
+ this.closeWindow(this.elements.todoWindow);
+ await sleep(320);
};
this.sceneHandlers.todoFinishConfirm = this.sceneHandlers.todoFinish;
this.sceneHandlers.todoDelete = async (payload, runtime) => {
- this.applySceneStatus(runtime, 'todoDelete', '正在移除任务');
- const title = payload?.arguments?.title;
- this.removeTodoCard(title);
- await sleep(300);
+ this.applySceneStatus(runtime, 'todoDelete', '正在移除待办');
+ const targetText = payload?.arguments?.title || payload?.arguments?.task || null;
+ const card = this.findTodoItemByText(targetText);
+ if (card) {
+ await this.scrollTodoItemIntoView(card);
+ card.classList.add('removing');
+ await this.movePointerToElement(card, { duration: 420 });
+ await this.click({ right: true });
+ setTimeout(() => card.remove(), 260);
+ }
+ await sleep(280);
};
this.sceneHandlers.terminalSession = async (payload, runtime) => {
@@ -4989,51 +5009,116 @@ export class MonitorDirector implements MonitorDriver {
this.updateMemoryMeta();
}
- private pushTodoCard(column: 'backlog' | 'doing' | 'done', title: string) {
- const map: Record<'backlog' | 'doing' | 'done', HTMLElement> = {
- backlog: this.elements.todoBacklog,
- doing: this.elements.todoDoing,
- done: this.elements.todoDone
- };
- const card = document.createElement('div');
- card.className = 'todo-card';
- card.textContent = title;
- map[column].appendChild(card);
+ private getTodoItems(): HTMLElement[] {
+ if (!this.elements.todoList) return [];
+ return Array.from(this.elements.todoList.children) as HTMLElement[];
}
- private shiftTodoCard(source: 'backlog' | 'doing', target: 'doing' | 'done') {
- const map: Record<'backlog' | 'doing' | 'done', HTMLElement> = {
- backlog: this.elements.todoBacklog,
- doing: this.elements.todoDoing,
- done: this.elements.todoDone
- };
- const sourceList = map[source];
- if (!sourceList.firstElementChild) {
- return;
+ private findTodoItemByText(text?: string | null): HTMLElement | null {
+ if (!text) return this.getTodoItems()[0] || null;
+ const target = (text || '').trim();
+ return this.getTodoItems().find(item => item.querySelector('.todo-text')?.textContent?.trim() === target) || null;
+ }
+
+ private normalizeTodoTasks(raw: any): Array<{ text: string; done?: boolean }> {
+ if (Array.isArray(raw)) {
+ return raw
+ .map(item => {
+ if (typeof item === 'string') return { text: item, done: false };
+ if (item && typeof item === 'object') {
+ const text = String(item.title || item.task || item.text || '').trim();
+ const done = typeof item.completed === 'boolean' ? item.completed : !!item.done;
+ if (!text) return null;
+ return { text, done };
+ }
+ return null;
+ })
+ .filter(Boolean) as Array<{ text: string; done?: boolean }>;
}
- const card = sourceList.firstElementChild as HTMLElement;
- sourceList.removeChild(card);
- card.classList.add('moving');
- requestAnimationFrame(() => {
- card.classList.remove('moving');
- map[target].appendChild(card);
- if (target === 'done') {
- card.classList.add('done');
- }
+ if (typeof raw === 'string') {
+ return [{ text: raw, done: false }];
+ }
+ return [];
+ }
+
+ private createTodoItem(text: string, done = false) {
+ const item = document.createElement('div');
+ item.className = 'todo-item fly-in';
+ const body = document.createElement('div');
+ body.className = 'todo-text';
+ body.textContent = text;
+ const check = document.createElement('div');
+ check.className = 'todo-check';
+ if (done) {
+ check.classList.add('checked');
+ item.classList.add('done');
+ }
+ item.appendChild(body);
+ item.appendChild(check);
+ return item;
+ }
+
+ private renderTodoItems(items: Array<{ text: string; done?: boolean }>) {
+ if (!this.elements.todoList) return;
+ this.elements.todoList.innerHTML = '';
+ items.forEach(task => {
+ const card = this.createTodoItem(task.text, !!task.done);
+ card.classList.add('visible');
+ this.elements.todoList.appendChild(card);
});
}
- private removeTodoCard(title?: string) {
- if (!title) {
+ private async ensureTodoWindowVisible() {
+ if (this.isWindowVisible(this.elements.todoWindow)) {
+ this.showWindow(this.elements.todoWindow);
return;
}
- const selector = `.todo-card`;
- const cards = this.elements.todoWindow.querySelectorAll(selector);
- cards.forEach(card => {
- if (card.textContent?.trim() === title.trim()) {
- card.classList.add('removing');
- setTimeout(() => card.remove(), 300);
- }
- });
+ await this.movePointerToApp('todo');
+ await this.click({ count: 2 });
+ this.showWindow(this.elements.todoWindow);
+ }
+
+ private async typeTodoSummary(text: string) {
+ if (!this.elements.todoSummary) return;
+ await this.movePointerToElement(this.elements.todoSummary, { duration: 420 });
+ await this.click();
+ this.elements.todoSummary.textContent = '';
+ const chars = Array.from(text || '暂无摘要');
+ for (const ch of chars) {
+ this.elements.todoSummary.textContent = (this.elements.todoSummary.textContent || '') + ch;
+ await sleep(20);
+ }
+ }
+
+ private async animateTodoAppend(task: { text: string; done?: boolean }) {
+ if (!this.elements.todoList) return;
+ const card = this.createTodoItem(task.text, !!task.done);
+ this.elements.todoList.appendChild(card);
+ requestAnimationFrame(() => card.classList.add('visible'));
+ await this.scrollTodoItemIntoView(card);
+ await sleep(180);
+ }
+
+ private async scrollTodoItemIntoView(card: HTMLElement | null) {
+ if (!card || !this.elements.todoList) return;
+ const body = this.elements.todoList;
+ const targetTop = card.offsetTop - body.clientHeight * 0.2;
+ const clamped = Math.max(0, targetTop);
+ body.scrollTo({ top: clamped, behavior: 'smooth' });
+ await this.waitForScrollSettled(body, clamped);
+ }
+
+ private async toggleTodoItem(text?: string | null, done?: boolean) {
+ const card = this.findTodoItemByText(text);
+ if (!card) return;
+ await this.scrollTodoItemIntoView(card);
+ const check = card.querySelector('.todo-check') as HTMLElement | null;
+ if (!check) return;
+ await this.movePointerToElement(check, { duration: 420 });
+ await this.click();
+ const targetState = typeof done === 'boolean' ? done : !check.classList.contains('checked');
+ check.classList.toggle('checked', targetState);
+ card.classList.toggle('done', targetState);
+ await sleep(200);
}
}
diff --git a/static/src/styles/components/chat/_virtual-monitor.scss b/static/src/styles/components/chat/_virtual-monitor.scss
index 84ba492..8325e09 100644
--- a/static/src/styles/components/chat/_virtual-monitor.scss
+++ b/static/src/styles/components/chat/_virtual-monitor.scss
@@ -1135,32 +1135,114 @@
transition: transform 0.24s ease, opacity 0.22s ease;
}
-/* memory-tag removed to simplify UI */
+.virtual-monitor-surface .memory-footer.hidden { display: none; }
-.virtual-monitor-surface .todo-columns {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 12px;
-}
-
-.virtual-monitor-surface .todo-column {
- background: rgba(255, 255, 255, 0.65);
- border-radius: 12px;
- padding: 10px;
-}
-
-.virtual-monitor-surface .todo-list {
+/* Todo Window */
+.virtual-monitor-surface .todo-window {
+ width: 520px;
+ height: 360px;
+ top: 180px;
+ left: 520px;
display: flex;
flex-direction: column;
- gap: 8px;
}
-.virtual-monitor-surface .todo-card {
- background: #fff;
+.virtual-monitor-surface .todo-body {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 12px;
+}
+
+.virtual-monitor-surface .todo-summary {
+ max-height: 120px;
+ min-height: 64px;
+ padding: 10px 12px;
border-radius: 10px;
- padding: 8px 10px;
+ background: rgba(15, 23, 42, 0.04);
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ overflow-y: auto;
+ line-height: 1.5;
font-size: 13px;
- box-shadow: 0 4px 12px rgba(15, 23, 42, 0.12);
+}
+
+.virtual-monitor-surface .todo-progress {
+ flex: 1 1 auto;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ overflow-y: auto;
+ padding-right: 2px;
+}
+
+.virtual-monitor-surface .todo-summary,
+.virtual-monitor-surface .todo-progress {
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+}
+.virtual-monitor-surface .todo-summary::-webkit-scrollbar,
+.virtual-monitor-surface .todo-progress::-webkit-scrollbar {
+ display: none;
+ width: 0;
+ height: 0;
+}
+
+.virtual-monitor-surface .todo-item {
+ display: grid;
+ grid-template-columns: 1fr 32px;
+ gap: 10px;
+ align-items: center;
+ padding: 12px 14px;
+ border-radius: 12px;
+ background: #fff;
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12);
+ opacity: 0;
+ transform: translateX(-28px);
+ transition: transform 0.32s ease, opacity 0.28s ease, box-shadow 0.22s ease;
+}
+
+.virtual-monitor-surface .todo-item.visible {
+ opacity: 1;
+ transform: translateX(0);
+}
+
+.virtual-monitor-surface .todo-item.done {
+ border-color: rgba(72, 187, 120, 0.55);
+ box-shadow: 0 10px 24px rgba(72, 187, 120, 0.18);
+}
+
+.virtual-monitor-surface .todo-text {
+ line-height: 1.5;
+ word-break: break-word;
+ white-space: pre-wrap;
+ font-size: 13px;
+}
+
+.virtual-monitor-surface .todo-check {
+ width: 20px;
+ height: 20px;
+ border-radius: 6px;
+ border: 2px solid rgba(15, 23, 42, 0.16);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+ position: relative;
+}
+
+.virtual-monitor-surface .todo-check.checked {
+ background: linear-gradient(135deg, #4caf50, #60c78f);
+ border-color: transparent;
+}
+
+.virtual-monitor-surface .todo-check.checked::after {
+ content: '✔';
+ color: #fff;
+ font-size: 12px;
+ line-height: 1;
}
.virtual-monitor-surface .todo-card.done {