继续收敛活动回合读取错误
保留 Direct 活动回合快照 IPC 失败正文 补充窗口标题栏显示和回归测试
This commit is contained in:
@@ -178,6 +178,7 @@ export function WindowChrome({ children }: WindowChromeProps) {
|
||||
activeTurns={activeProjectRuns.activeTurns}
|
||||
currentProjectPath={activeProjectRuns.currentProjectPath}
|
||||
readFailed={activeProjectRuns.readFailed}
|
||||
readError={activeProjectRuns.readError}
|
||||
onOpenProject={activeProjectRuns.onOpenProject}
|
||||
placement="titlebar"
|
||||
/>
|
||||
|
||||
@@ -19,6 +19,7 @@ export type WindowChromeActiveProjectRuns = {
|
||||
activeTurns: GameCreatorDirectActiveTurn[];
|
||||
currentProjectPath?: string | null;
|
||||
readFailed?: boolean;
|
||||
readError?: string;
|
||||
onOpenProject?: (projectPath: string) => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { GameCreatorDirectActiveTurn, TauriInvoke } from '../../app/types';
|
||||
import { visibleClientErrorMessage } from '../../services/errorReporting';
|
||||
import {
|
||||
canSubscribeTauriEvents,
|
||||
subscribeTauriEvent,
|
||||
@@ -35,6 +36,7 @@ export function useDirectActiveTurns({
|
||||
[],
|
||||
);
|
||||
const [snapshotReadFailed, setSnapshotReadFailed] = useState(false);
|
||||
const [snapshotReadError, setSnapshotReadError] = useState('');
|
||||
const mountedRef = useRef(true);
|
||||
const inFlightRef = useRef<Promise<void> | null>(null);
|
||||
/**
|
||||
@@ -93,10 +95,12 @@ export function useDirectActiveTurns({
|
||||
setActiveTurns(nextTurns);
|
||||
}
|
||||
setSnapshotReadFailed(false);
|
||||
setSnapshotReadError('');
|
||||
inFlightRef.current = null;
|
||||
return;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (!isCurrent()) return;
|
||||
setSnapshotReadError(visibleClientErrorMessage(error));
|
||||
if (attempt < DIRECT_ACTIVE_TURNS_READ_ATTEMPTS) {
|
||||
await new Promise<void>((resolve) => {
|
||||
retryTimerRef.current = window.setTimeout(() => {
|
||||
@@ -132,6 +136,7 @@ export function useDirectActiveTurns({
|
||||
// 空态也要保持引用稳定:已经空了就不要再换一个新数组。
|
||||
setActiveTurns((current) => (current.length === 0 ? current : []));
|
||||
setSnapshotReadFailed((current) => (current ? false : current));
|
||||
setSnapshotReadError((current) => (current ? '' : current));
|
||||
return;
|
||||
}
|
||||
let disposed = false;
|
||||
@@ -172,5 +177,10 @@ export function useDirectActiveTurns({
|
||||
};
|
||||
}, [enabled, invoke, refreshActiveTurns]);
|
||||
|
||||
return { activeTurns, refreshActiveTurns, snapshotReadFailed };
|
||||
return {
|
||||
activeTurns,
|
||||
refreshActiveTurns,
|
||||
snapshotReadFailed,
|
||||
snapshotReadError,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export type ActiveProjectRunsPanelProps = {
|
||||
activeTurns: GameCreatorDirectActiveTurn[];
|
||||
currentProjectPath?: string | null;
|
||||
readFailed?: boolean;
|
||||
readError?: string;
|
||||
onOpenProject?: (projectPath: string) => void;
|
||||
placement?: 'panel' | 'titlebar';
|
||||
};
|
||||
@@ -59,6 +60,7 @@ export function ActiveProjectRunsPanel({
|
||||
activeTurns,
|
||||
currentProjectPath = null,
|
||||
readFailed = false,
|
||||
readError = '',
|
||||
onOpenProject,
|
||||
placement = 'panel',
|
||||
}: ActiveProjectRunsPanelProps) {
|
||||
@@ -97,7 +99,7 @@ export function ActiveProjectRunsPanel({
|
||||
className="launcher-runs-titlebar launcher-runs-titlebar--error"
|
||||
role="status"
|
||||
>
|
||||
正在运行的项目读取失败
|
||||
{readError || '正在运行的项目读取失败'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -105,7 +107,7 @@ export function ActiveProjectRunsPanel({
|
||||
return (
|
||||
<aside className="launcher-runs-panel" aria-label="正在运行的项目">
|
||||
<span className="launcher-runs-panel-note" role="status">
|
||||
未能读取正在运行的项目
|
||||
{readError || '未能读取正在运行的项目'}
|
||||
</span>
|
||||
</aside>
|
||||
);
|
||||
@@ -166,10 +168,10 @@ export function ActiveProjectRunsPanel({
|
||||
const elapsed = formatActiveTurnElapsed(turn.startedAt, now);
|
||||
const isCurrent = Boolean(
|
||||
currentProjectPath &&
|
||||
projectPathsMatchForInvalidation(
|
||||
turn.projectPath,
|
||||
currentProjectPath,
|
||||
),
|
||||
projectPathsMatchForInvalidation(
|
||||
turn.projectPath,
|
||||
currentProjectPath,
|
||||
),
|
||||
);
|
||||
return (
|
||||
<li key={`${turn.projectPath}:${turn.turnId}`}>
|
||||
@@ -215,10 +217,10 @@ export function ActiveProjectRunsPanel({
|
||||
const elapsed = formatActiveTurnElapsed(turn.startedAt, now);
|
||||
const isCurrent = Boolean(
|
||||
currentProjectPath &&
|
||||
projectPathsMatchForInvalidation(
|
||||
turn.projectPath,
|
||||
currentProjectPath,
|
||||
),
|
||||
projectPathsMatchForInvalidation(
|
||||
turn.projectPath,
|
||||
currentProjectPath,
|
||||
),
|
||||
);
|
||||
return (
|
||||
<li key={`${turn.projectPath}:${turn.turnId}`}>
|
||||
|
||||
@@ -174,10 +174,11 @@ export function WorkspaceLauncherShell({
|
||||
: null,
|
||||
);
|
||||
const directInvoke = resolveTauriInvoke();
|
||||
const { activeTurns, snapshotReadFailed } = useDirectActiveTurns({
|
||||
invoke: directInvoke,
|
||||
enabled: true,
|
||||
});
|
||||
const { activeTurns, snapshotReadFailed, snapshotReadError } =
|
||||
useDirectActiveTurns({
|
||||
invoke: directInvoke,
|
||||
enabled: true,
|
||||
});
|
||||
const switchedToGameRuntime =
|
||||
gameRuntimeSwitch !== null &&
|
||||
currentProjectContext !== null &&
|
||||
@@ -304,6 +305,7 @@ export function WorkspaceLauncherShell({
|
||||
activeTurns,
|
||||
currentProjectPath: currentProjectContext?.projectPath ?? null,
|
||||
readFailed: snapshotReadFailed,
|
||||
readError: snapshotReadError,
|
||||
onOpenProject: (projectPath: string) =>
|
||||
openActiveProjectRef.current(projectPath),
|
||||
});
|
||||
@@ -312,6 +314,7 @@ export function WorkspaceLauncherShell({
|
||||
currentProjectContext?.projectPath,
|
||||
setActiveProjectRuns,
|
||||
snapshotReadFailed,
|
||||
snapshotReadError,
|
||||
]);
|
||||
useEffect(() => () => setActiveProjectRuns(null), [setActiveProjectRuns]);
|
||||
|
||||
|
||||
@@ -152,6 +152,9 @@ describe('useDirectActiveTurns', () => {
|
||||
});
|
||||
expect(result.current.activeTurns).toBe(firstSnapshot);
|
||||
expect(result.current.snapshotReadFailed).toBe(true);
|
||||
expect(result.current.snapshotReadError).toContain(
|
||||
'snapshot unavailable',
|
||||
);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
|
||||
@@ -135,6 +135,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接
|
||||
- 模板库权限探测和 Direct 线程订阅/consume/bootstrap 属于用户可见的前端状态边界:权限/IPC 失败可以关闭入口或结束订阅,但必须同时保留安全错误正文;禁止用空 catch、只 settle 锚点或清空状态来吞掉认证、网络和 IPC 原因。
|
||||
- UI 编辑器剪贴板、文件选择、预览等原生 IPC 操作的失败提示也必须经过 `visibleClientErrorMessage`;操作指引可以保留,但不能覆盖原生错误正文。
|
||||
- `agc_web_search` 的上游非 2xx 也属于用户可见的受控工具失败:MCP 结果必须保留 HTTP 状态与有界脱敏正文,不能只返回状态码。
|
||||
- 跨项目活动回合快照读取失败即使保留旧快照,也必须把最近一次安全正文一并呈现;“读不到不等于没有在跑”不能变成“读不到所以没有原因”。
|
||||
|
||||
## 后果与边界
|
||||
|
||||
|
||||
@@ -56,3 +56,4 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时,
|
||||
- 前端继续发现两个静默出口:模板库权限检查失败会直接清空入口,Direct 线程订阅/consume/bootstrap 的 IPC 失败会只 settle anchor 而不显示正文;现分别保留权限检查错误,并把订阅链错误正文送入项目状态栏/运行错误出口。认证失败回归已验证显示 `authentication-required`,不再变成泛化“执行失败,请稍后重试”。
|
||||
- UI 编辑器复制路径的原生剪贴板 IPC 失败也曾只显示“复制失败,请手动复制路径”,现保留脱敏后的剪贴板错误正文并同时给出手动复制指引。
|
||||
- 受控联网搜索的上游非 2xx 响应原来只返回 HTTP 状态;现保留有界、脱敏的服务端正文,错误码/限流原因不再丢失。
|
||||
- 窗口标题栏的 Direct 活动回合快照读取失败原来只给“读取失败”标志并保留旧快照;现额外保留脱敏后的 IPC/读取正文,面板不会把真实原因吞掉。
|
||||
|
||||
Reference in New Issue
Block a user