fix: refine memory monitor scrolling and snapshots
This commit is contained in:
parent
ca8f65fe35
commit
9183e0caf0
@ -301,7 +301,8 @@ const appOptions = {
|
|||||||
return this.streamingMessage || this.hasPendingToolActions();
|
return this.streamingMessage || this.hasPendingToolActions();
|
||||||
},
|
},
|
||||||
composerBusy() {
|
composerBusy() {
|
||||||
return this.streamingUi || this.monitorIsLocked || this.stopRequested;
|
const monitorLock = this.monitorIsLocked && this.chatDisplayMode === 'monitor';
|
||||||
|
return this.streamingUi || monitorLock || this.stopRequested;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -130,21 +130,14 @@
|
|||||||
|
|
||||||
<div class="window memory-window" ref="memoryWindow">
|
<div class="window memory-window" ref="memoryWindow">
|
||||||
<div class="window-header">
|
<div class="window-header">
|
||||||
<div>
|
|
||||||
<span class="traffic-dot red"></span>
|
<span class="traffic-dot red"></span>
|
||||||
<span class="traffic-dot yellow"></span>
|
<span class="traffic-dot yellow"></span>
|
||||||
<span class="traffic-dot green"></span>
|
<span class="traffic-dot green"></span>
|
||||||
<span>记忆记录</span>
|
<span class="window-title">记忆</span>
|
||||||
</div>
|
|
||||||
<span ref="memoryStatus">同步待命</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="memory-body">
|
<div class="memory-body">
|
||||||
<div class="memory-list" ref="memoryList"></div>
|
<div class="memory-list" ref="memoryList"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="memory-footer">
|
|
||||||
<span>总计 <strong ref="memoryCount">0</strong> 条</span>
|
|
||||||
<span>最后更新 <strong ref="memoryTime">--:--</strong></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="window todo-window" ref="todoWindow">
|
<div class="window todo-window" ref="todoWindow">
|
||||||
|
|||||||
@ -232,6 +232,7 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
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;
|
||||||
|
private latestMemoryScroll = 0;
|
||||||
private thinkingBubbleTimer: number | null = null;
|
private thinkingBubbleTimer: number | null = null;
|
||||||
private thinkingBubblePhase = 0;
|
private thinkingBubblePhase = 0;
|
||||||
private waitingBubbleTimer: number | null = null;
|
private waitingBubbleTimer: number | null = null;
|
||||||
@ -343,7 +344,6 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
this.terminalLastFocusedAt = 0;
|
this.terminalLastFocusedAt = 0;
|
||||||
this.elements.readerLines.innerHTML = '';
|
this.elements.readerLines.innerHTML = '';
|
||||||
this.elements.readerOcr.innerHTML = '';
|
this.elements.readerOcr.innerHTML = '';
|
||||||
this.elements.memoryList.innerHTML = '';
|
|
||||||
this.elements.todoBacklog.innerHTML = '';
|
this.elements.todoBacklog.innerHTML = '';
|
||||||
this.elements.todoDoing.innerHTML = '';
|
this.elements.todoDoing.innerHTML = '';
|
||||||
this.elements.todoDone.innerHTML = '';
|
this.elements.todoDone.innerHTML = '';
|
||||||
@ -1379,6 +1379,244 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ensureMemoryWindowVisible(options: { initialEntries?: string[]; memoryType?: string } = {}) {
|
||||||
|
const { initialEntries = [], memoryType = 'main' } = options;
|
||||||
|
const hydrate = async () => {
|
||||||
|
if (initialEntries.length) {
|
||||||
|
this.renderMemoryEntries(initialEntries);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.getMemoryItems().length) {
|
||||||
|
await this.loadMemoryEntries(memoryType);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (this.isWindowVisible(this.elements.memoryWindow)) {
|
||||||
|
this.showWindow(this.elements.memoryWindow);
|
||||||
|
if (!this.getMemoryItems().length || initialEntries.length) {
|
||||||
|
await hydrate();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.movePointerToApp('memory');
|
||||||
|
await this.click();
|
||||||
|
this.showWindow(this.elements.memoryWindow);
|
||||||
|
await hydrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMemoryItems(): HTMLElement[] {
|
||||||
|
if (!this.elements.memoryList) return [];
|
||||||
|
return Array.from(this.elements.memoryList.children) as HTMLElement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractMemorySnapshotEntries(payload: any, stage: 'before' | 'after' = 'before'): string[] {
|
||||||
|
const key = stage === 'after' ? 'monitor_snapshot_after' : 'monitor_snapshot';
|
||||||
|
const snapshot = payload?.[key];
|
||||||
|
const entries = snapshot?.entries;
|
||||||
|
if (Array.isArray(entries)) {
|
||||||
|
return entries.map(entry => String(entry ?? ''));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMemoryItemByIndex(index: number): HTMLElement | null {
|
||||||
|
const items = this.getMemoryItems();
|
||||||
|
if (!index || index < 1 || index > items.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return items[index - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForScrollSettled(el: HTMLElement, targetTop: number, timeout = 600) {
|
||||||
|
const started = Date.now();
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
const tick = () => {
|
||||||
|
const arrived = Math.abs(el.scrollTop - targetTop) < 2;
|
||||||
|
const expired = Date.now() - started > timeout;
|
||||||
|
if (arrived || expired) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
requestAnimationFrame(tick);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async scrollMemoryToBottom(waitMs = 200) {
|
||||||
|
const body = this.elements.memoryList;
|
||||||
|
if (!body) return;
|
||||||
|
const target = body.scrollHeight;
|
||||||
|
body.scrollTo({ top: target, behavior: 'smooth' });
|
||||||
|
await this.waitForScrollSettled(body, target);
|
||||||
|
this.latestMemoryScroll = Date.now();
|
||||||
|
if (waitMs > 0) {
|
||||||
|
await sleep(waitMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async scrollMemoryItemIntoView(card: HTMLElement | null, waitMs = 200) {
|
||||||
|
if (!card) return;
|
||||||
|
const body = this.elements.memoryList;
|
||||||
|
if (!body) return;
|
||||||
|
const targetTop = card.offsetTop - body.clientHeight * 0.35;
|
||||||
|
const clamped = Math.max(0, targetTop);
|
||||||
|
body.scrollTo({ top: clamped, behavior: 'smooth' });
|
||||||
|
await this.waitForScrollSettled(body, clamped);
|
||||||
|
this.latestMemoryScroll = Date.now();
|
||||||
|
if (waitMs > 0) {
|
||||||
|
await sleep(waitMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureMemoryTypingVisible(card: HTMLElement | null) {
|
||||||
|
if (!card) return;
|
||||||
|
const body = this.elements.memoryList;
|
||||||
|
if (!body) return;
|
||||||
|
const bodyTop = body.scrollTop;
|
||||||
|
const bodyBottom = bodyTop + body.clientHeight;
|
||||||
|
const cardTop = card.offsetTop;
|
||||||
|
const cardBottom = cardTop + card.offsetHeight;
|
||||||
|
// 如果底部被遮挡,向下滚动到露出多 12px 缓冲
|
||||||
|
if (cardBottom > bodyBottom - 4) {
|
||||||
|
const delta = cardBottom - bodyBottom + 12;
|
||||||
|
body.scrollTop = bodyTop + delta;
|
||||||
|
this.latestMemoryScroll = Date.now();
|
||||||
|
} else if (cardTop < bodyTop) {
|
||||||
|
// 若顶部超出,滚回顶部
|
||||||
|
body.scrollTop = Math.max(0, cardTop - 8);
|
||||||
|
this.latestMemoryScroll = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private highlightMemoryCard(card: HTMLElement, active = true) {
|
||||||
|
if (!card) return;
|
||||||
|
card.classList.toggle('editing', !!active);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateMemoryMeta() {
|
||||||
|
if (!this.elements.memoryList) return;
|
||||||
|
const count = this.elements.memoryList.children.length;
|
||||||
|
if (this.elements.memoryCount) {
|
||||||
|
this.elements.memoryCount.textContent = String(count);
|
||||||
|
}
|
||||||
|
if (this.elements.memoryStatus) {
|
||||||
|
this.elements.memoryStatus.textContent = '记忆已同步';
|
||||||
|
}
|
||||||
|
if (this.elements.memoryTime) {
|
||||||
|
this.elements.memoryTime.textContent = new Date().toLocaleTimeString('zh-CN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createMemoryCard(text: string) {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'memory-item new';
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.className = 'memory-text';
|
||||||
|
body.textContent = text;
|
||||||
|
item.appendChild(body);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async typeIntoMemoryCard(card: HTMLElement, text: string, options: { clearFirst?: boolean } = {}) {
|
||||||
|
if (!card) return;
|
||||||
|
const body = card.querySelector('.memory-text') as HTMLElement | null;
|
||||||
|
if (!body) return;
|
||||||
|
if (options.clearFirst) {
|
||||||
|
const existing = body.textContent || '';
|
||||||
|
for (let i = existing.length; i > 0; i--) {
|
||||||
|
body.textContent = existing.slice(0, i - 1);
|
||||||
|
await sleep(18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const chars = Array.from(text);
|
||||||
|
let idx = 0;
|
||||||
|
for (const ch of chars) {
|
||||||
|
idx += 1;
|
||||||
|
body.textContent = (body.textContent || '') + ch;
|
||||||
|
if (idx % 4 === 0) {
|
||||||
|
this.ensureMemoryTypingVisible(card);
|
||||||
|
}
|
||||||
|
await sleep(18);
|
||||||
|
}
|
||||||
|
// 最终再校准一次,确保结尾可见
|
||||||
|
this.ensureMemoryTypingVisible(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async animateMemoryAppend(text: string) {
|
||||||
|
if (!this.elements.memoryList) return;
|
||||||
|
await this.scrollMemoryToBottom();
|
||||||
|
const card = this.createMemoryCard('');
|
||||||
|
this.elements.memoryList.appendChild(card);
|
||||||
|
requestAnimationFrame(() => card.classList.add('visible'));
|
||||||
|
await sleep(160);
|
||||||
|
await this.scrollMemoryToBottom();
|
||||||
|
await this.movePointerToElement(card, { duration: 420 });
|
||||||
|
await this.click();
|
||||||
|
this.highlightMemoryCard(card, true);
|
||||||
|
await this.typeIntoMemoryCard(card, text, { clearFirst: true });
|
||||||
|
// 若内容超出单行,确保滚动至卡片底部
|
||||||
|
await this.scrollMemoryItemIntoView(card);
|
||||||
|
this.highlightMemoryCard(card, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async animateMemoryReplace(index: number, text: string) {
|
||||||
|
const card = this.getMemoryItemByIndex(index);
|
||||||
|
if (!card) {
|
||||||
|
await this.animateMemoryAppend(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.scrollMemoryItemIntoView(card);
|
||||||
|
await this.movePointerToElement(card, { duration: 420 });
|
||||||
|
await this.click();
|
||||||
|
this.highlightMemoryCard(card, true);
|
||||||
|
await this.typeIntoMemoryCard(card, text, { clearFirst: true });
|
||||||
|
await this.scrollMemoryItemIntoView(card);
|
||||||
|
this.highlightMemoryCard(card, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async animateMemoryDelete(index: number) {
|
||||||
|
const card = this.getMemoryItemByIndex(index);
|
||||||
|
if (!card || !this.elements.memoryList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.scrollMemoryItemIntoView(card);
|
||||||
|
await this.movePointerToElement(card, { duration: 360 });
|
||||||
|
await this.click();
|
||||||
|
this.highlightMemoryCard(card, true);
|
||||||
|
card.classList.add('swipe-out');
|
||||||
|
await this.movePointerToElement(card, { offsetX: card.clientWidth * 0.45, duration: 240 });
|
||||||
|
await sleep(200);
|
||||||
|
card.remove();
|
||||||
|
this.highlightMemoryCard(card, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadMemoryEntries(memoryType = 'main') {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/memory?type=${encodeURIComponent(memoryType)}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
if (!data?.success || !Array.isArray(data.entries)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.renderMemoryEntries(data.entries);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('loadMemoryEntries failed', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderMemoryEntries(entries: string[]) {
|
||||||
|
if (!this.elements.memoryList) return;
|
||||||
|
this.elements.memoryList.innerHTML = '';
|
||||||
|
entries.forEach(text => {
|
||||||
|
const card = this.createMemoryCard(text);
|
||||||
|
card.classList.add('visible');
|
||||||
|
this.elements.memoryList.appendChild(card);
|
||||||
|
});
|
||||||
|
this.updateMemoryMeta();
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
@ -2509,12 +2747,25 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
|
|
||||||
this.sceneHandlers.memoryUpdate = async (payload, runtime) => {
|
this.sceneHandlers.memoryUpdate = async (payload, runtime) => {
|
||||||
this.applySceneStatus(runtime, 'memoryUpdate', '正在同步记忆');
|
this.applySceneStatus(runtime, 'memoryUpdate', '正在同步记忆');
|
||||||
await this.movePointerToApp('memory');
|
const initialEntries = this.extractMemorySnapshotEntries(payload, 'before');
|
||||||
await this.click({ count: 2 });
|
const memoryType =
|
||||||
this.showWindow(this.elements.memoryWindow);
|
(payload?.arguments?.memory_type || payload?.result?.memory_type || 'main').toString().toLowerCase();
|
||||||
const content = payload?.arguments?.content || payload?.arguments?.memory || payload?.result?.content || '记忆内容';
|
await this.ensureMemoryWindowVisible({ initialEntries, memoryType });
|
||||||
this.addMemoryCard(content);
|
|
||||||
await sleep(500);
|
const op = (payload?.arguments?.operation || payload?.result?.operation || 'append').toLowerCase();
|
||||||
|
const content = payload?.arguments?.content || payload?.result?.content || '';
|
||||||
|
const index = Number(payload?.arguments?.index || payload?.result?.index || 0) || 0;
|
||||||
|
|
||||||
|
if (op === 'replace') {
|
||||||
|
await this.animateMemoryReplace(index, content || '');
|
||||||
|
} else if (op === 'delete') {
|
||||||
|
await this.animateMemoryDelete(index);
|
||||||
|
} else {
|
||||||
|
await this.animateMemoryAppend(content || '新记忆');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateMemoryMeta();
|
||||||
|
await sleep(360);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sceneHandlers.todoCreate = async (payload, runtime) => {
|
this.sceneHandlers.todoCreate = async (payload, runtime) => {
|
||||||
@ -4732,22 +4983,10 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private addMemoryCard(text: string) {
|
private addMemoryCard(text: string) {
|
||||||
const item = document.createElement('div');
|
const card = this.createMemoryCard(text);
|
||||||
item.className = 'memory-item new';
|
this.elements.memoryList.appendChild(card);
|
||||||
const tag = document.createElement('div');
|
requestAnimationFrame(() => card.classList.add('visible'));
|
||||||
tag.className = 'memory-tag';
|
this.updateMemoryMeta();
|
||||||
tag.textContent = 'MEM';
|
|
||||||
const body = document.createElement('div');
|
|
||||||
body.className = 'memory-text';
|
|
||||||
body.textContent = text;
|
|
||||||
item.appendChild(tag);
|
|
||||||
item.appendChild(body);
|
|
||||||
this.elements.memoryList.appendChild(item);
|
|
||||||
requestAnimationFrame(() => item.classList.add('visible'));
|
|
||||||
const count = this.elements.memoryList.children.length;
|
|
||||||
this.elements.memoryCount.textContent = String(count);
|
|
||||||
this.elements.memoryStatus.textContent = '记忆已同步';
|
|
||||||
this.elements.memoryTime.textContent = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private pushTodoCard(column: 'backlog' | 'doing' | 'done', title: string) {
|
private pushTodoCard(column: 'backlog' | 'doing' | 'done', title: string) {
|
||||||
|
|||||||
@ -193,6 +193,19 @@
|
|||||||
max-height: calc(100% - 36px);
|
max-height: calc(100% - 36px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 隐藏窗口内所有滚动条(横向与纵向),仍保留滚动能力 */
|
||||||
|
.virtual-monitor-surface .window,
|
||||||
|
.virtual-monitor-surface .window * {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
.virtual-monitor-surface .window::-webkit-scrollbar,
|
||||||
|
.virtual-monitor-surface .window *::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.virtual-monitor-surface .monitor-screen.manual-interactive .window-header {
|
.virtual-monitor-surface .monitor-screen.manual-interactive .window-header {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -1056,28 +1069,54 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.virtual-monitor-surface .memory-window {
|
.virtual-monitor-surface .memory-window {
|
||||||
background: #fff9f2;
|
background: rgba(244, 246, 251, 0.98);
|
||||||
border: 1px solid rgba(189, 93, 58, 0.3);
|
border: 1px solid rgba(15, 23, 42, 0.12);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-monitor-surface .memory-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.virtual-monitor-surface .memory-list {
|
.virtual-monitor-surface .memory-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
height: auto;
|
||||||
|
max-height: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.virtual-monitor-surface .memory-item {
|
.virtual-monitor-surface .memory-item {
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid rgba(189, 93, 58, 0.2);
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
box-shadow: 0 8px 20px rgba(83, 41, 27, 0.12);
|
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-height: 44px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(8px);
|
transform: translateY(8px);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.32s ease;
|
||||||
|
}
|
||||||
|
.virtual-monitor-surface .memory-text {
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.virtual-monitor-surface .memory-item.visible {
|
.virtual-monitor-surface .memory-item.visible {
|
||||||
@ -1085,14 +1124,19 @@
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.virtual-monitor-surface .memory-tag {
|
.virtual-monitor-surface .memory-item.editing {
|
||||||
font-size: 12px;
|
box-shadow: 0 0 0 2px rgba(64, 99, 178, 0.25), 0 10px 26px rgba(15, 23, 42, 0.18);
|
||||||
letter-spacing: 0.1em;
|
transform: translateY(-1px) scale(1.01);
|
||||||
color: #a35a2d;
|
|
||||||
min-width: 70px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.virtual-monitor-surface .memory-item.swipe-out {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(140%);
|
||||||
|
transition: transform 0.24s ease, opacity 0.22s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* memory-tag removed to simplify UI */
|
||||||
|
|
||||||
.virtual-monitor-surface .todo-columns {
|
.virtual-monitor-surface .todo-columns {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
|||||||
@ -146,7 +146,9 @@ connection_users: Dict[str, str] = {}
|
|||||||
stop_flags: Dict[str, Dict[str, Any]] = {}
|
stop_flags: Dict[str, Dict[str, Any]] = {}
|
||||||
|
|
||||||
MONITOR_FILE_TOOLS = {'append_to_file', 'modify_file', 'write_file_diff'}
|
MONITOR_FILE_TOOLS = {'append_to_file', 'modify_file', 'write_file_diff'}
|
||||||
|
MONITOR_MEMORY_TOOLS = {'update_memory'}
|
||||||
MONITOR_SNAPSHOT_CHAR_LIMIT = 60000
|
MONITOR_SNAPSHOT_CHAR_LIMIT = 60000
|
||||||
|
MONITOR_MEMORY_ENTRY_LIMIT = 256
|
||||||
RATE_LIMIT_BUCKETS: Dict[str, deque] = defaultdict(deque)
|
RATE_LIMIT_BUCKETS: Dict[str, deque] = defaultdict(deque)
|
||||||
FAILURE_TRACKERS: Dict[str, Dict[str, float]] = {}
|
FAILURE_TRACKERS: Dict[str, Dict[str, float]] = {}
|
||||||
pending_socket_tokens: Dict[str, Dict[str, Any]] = {}
|
pending_socket_tokens: Dict[str, Dict[str, Any]] = {}
|
||||||
@ -1617,6 +1619,21 @@ def gui_text_entry(terminal: WebTerminal, workspace: UserWorkspace, username: st
|
|||||||
return jsonify({"success": False, "error": str(exc)}), 400
|
return jsonify({"success": False, "error": str(exc)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/memory', methods=['GET'])
|
||||||
|
@api_login_required
|
||||||
|
@with_terminal
|
||||||
|
def api_memory_entries(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
|
"""返回主/任务记忆条目列表,供虚拟显示器加载"""
|
||||||
|
memory_type = request.args.get('type', 'main')
|
||||||
|
if memory_type not in ('main', 'task'):
|
||||||
|
return jsonify({"success": False, "error": "type 必须是 main 或 task"}), 400
|
||||||
|
try:
|
||||||
|
entries = terminal.memory_manager._read_entries(memory_type) # type: ignore
|
||||||
|
return jsonify({"success": True, "type": memory_type, "entries": entries})
|
||||||
|
except Exception as exc:
|
||||||
|
return jsonify({"success": False, "error": str(exc)}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/gui/monitor_snapshot', methods=['GET'])
|
@app.route('/api/gui/monitor_snapshot', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def get_monitor_snapshot_api():
|
def get_monitor_snapshot_api():
|
||||||
@ -2791,6 +2808,12 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
|
|||||||
return trimmed
|
return trimmed
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_monitor_memory(entries: Any) -> Optional[List[str]]:
|
||||||
|
if isinstance(entries, list):
|
||||||
|
return [str(item) for item in entries][:MONITOR_MEMORY_ENTRY_LIMIT]
|
||||||
|
return None
|
||||||
|
|
||||||
def capture_monitor_snapshot(path: Optional[str]) -> Optional[Dict[str, Any]]:
|
def capture_monitor_snapshot(path: Optional[str]) -> Optional[Dict[str, Any]]:
|
||||||
if not path:
|
if not path:
|
||||||
return None
|
return None
|
||||||
@ -4274,11 +4297,25 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
|
|||||||
tool_display_id = f"tool_{iteration}_{function_name}_{time.time()}"
|
tool_display_id = f"tool_{iteration}_{function_name}_{time.time()}"
|
||||||
monitor_snapshot = None
|
monitor_snapshot = None
|
||||||
snapshot_path = None
|
snapshot_path = None
|
||||||
|
memory_snapshot_type = None
|
||||||
if function_name in MONITOR_FILE_TOOLS:
|
if function_name in MONITOR_FILE_TOOLS:
|
||||||
snapshot_path = resolve_monitor_path(arguments)
|
snapshot_path = resolve_monitor_path(arguments)
|
||||||
monitor_snapshot = capture_monitor_snapshot(snapshot_path)
|
monitor_snapshot = capture_monitor_snapshot(snapshot_path)
|
||||||
if monitor_snapshot:
|
if monitor_snapshot:
|
||||||
cache_monitor_snapshot(tool_display_id, 'before', monitor_snapshot)
|
cache_monitor_snapshot(tool_display_id, 'before', monitor_snapshot)
|
||||||
|
elif function_name in MONITOR_MEMORY_TOOLS:
|
||||||
|
memory_snapshot_type = (arguments.get('memory_type') or 'main').lower()
|
||||||
|
before_entries = None
|
||||||
|
try:
|
||||||
|
before_entries = resolve_monitor_memory(web_terminal.memory_manager._read_entries(memory_snapshot_type))
|
||||||
|
except Exception as exc:
|
||||||
|
debug_log(f"[MonitorSnapshot] 读取记忆失败: {memory_snapshot_type} ({exc})")
|
||||||
|
if before_entries is not None:
|
||||||
|
monitor_snapshot = {
|
||||||
|
'memory_type': memory_snapshot_type,
|
||||||
|
'entries': before_entries
|
||||||
|
}
|
||||||
|
cache_monitor_snapshot(tool_display_id, 'before', monitor_snapshot)
|
||||||
|
|
||||||
sender('tool_start', {
|
sender('tool_start', {
|
||||||
'id': tool_display_id,
|
'id': tool_display_id,
|
||||||
@ -4344,6 +4381,23 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
|
|||||||
if not result_path:
|
if not result_path:
|
||||||
result_path = resolve_monitor_path(arguments, snapshot_path) or snapshot_path
|
result_path = resolve_monitor_path(arguments, snapshot_path) or snapshot_path
|
||||||
monitor_snapshot_after = capture_monitor_snapshot(result_path)
|
monitor_snapshot_after = capture_monitor_snapshot(result_path)
|
||||||
|
elif function_name in MONITOR_MEMORY_TOOLS:
|
||||||
|
memory_after_type = str(
|
||||||
|
arguments.get('memory_type')
|
||||||
|
or (isinstance(result_data, dict) and result_data.get('memory_type'))
|
||||||
|
or memory_snapshot_type
|
||||||
|
or 'main'
|
||||||
|
).lower()
|
||||||
|
after_entries = None
|
||||||
|
try:
|
||||||
|
after_entries = resolve_monitor_memory(web_terminal.memory_manager._read_entries(memory_after_type))
|
||||||
|
except Exception as exc:
|
||||||
|
debug_log(f"[MonitorSnapshot] 读取记忆失败(after): {memory_after_type} ({exc})")
|
||||||
|
if after_entries is not None:
|
||||||
|
monitor_snapshot_after = {
|
||||||
|
'memory_type': memory_after_type,
|
||||||
|
'entries': after_entries
|
||||||
|
}
|
||||||
|
|
||||||
update_payload = {
|
update_payload = {
|
||||||
'id': tool_display_id,
|
'id': tool_display_id,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user