优化策划工作区后台刷新体验
Project CI / Repository checks (pull_request) Successful in 2m36s
Project CI / Frontend tests (pull_request) Successful in 3m27s
Project CI / Native shell tests (pull_request) Successful in 20m41s
Project CI / Backend tests (pull_request) Successful in 22m53s

合并 Agent 工具事件并避免刷新时清空文件树

仅首次打开和手动刷新显示加载状态
This commit is contained in:
2026-09-11 05:28:05 +00:00
parent 26169102a0
commit 8444dee9e9
@@ -7,7 +7,7 @@ import {
Folder,
RefreshCw,
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { resolveTauriInvoke } from '../../app/tauri';
import type { DesignView, DesignWorkspaceEntry } from '../../app/types';
@@ -181,61 +181,78 @@ export function DesignWorkspacePanel({
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(
() => new Set(),
);
const refreshTimerRef = useRef<number | null>(null);
const loadWorkspace = useCallback(async () => {
const invoke = resolveTauriInvoke();
if (!invoke || !projectPath.trim()) {
setLoading(false);
return;
}
setLoading(true);
setError('');
try {
const [nextView, nextFiles] = await Promise.all([
invoke<DesignView | null>('hydrate_design_agent_session', {
projectPath,
}),
invoke<DesignWorkspaceEntry[]>('list_design_workspace', {
projectPath,
}),
]);
setView(nextView);
setFiles(nextFiles);
setExpandedPaths((current) => {
const next = new Set(current);
if (current.size === 0) {
nextFiles
.filter((entry) => entry.kind === 'directory')
.forEach((entry) => next.add(entry.path));
} else {
nextFiles
.filter(
(entry) => entry.kind === 'directory' && next.has(entry.path),
)
.forEach((entry) => next.add(entry.path));
}
return next;
});
setSelectedPath((current) =>
current &&
nextFiles.some(
(entry) => entry.kind !== 'directory' && entry.path === current,
)
? current
: null,
);
} catch (nextError) {
setError(
nextError instanceof Error ? nextError.message : String(nextError),
);
setFiles([]);
} finally {
setLoading(false);
}
}, [projectPath]);
const loadWorkspace = useCallback(
async ({
showLoading,
hydrateSession,
}: {
showLoading: boolean;
hydrateSession: boolean;
}) => {
const invoke = resolveTauriInvoke();
if (!invoke || !projectPath.trim()) {
if (showLoading) setLoading(false);
return;
}
if (showLoading) {
setLoading(true);
setError('');
}
try {
const nextFilesPromise = invoke<DesignWorkspaceEntry[]>(
'list_design_workspace',
{ projectPath },
);
const nextViewPromise = hydrateSession
? invoke<DesignView | null>('hydrate_design_agent_session', {
projectPath,
})
: Promise.resolve(null);
const [nextFiles, nextView] = await Promise.all([
nextFilesPromise,
nextViewPromise,
]);
if (hydrateSession) setView(nextView);
setFiles(nextFiles);
setExpandedPaths((current) => {
const next = new Set(current);
if (current.size === 0) {
nextFiles
.filter((entry) => entry.kind === 'directory')
.forEach((entry) => next.add(entry.path));
} else {
nextFiles
.filter(
(entry) => entry.kind === 'directory' && next.has(entry.path),
)
.forEach((entry) => next.add(entry.path));
}
return next;
});
setSelectedPath((current) =>
current &&
nextFiles.some(
(entry) => entry.kind !== 'directory' && entry.path === current,
)
? current
: null,
);
} catch (nextError) {
setError(
nextError instanceof Error ? nextError.message : String(nextError),
);
if (showLoading) setFiles([]);
} finally {
if (showLoading) setLoading(false);
}
},
[projectPath],
);
useEffect(() => {
void loadWorkspace();
void loadWorkspace({ showLoading: true, hydrateSession: true });
}, [loadWorkspace]);
useEffect(() => {
@@ -245,11 +262,20 @@ export function DesignWorkspacePanel({
}
let disposed = false;
let cleanup: (() => void) | null = null;
const scheduleRefresh = () => {
if (refreshTimerRef.current !== null) {
window.clearTimeout(refreshTimerRef.current);
}
refreshTimerRef.current = window.setTimeout(() => {
refreshTimerRef.current = null;
void loadWorkspace({ showLoading: false, hydrateSession: false });
}, 250);
};
void listen<{ projectPath: string; kind: string }>(
'design-agent-update',
(event) => {
if (!disposed && event.payload.projectPath === projectPath) {
void loadWorkspace();
scheduleRefresh();
}
},
)
@@ -263,6 +289,10 @@ export function DesignWorkspacePanel({
.catch(() => undefined);
return () => {
disposed = true;
if (refreshTimerRef.current !== null) {
window.clearTimeout(refreshTimerRef.current);
refreshTimerRef.current = null;
}
cleanup?.();
};
}, [loadWorkspace, projectPath]);
@@ -316,7 +346,9 @@ export function DesignWorkspacePanel({
<button
type="button"
className="design-workspace-panel__refresh"
onClick={() => void loadWorkspace()}
onClick={() =>
void loadWorkspace({ showLoading: true, hydrateSession: true })
}
disabled={loading}
aria-label="刷新策划工作区"
>