fix(ui): load folder contents in monitor and improve messages

This commit is contained in:
JOJO 2025-12-15 22:34:25 +08:00
parent 3729cae4d2
commit c96e99cd13
4 changed files with 67 additions and 38 deletions

View File

@ -158,8 +158,8 @@ class FileManager:
if full_path.parent == self.project_path: if full_path.parent == self.project_path:
return { return {
"success": False, "success": False,
"error": "禁止在项目根目录直接创建文件,请先创建或选择子目录", "error": "禁止在项目根目录直接创建文件,请先创建或选择子目录(例如 ./data 或 ./docs",
"suggestion": "创建文件所属文件夹,在其中创建新文件。" "suggestion": "请先创建文件夹,再在该文件夹中创建新文件。"
} }
if self._use_container(): if self._use_container():
result = self._container_call("create_file", { result = self._container_call("create_file", {

View File

@ -96,7 +96,7 @@ type EditorOperation = {
const EDITOR_MAX_RENDER_LINES = 360; const EDITOR_MAX_RENDER_LINES = 360;
const EDITOR_DIFF_LIMIT = 2000; const EDITOR_DIFF_LIMIT = 2000;
const EDITOR_MAX_ANIMATION_STEPS = 4000; const EDITOR_MAX_ANIMATION_STEPS = 4000;
const EDITOR_TYPING_THRESHOLD = 4096; const EDITOR_TYPING_THRESHOLD = 180;
const EDITOR_TYPING_INTERVAL = 34; const EDITOR_TYPING_INTERVAL = 34;
const EDITOR_ERASE_INTERVAL = 26; const EDITOR_ERASE_INTERVAL = 26;
const MONITOR_EDITOR_DEBUG = false; const MONITOR_EDITOR_DEBUG = false;
@ -777,6 +777,30 @@ export class MonitorDirector implements MonitorDriver {
} }
} }
private async loadFolderEntries(folderKey: string) {
const path = folderKey || '';
try {
const resp = await fetch(`/api/gui/files/entries?path=${encodeURIComponent(path)}`, {
method: 'GET',
credentials: 'include',
headers: { Accept: 'application/json' }
});
const data = await resp.json().catch(() => null);
if (!data?.success) {
return;
}
const items = Array.isArray(data?.data?.items) ? data.data.items : [];
const entries: FolderEntry[] = items.map((item: any) => ({
name: item?.name || '',
path: item?.path || this.composePath([path, item?.name].filter(Boolean)),
type: item?.type === 'directory' ? 'folder' : 'file'
}));
this.folderEntries.set(path, entries);
} catch (error) {
console.warn('[MonitorDirector] loadFolderEntries failed', path, error);
}
}
private renderFolderEntries(folderKey: string, animate = true) { private renderFolderEntries(folderKey: string, animate = true) {
this.ensureFolderKey(folderKey); this.ensureFolderKey(folderKey);
const entries = this.folderEntries.get(folderKey) || []; const entries = this.folderEntries.get(folderKey) || [];
@ -840,7 +864,7 @@ export class MonitorDirector implements MonitorDriver {
await this.movePointerToElement(icon, { duration: 720 }); await this.movePointerToElement(icon, { duration: 720 });
await this.click({ count: 2 }); await this.click({ count: 2 });
await sleep(180); await sleep(180);
this.openFolder(folderName, folderName); await this.openFolder(folderName, folderName);
await sleep(60); await sleep(60);
} }
@ -869,7 +893,7 @@ export class MonitorDirector implements MonitorDriver {
return null; return null;
} }
this.upsertFolderEntry(parentKey, { name: filename, type: 'file' }, { animate: false }); this.upsertFolderEntry(parentKey, { name: filename, type: 'file' }, { animate: false });
this.openFolder(parentKey, parentKey); await this.openFolder(parentKey, parentKey);
await sleep(40); await sleep(40);
const entryPath = this.composePath([parentKey, filename]); const entryPath = this.composePath([parentKey, filename]);
const entryEl = this.findFolderEntryElement(entryPath); const entryEl = this.findFolderEntryElement(entryPath);
@ -935,7 +959,7 @@ export class MonitorDirector implements MonitorDriver {
if (matches) { if (matches) {
currentKey = this.composePath(activeSegments); currentKey = this.composePath(activeSegments);
startIndex = activeSegments.length; startIndex = activeSegments.length;
this.openFolder(currentKey, currentKey); await this.openFolder(currentKey, currentKey);
await sleep(40); await sleep(40);
} }
} }
@ -952,11 +976,11 @@ export class MonitorDirector implements MonitorDriver {
const segment = targetSegments[i]; const segment = targetSegments[i];
const nextKey = this.composePath([currentKey, segment]); const nextKey = this.composePath([currentKey, segment]);
this.upsertFolderEntry(currentKey, { name: segment, type: 'folder' }, { animate: false }); this.upsertFolderEntry(currentKey, { name: segment, type: 'folder' }, { animate: false });
this.openFolder(currentKey, currentKey); await this.openFolder(currentKey, currentKey);
await sleep(40); await sleep(40);
await this.doubleClickFolderEntry(nextKey); await this.doubleClickFolderEntry(nextKey);
currentKey = nextKey; currentKey = nextKey;
this.openFolder(currentKey, currentKey); await this.openFolder(currentKey, currentKey);
await sleep(60); await sleep(60);
} }
return currentKey; return currentKey;
@ -1367,28 +1391,22 @@ export class MonitorDirector implements MonitorDriver {
* *
* *
*/ */
private async typeSmartText(target: HTMLElement, text: string) { private async typeSmartText(target: HTMLElement, text: string, forceLineMode = false) {
const lines = text.split('\n'); const lines = text.split('\n');
const long = lines.length > 8 || text.length > 400; const long = forceLineMode || lines.length > 2 || text.length > 120;
const lineDelay = 55; const lineDelay = 32;
if (long) { if (long) {
target.textContent = ''; target.textContent = '';
lines.forEach((line, idx) => { for (let i = 0; i < lines.length; i += 1) {
target.textContent += (idx > 0 ? '\n' : '') + line; target.textContent += (i > 0 ? '\n' : '') + lines[i];
}); await sleep(Math.max(12, lineDelay - Math.min(i, 6) * 3));
// 行级动画:逐行显现
const chunks = target.textContent.split('\n');
target.textContent = '';
for (let i = 0; i < chunks.length; i += 1) {
target.textContent += (i > 0 ? '\n' : '') + chunks[i];
await sleep(lineDelay);
} }
return; return;
} }
const chars = Array.from(text); const chars = Array.from(text);
for (const ch of chars) { for (const ch of chars) {
target.textContent = (target.textContent || '') + ch; target.textContent = (target.textContent || '') + ch;
await sleep(26); await sleep(18);
} }
} }
@ -1781,6 +1799,16 @@ export class MonitorDirector implements MonitorDriver {
this.renderTerminalHistory(sessionId); this.renderTerminalHistory(sessionId);
const { bodyEl } = this.getTerminalInstance(); const { bodyEl } = this.getTerminalInstance();
const promptEl = bodyEl.lastElementChild as HTMLElement | null; const promptEl = bodyEl.lastElementChild as HTMLElement | null;
const long = command.length > 80 || command.split('\n').length > 1;
if (long) {
prompt.text = `${command}`;
if (promptEl) {
promptEl.textContent = prompt.text;
this.scrollPromptIntoView(this.getTerminalInstance());
}
await sleep(50);
return;
}
let charIndex = 0; let charIndex = 0;
const chars = command.split(''); const chars = command.split('');
for (const ch of chars) { for (const ch of chars) {
@ -1792,7 +1820,7 @@ export class MonitorDirector implements MonitorDriver {
if (promptEl && (charIndex % 6 === 0 || charIndex === chars.length)) { if (promptEl && (charIndex % 6 === 0 || charIndex === chars.length)) {
this.scrollPromptIntoView(this.getTerminalInstance()); this.scrollPromptIntoView(this.getTerminalInstance());
} }
await sleep(46); await sleep(32);
} }
} }
@ -2381,7 +2409,7 @@ export class MonitorDirector implements MonitorDriver {
const existing = (this.folderEntries.get(parentKey) || []).find(item => item.name === fromName); const existing = (this.folderEntries.get(parentKey) || []).find(item => item.name === fromName);
const entryType = existing?.type || 'file'; const entryType = existing?.type || 'file';
this.upsertFolderEntry(parentKey, { name: fromName, type: entryType }, { animate: false }); this.upsertFolderEntry(parentKey, { name: fromName, type: entryType }, { animate: false });
this.openFolder(parentKey, parentKey); await this.openFolder(parentKey, parentKey);
await sleep(40); await sleep(40);
const entryPath = this.composePath([parentKey, fromName]); const entryPath = this.composePath([parentKey, fromName]);
const entryEl = this.findFolderEntryElement(entryPath); const entryEl = this.findFolderEntryElement(entryPath);
@ -2436,7 +2464,7 @@ export class MonitorDirector implements MonitorDriver {
const existing = (this.folderEntries.get(parentKey) || []).find(item => item.name === name); const existing = (this.folderEntries.get(parentKey) || []).find(item => item.name === name);
const entryType = existing?.type || 'file'; const entryType = existing?.type || 'file';
this.upsertFolderEntry(parentKey, { name, type: entryType }, { animate: false }); this.upsertFolderEntry(parentKey, { name, type: entryType }, { animate: false });
this.openFolder(parentKey, parentKey); await this.openFolder(parentKey, parentKey);
await sleep(40); await sleep(40);
const entryPath = this.composePath([parentKey, name]); const entryPath = this.composePath([parentKey, name]);
const entryEl = this.findFolderEntryElement(entryPath); const entryEl = this.findFolderEntryElement(entryPath);
@ -2899,7 +2927,7 @@ export class MonitorDirector implements MonitorDriver {
}; };
this.sceneHandlers.terminalSession = async (payload, runtime) => { this.sceneHandlers.terminalSession = async (payload, runtime) => {
this.applySceneStatus(runtime, 'terminalSession', '正在查看终端'); this.applySceneStatus(runtime, 'terminalSession', '打开终端');
const action = (payload?.arguments?.action || payload?.action || '').toLowerCase(); const action = (payload?.arguments?.action || payload?.action || '').toLowerCase();
// 特殊处理:如果是关闭/切换终端,不要无意中新建会话 // 特殊处理:如果是关闭/切换终端,不要无意中新建会话
if (action === 'close' || action === 'switch') { if (action === 'close' || action === 'switch') {
@ -2969,7 +2997,7 @@ export class MonitorDirector implements MonitorDriver {
}; };
this.sceneHandlers.terminalInput = async (payload, runtime) => { this.sceneHandlers.terminalInput = async (payload, runtime) => {
this.applySceneStatus(runtime, 'terminalInput', '正在发送命令'); this.applySceneStatus(runtime, 'terminalInput', '调用 terminal_input');
const { sessionId } = await this.ensureTerminalSessionReady(payload, { focusPrompt: true, activate: true }); const { sessionId } = await this.ensureTerminalSessionReady(payload, { focusPrompt: true, activate: true });
const command = const command =
payload?.arguments?.command || payload?.arguments?.command ||
@ -3892,10 +3920,11 @@ export class MonitorDirector implements MonitorDriver {
return div; return div;
} }
private openFolder(folderKey: string, label?: string) { private async openFolder(folderKey: string, label?: string) {
if (!folderKey) { if (!folderKey) {
return; return;
} }
await this.loadFolderEntries(folderKey);
this.activeFolder = folderKey; this.activeFolder = folderKey;
this.showWindow(this.elements.folderWindow); this.showWindow(this.elements.folderWindow);
this.elements.folderHeaderText.textContent = label || folderKey || 'workspace'; this.elements.folderHeaderText.textContent = label || folderKey || 'workspace';

View File

@ -11,11 +11,11 @@ export const SCENE_PROGRESS_LABELS: Record<string, string> = {
focus: '正在聚焦', focus: '正在聚焦',
unfocus: '正在处理', unfocus: '正在处理',
// 运行类工具显示具体工具名,由运行时传入 // 运行类工具显示具体工具名,由运行时传入
runCommand: '', runCommand: '运行命令',
runPython: '', runPython: '运行 Python',
terminalSession: '正在连接终端', terminalSession: '打开终端',
terminalInput: '', terminalInput: '终端输入',
terminalSnapshot: '正在获取终端', terminalSnapshot: '获取终端输出',
memoryUpdate: '正在同步记忆', memoryUpdate: '正在同步记忆',
todoCreate: '正在更新待办', todoCreate: '正在更新待办',
todoUpdate: '正在更新待办', todoUpdate: '正在更新待办',
@ -28,11 +28,11 @@ export const SCENE_PROGRESS_LABELS: Record<string, string> = {
renameFile: '正在重命名', renameFile: '正在重命名',
terminalReset: '正在重置终端', terminalReset: '正在重置终端',
terminalSleep: '准备等待', terminalSleep: '准备等待',
terminalRun: '', terminalRun: '终端运行中',
ocr: '正在提取', ocr: '正在提取',
memory: '正在同步记忆', memory: '正在同步记忆',
todo: '正在管理待办', todo: '正在管理待办',
genericTool: '' genericTool: '调用工具'
}; };
export function getSceneProgressLabel(name: string): string | null { export function getSceneProgressLabel(name: string): string | null {

View File

@ -44,11 +44,11 @@ const RUNNING_STATUS_TEXTS: Record<string, string> = {
web_search: '正在搜索网络...', web_search: '正在搜索网络...',
extract_webpage: '正在提取网页...', extract_webpage: '正在提取网页...',
save_webpage: '正在保存网页...', save_webpage: '正在保存网页...',
run_python: '', run_python: '调用 run_python',
run_command: '', run_command: '调用 run_command',
update_memory: '正在更新记忆...', update_memory: '正在更新记忆...',
terminal_session: '正在管理终端会话...', terminal_session: '正在管理终端会话...',
terminal_input: '', terminal_input: '调用 terminal_input',
terminal_snapshot: '正在获取终端快照...', terminal_snapshot: '正在获取终端快照...',
terminal_reset: '正在重置终端...' terminal_reset: '正在重置终端...'
}; };
@ -178,7 +178,7 @@ export function getToolStatusText(tool: any): string {
tool.display_name || tool.display_name ||
tool.name || tool.name ||
''; '';
return label ? `调用 ${label}` : '调用工具中'; return label ? label : '调用工具中';
} }
if (tool.status === 'completed') { if (tool.status === 'completed') {
if (tool.name === 'read_file') { if (tool.name === 'read_file') {