隐藏首页模糊项目提示 #188

Merged
kdletters merged 7 commits from codex/hide-home-project-prompts into master 2026-08-25 10:47:20 +08:00
9 changed files with 526 additions and 98 deletions
+5 -1
View File
@@ -1243,6 +1243,9 @@ export function App({
const initialProjectOpenedRef = useRef(false);
const pendingUiConfirmationActionRef = useRef<(() => void) | null>(null);
const queueRunLocalShortcutRef = useRef<() => void>(() => undefined);
const executeRunLocalRef = useRef<(announceToChat: boolean) => void>(
() => undefined,
);
function requestRuntimeConfigOpen() {
if (projectSupervisorOnly && !supervisorChatOnly && !gameChatOnly) {
@@ -3283,6 +3286,7 @@ export function App({
]);
}
queueRunLocalShortcutRef.current = queueRunLocalShortcut;
executeRunLocalRef.current = executeRunLocal;
useEffect(() => {
const nextProjectPath = localProject?.projectPath;
@@ -3300,7 +3304,7 @@ export function App({
}
handledPlayRequestRef.current = requestKey;
onPlayRequestHandled?.(playRequest.requestId);
queueRunLocalShortcutRef.current();
void executeRunLocalRef.current(true);
}, [
localProject?.projectPath,
onPlayRequestHandled,
@@ -34,7 +34,7 @@ export function WorkspaceLauncherShell({
ProjectSupervisor,
}: WorkspaceLauncherShellProps) {
const accountWallet = useAccountWallet(currentUser.id);
const [status, setStatus] = useState('请选择项目');
const [status, setStatus] = useState('');
const recentProjects = useRecentProjects(setStatus);
const { recentProjectRows, rememberRecentWorkspace } = recentProjects;
const [launcherView, setLauncherView] = useState<LauncherView>(initialView);
@@ -273,7 +273,6 @@ export function WorkspaceLauncherShell({
{launcherView === 'home' ? (
<HomeView
hasPromo={launcherNotifications.length > 0}
status={status}
onStatusChange={setStatus}
recentProjectRows={recentProjectRows}
onCreateDraftAutomatically={createHomeDraftAutomatically}
@@ -1,4 +1,4 @@
import { Send } from 'lucide-react';
import { ArrowUp } from 'lucide-react';
import type {
ComponentProps,
Dispatch,
@@ -251,14 +251,7 @@ export function ProjectSupervisorView({
type="submit"
disabled={runtimePanelProps.controlBusy || needsUserInput}
>
<Send size={16} aria-hidden="true" />
<span>
{needsUserInput
? '等待回答'
: runtimePanelProps.controlBusy
? '思考中'
: '发送'}
</span>
<ArrowUp size={16} aria-hidden="true" />
</button>
</form>
<small className="project-supervisor-workspace-status">
+354 -5
View File
@@ -6879,6 +6879,7 @@ iframe.preview-frame {
align-content: start;
gap: 8px;
min-width: 0;
min-height: 156px;
padding: 12px;
border: 1px solid #eadbd4;
border-radius: 12px;
@@ -7022,12 +7023,260 @@ iframe.preview-frame {
font-size: 10px;
}
.game-workbench-chat-avatar {
width: 48px;
height: 48px;
.game-workbench-chat .supervisor-chat-only-runtime-controls {
max-width: none;
}
.game-workbench-chat
.supervisor-chat-only-runtime-controls
.pending-command {
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--platform-surface-border);
border-radius: 10px;
background: var(--platform-warm-bg);
}
.game-workbench-chat
.supervisor-chat-only-runtime-controls
.pending-command
> span {
gap: 4px;
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
color: var(--platform-text-strong);
font-size: 12px;
font-weight: 800;
}
.game-workbench-chat
.supervisor-chat-only-runtime-controls
.pending-command
> span
small {
color: var(--platform-text-soft);
font-weight: 500;
line-height: 1.4;
}
.game-workbench-chat .pending-command-actions button {
min-width: 52px;
height: 34px;
flex: 0 0 auto;
object-fit: contain;
filter: drop-shadow(0 5px 9px rgb(182 98 63 / 16%));
border: 1px solid var(--platform-surface-border);
background: var(--platform-button-secondary-fill);
color: var(--platform-button-secondary-text);
font-size: 11px;
font-weight: 800;
}
.game-workbench-chat .pending-command-actions button:last-child {
border-color: var(--platform-button-primary-border);
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
}
.agent-runtime-status .project-runtime-pending-command > span {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
color: var(--platform-text-strong);
font-size: 12px;
font-weight: 800;
}
.agent-runtime-status .project-runtime-pending-command > span small {
display: block;
margin-top: 4px;
color: var(--platform-text-soft);
font-weight: 500;
line-height: 1.4;
}
.agent-runtime-status .project-runtime-pending-command .pending-command-actions {
display: flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
}
.agent-runtime-status
.project-runtime-pending-command
.pending-command-actions
button:last-child {
border-color: var(--platform-button-primary-border);
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
}
.game-workbench-chat
.agent-runtime-status
.project-runtime-pending-command {
grid-template-columns: minmax(0, 1fr) 136px;
}
.game-workbench-chat
.agent-runtime-status
.project-runtime-pending-command
.pending-command-actions {
display: grid;
grid-template-columns: repeat(2, 64px);
gap: 8px;
width: 136px;
flex: 0 0 136px;
}
.game-workbench-chat
.agent-runtime-status
.project-runtime-pending-command
.pending-command-actions
button {
width: 64px;
min-width: 64px;
max-width: 64px;
padding: 0;
white-space: nowrap;
}
/* Runtime 确认卡在部分项目状态下位于状态包装层外,工作台样式直接绑定卡片自身,避免回落旧的蓝色按钮样式。 */
.game-workbench-chat .project-runtime-pending-command {
display: grid;
grid-template-columns: minmax(0, 1fr) 136px;
align-items: center;
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--platform-surface-border);
border-radius: 10px;
background: var(--platform-warm-bg);
}
.game-workbench-chat .project-supervisor-surface > .project-runtime-pending-command,
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command {
display: grid;
grid-template-columns: minmax(0, 1fr) 136px;
align-items: center;
gap: 12px;
width: 100%;
margin: 0;
padding: 10px 12px;
border: 1px solid var(--platform-surface-border);
border-radius: 10px;
background: var(--platform-warm-bg);
}
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command > span {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
color: var(--platform-text-strong);
font-size: 12px;
font-weight: 800;
}
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command > span small {
display: block;
margin-top: 4px;
color: var(--platform-text-soft);
font-size: 10px;
font-weight: 500;
line-height: 1.4;
}
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command .pending-command-actions {
display: grid;
grid-template-columns: repeat(2, 64px);
gap: 8px;
width: 136px;
flex: 0 0 136px;
}
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command .pending-command-actions button {
width: 64px;
min-width: 64px;
max-width: 64px;
height: 34px;
padding: 0;
border: 1px solid var(--platform-surface-border);
border-radius: 8px;
background: var(--platform-button-secondary-fill);
color: var(--platform-button-secondary-text);
font-size: 11px;
font-weight: 800;
white-space: nowrap;
}
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command .pending-command-actions button:last-child {
border-color: var(--platform-button-primary-border);
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
}
/* 该卡片的两个操作按钮禁止被全局 button 规则压缩,确保“拒绝 / 确认”完整可见。 */
.project-runtime-pending-command .pending-command-actions button {
display: inline-flex !important;
width: 64px !important;
min-width: 64px !important;
max-width: 64px !important;
flex: 0 0 64px !important;
align-items: center;
justify-content: center;
padding: 0 !important;
overflow: visible;
white-space: nowrap;
}
.game-workbench-chat .project-runtime-pending-command > span {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
color: var(--platform-text-strong);
font-size: 12px;
font-weight: 800;
}
.game-workbench-chat .project-runtime-pending-command > span small {
display: block;
margin-top: 4px;
color: var(--platform-text-soft);
font-size: 10px;
font-weight: 500;
line-height: 1.4;
}
.game-workbench-chat .project-runtime-pending-command .pending-command-actions {
display: grid;
grid-template-columns: repeat(2, 64px);
gap: 8px;
width: 136px;
flex: 0 0 136px;
}
.game-workbench-chat
.project-runtime-pending-command
.pending-command-actions
button {
width: 64px;
min-width: 64px;
max-width: 64px;
height: 34px;
padding: 0;
border: 1px solid var(--platform-surface-border);
border-radius: 8px;
background: var(--platform-button-secondary-fill);
color: var(--platform-button-secondary-text);
font-size: 11px;
font-weight: 800;
white-space: nowrap;
}
.game-workbench-chat
.project-runtime-pending-command
.pending-command-actions
button:last-child {
border-color: var(--platform-button-primary-border);
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
}
.game-workbench-chat .project-supervisor-surface {
@@ -7281,6 +7530,100 @@ iframe.preview-frame {
white-space: nowrap;
}
/* 右侧对话采用简洁的 Codex 式消息流与编辑框,减少卡片嵌套和视觉噪音。 */
.game-workbench-chat .project-supervisor-message-list {
position: absolute;
top: 0;
right: 0;
bottom: 104px;
left: 0;
gap: 34px;
height: auto;
padding: 18px 20px 24px;
scroll-padding-bottom: 24px;
border: 1px solid var(--platform-subpanel-border);
border-radius: 12px;
background: var(--platform-input-fill);
}
.game-workbench-chat .project-supervisor-message-list .message {
display: block;
width: 100%;
max-width: 100%;
padding: 4px 0;
border: 0;
border-radius: 0;
background: transparent;
color: var(--platform-text-base);
font-size: 14px;
line-height: 1.85;
}
.game-workbench-chat .project-supervisor-message-list .message + .message {
margin-top: 0;
}
.game-workbench-chat .project-supervisor-message-list .message--user {
width: fit-content;
max-width: min(82%, 640px);
margin-left: auto;
padding: 11px 16px;
border: 1px solid var(--platform-button-primary-border);
border-radius: 14px 14px 4px;
background: var(--platform-warm-bg);
color: var(--platform-text-strong);
}
.game-workbench-chat .project-supervisor-composer {
display: block;
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 5;
padding: 0;
pointer-events: none;
}
.game-workbench-chat .project-supervisor-composer textarea {
display: block;
width: 100%;
min-height: 88px;
max-height: 180px;
padding: 14px 48px 38px 15px;
border: 1px solid var(--platform-surface-border);
border-radius: 14px;
background: var(--platform-input-fill);
color: var(--platform-text-strong);
font-size: 13px;
line-height: 1.6;
resize: vertical;
pointer-events: auto;
}
.game-workbench-chat .project-supervisor-composer button {
position: absolute;
right: 10px;
bottom: 10px;
display: grid;
width: 30px;
min-width: 30px;
height: 30px;
min-height: 30px;
padding: 0;
border: 0;
border-radius: 9px;
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
place-items: center;
pointer-events: auto;
}
.game-workbench-chat .project-supervisor-composer button svg {
width: 14px;
height: 14px;
}
.game-agent-dock {
position: relative;
z-index: 30;
@@ -7691,6 +8034,7 @@ iframe.preview-frame {
.game-workbench-view-actions {
justify-content: flex-end;
}
}
@media (max-width: 760px) {
@@ -7721,6 +8065,11 @@ iframe.preview-frame {
flex-direction: column;
}
.game-workbench-view-actions .game-workbench-play-button {
position: static;
transform: none;
}
.game-workbench-tabs {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -93,7 +93,6 @@ function projectTypeLabel(projectKind: HomeProjectRow['projectKind']) {
type HomeViewProps = {
hasPromo: boolean;
status: string;
onStatusChange: (status: string) => void;
recentProjectRows: readonly HomeProjectRow[];
onCreateDraftAutomatically: (
@@ -107,7 +106,6 @@ type HomeViewProps = {
export default function HomeView({
hasPromo,
status,
onStatusChange,
recentProjectRows,
onCreateDraftAutomatically,
@@ -250,11 +248,6 @@ export default function HomeView({
</div>
</RichInputArea>
</form>
{status ? (
<span className="w-[min(814px,calc(100vw-122px))] text-left text-[12px] text-(--platform-text-soft) max-[760px]:w-[min(100%,calc(100vw-76px))]">
{status}
</span>
) : null}
</section>
<section
@@ -266,9 +259,6 @@ export default function HomeView({
<h2 className="m-0 text-[15px] text-(--platform-text-strong)">
</h2>
<p className="mb-0 mt-1 text-[12px] text-(--platform-text-soft)">
</p>
</div>
<div className="launcher-project-list-actions">
<button
@@ -45,7 +45,6 @@ import type {
GameIterationVersion,
ProjectResourceCanvasLayoutMode,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import PRODUCT_LOGO from '../../../../../packages/shared/src/icons/taonier-product-ip.png';
import { canonicalAssetBaseName } from '../../features/asset-canvas/assetCanvasNaming';
import {
type AssetCanvasCommitNotification,
@@ -841,6 +840,15 @@ export default function ProjectDevelopmentView({
useState<ResourceCanvasViewportBySortMode>(defaultResourceCanvasViewports);
const [activeResourceCategory, setActiveResourceCategory] =
useState<ResourceCategory | null>(null);
const [unreadResourceCategoryState, setUnreadResourceCategoryState] =
useState<{
scopeKey: string;
categories: Set<ResourceCategory>;
}>(() => ({ scopeKey: '', categories: new Set() }));
const resourceCategorySnapshotRef = useRef<{
scopeKey: string;
resourceIdsByCategory: Map<ResourceCategory, Set<string>>;
} | null>(null);
const [selectedResourceId, setSelectedResourceId] = useState<string | null>(
null,
);
@@ -880,8 +888,10 @@ export default function ProjectDevelopmentView({
const [hiddenCommittedResourceId, setHiddenCommittedResourceId] = useState<
string | null
>(null);
const [resourcePreviewVersionByResourceId, setResourcePreviewVersionByResourceId] =
useState<Map<string, string>>(() => new Map());
const [
resourcePreviewVersionByResourceId,
setResourcePreviewVersionByResourceId,
] = useState<Map<string, string>>(() => new Map());
const [approvalMode, setApprovalMode] = useState<ApprovalMode>('strict');
const [approvalDialogOpen, setApprovalDialogOpen] = useState(false);
const [approvalNotice, setApprovalNotice] = useState('');
@@ -1388,6 +1398,24 @@ export default function ProjectDevelopmentView({
),
[resources],
);
const resourceIdsByCategory = useMemo(
() =>
new Map(
categoryOrder.map((category) => [
category,
new Set(
(projectResourcesByCategory.get(category) ?? []).map(
(resource) => resource.id,
),
),
]),
),
[projectResourcesByCategory],
);
const resourceCategoryScopeKey = useMemo(
() => JSON.stringify([projectPath, manifest.projectId]),
[manifest.projectId, projectPath],
);
const resourcePageCategories = useMemo(
() => (resources.length > 0 ? categoryOrder : []),
[resources.length],
@@ -1401,6 +1429,63 @@ export default function ProjectDevelopmentView({
) ??
resourcePageCategories[0] ??
null;
const viewedResourceCategory =
mode === 'resources' ? activePageCategory : null;
useEffect(() => {
const previousSnapshot = resourceCategorySnapshotRef.current;
if (
!previousSnapshot ||
previousSnapshot.scopeKey !== resourceCategoryScopeKey
) {
resourceCategorySnapshotRef.current = {
scopeKey: resourceCategoryScopeKey,
resourceIdsByCategory,
};
setUnreadResourceCategoryState({
scopeKey: resourceCategoryScopeKey,
categories: new Set(),
});
return;
}
const categoriesWithNewResources = categoryOrder.filter((category) => {
if (category === viewedResourceCategory) {
return false;
}
const previousIds =
previousSnapshot.resourceIdsByCategory.get(category) ?? new Set();
return Array.from(resourceIdsByCategory.get(category) ?? []).some(
(resourceId) => !previousIds.has(resourceId),
);
});
resourceCategorySnapshotRef.current = {
scopeKey: resourceCategoryScopeKey,
resourceIdsByCategory,
};
setUnreadResourceCategoryState((current) => {
const categories =
current.scopeKey === resourceCategoryScopeKey
? new Set(current.categories)
: new Set<ResourceCategory>();
let changed = current.scopeKey !== resourceCategoryScopeKey;
if (viewedResourceCategory && categories.delete(viewedResourceCategory)) {
changed = true;
}
categoriesWithNewResources.forEach((category) => {
if (!categories.has(category)) {
categories.add(category);
changed = true;
}
});
return changed
? { scopeKey: resourceCategoryScopeKey, categories }
: current;
});
}, [resourceCategoryScopeKey, resourceIdsByCategory, viewedResourceCategory]);
const unreadResourceCategories =
unreadResourceCategoryState.scopeKey === resourceCategoryScopeKey
? unreadResourceCategoryState.categories
: new Set<ResourceCategory>();
resourceCanvasPageWheelRef.current.activeCategory = activePageCategory;
const nextPageCategory =
activePageCategory && resourcePageCategories.length > 1
@@ -1515,8 +1600,7 @@ export default function ProjectDevelopmentView({
: { x: 48, y: 48, scale: 1 };
const setResourceCanvasViewport = useCallback(
(candidate: CanvasViewport) => {
const activeCategory =
resourceCanvasPageWheelRef.current.activeCategory;
const activeCategory = resourceCanvasPageWheelRef.current.activeCategory;
if (!activeCategory) {
return;
}
@@ -1800,8 +1884,7 @@ export default function ProjectDevelopmentView({
const advanceResourceCanvasPage = useCallback(
(stepOffset: number) => {
const currentCategory =
resourceCanvasPageWheelRef.current.activeCategory;
const currentCategory = resourceCanvasPageWheelRef.current.activeCategory;
const normalizedStepOffset = Math.trunc(stepOffset);
if (
!currentCategory ||
@@ -1849,8 +1932,7 @@ export default function ProjectDevelopmentView({
return;
}
const now = performance.now();
const waitMs =
pageWheelSwitchIntervalMs - (now - state.lastSwitchAt);
const waitMs = pageWheelSwitchIntervalMs - (now - state.lastSwitchAt);
if (waitMs > 0) {
if (state.pendingTimerId === null) {
state.pendingTimerId = window.setTimeout(() => {
@@ -1925,8 +2007,7 @@ export default function ProjectDevelopmentView({
}
pageWheelState.accumulatedDeltaY += normalizedDeltaY;
if (
Math.abs(pageWheelState.accumulatedDeltaY) >=
pageWheelDeltaThreshold
Math.abs(pageWheelState.accumulatedDeltaY) >= pageWheelDeltaThreshold
) {
const stepCount = Math.max(
1,
@@ -2063,11 +2144,7 @@ export default function ProjectDevelopmentView({
]);
useLayoutEffect(() => {
if (
mode !== 'resources' ||
!activePageCategory ||
!activePageLayoutReady
) {
if (mode !== 'resources' || !activePageCategory || !activePageLayoutReady) {
return undefined;
}
const canvas = resourceCanvasRef.current;
@@ -2752,9 +2829,7 @@ export default function ProjectDevelopmentView({
}
drag.changed = true;
const coordinate = (start: number, delta: number) =>
normalizeResourceCanvasDragCoordinate(
start + delta / drag.startScale,
);
normalizeResourceCanvasDragCoordinate(start + delta / drag.startScale);
const preview = {
resourceId: drag.resourceId,
x: coordinate(drag.startX, deltaX),
@@ -3944,27 +4019,37 @@ export default function ProjectDevelopmentView({
data-unavailable={!runAvailable || uiEditorRoute || undefined}
className={mode === 'run' ? 'is-active' : ''}
disabled={uiEditorRoute !== null}
onClick={showRunView}
onClick={() => {
if (!runAvailable) {
return;
}
showRunView();
onPlay?.();
}}
>
</button>
</div>
<div className="game-workbench-view-actions">
<button
type="button"
className="game-workbench-play-button"
disabled={!runAvailable || !onPlay || uiEditorRoute !== null}
onClick={() => {
showRunView();
onPlay?.();
}}
title={
runAvailable ? '运行当前游戏' : '首个可运行原型完成后才能播放'
}
>
<Play size={15} aria-hidden="true" />
</button>
{mode === 'run' ? (
<button
type="button"
className="game-workbench-play-button"
disabled={!runAvailable || !onPlay || uiEditorRoute !== null}
onClick={() => {
showRunView();
onPlay?.();
}}
title={
runAvailable
? '运行当前游戏'
: '首个可运行原型完成后才能播放'
}
>
<Play size={15} aria-hidden="true" />
</button>
) : null}
{mode === 'resources' &&
!assetCanvasRoute &&
!resourceEditorRoute &&
@@ -4506,10 +4591,15 @@ export default function ProjectDevelopmentView({
>
{resourcePageCategories.map((category) => {
const Icon = categoryIcons[category];
const hasUnreadResources =
unreadResourceCategories.has(category);
return (
<button
key={category}
type="button"
aria-label={`${categoryLabels[category]}${
hasUnreadResources ? ',有新资源' : ''
}`}
aria-current={
category === activePageCategory
? 'page'
@@ -4525,7 +4615,15 @@ export default function ProjectDevelopmentView({
}
>
<Icon size={14} aria-hidden="true" />
<strong>{categoryLabels[category]}</strong>
<strong className="game-resource-outline-label">
{categoryLabels[category]}
{hasUnreadResources ? (
<span
className="game-resource-outline-unread"
aria-hidden="true"
/>
) : null}
</strong>
</button>
);
})}
@@ -4787,29 +4885,13 @@ export default function ProjectDevelopmentView({
<dd>{selectedResource.mediaType}</dd>
</div>
</dl>
) : (
<p></p>
)}
) : null}
</section>
<section aria-label="数值微调面板">
<header>
<SlidersHorizontal size={16} aria-hidden="true" />
</header>
<label>
<input type="number" disabled placeholder="未载入" />
</label>
<label>
<input type="number" disabled placeholder="未载入" />
</label>
<input
type="text"
disabled
aria-label="新增微调数值项"
placeholder="用自然语言新增微调项(尚未接入)"
/>
</section>
</div>
</section>
@@ -4819,11 +4901,6 @@ export default function ProjectDevelopmentView({
{!uiEditorRoute ? (
<aside className="game-workbench-chat" aria-label="陶泥儿 Agent 对话">
<header>
<img
className="game-workbench-chat-avatar"
src={PRODUCT_LOGO}
alt="陶泥儿"
/>
<div className="game-workbench-chat-title">
<strong></strong>
<small>
@@ -1335,7 +1335,7 @@ export function registerHomeProjectCreationTests() {
expect(screen.getAllByText('今天有什么设计需要帮你整理')).toHaveLength(1);
// 做方案走立项策划链路,建项按钮在这一档改叫「进入立项策划」。
fireEvent.click(screen.getByRole('button', { name: '进入立项策划' }));
expect(screen.getByText('请输入方案需求或上传资料')).not.toBeNull();
expect(screen.queryByText('请输入方案需求或上传资料')).toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'create_automatic_local_game_project',
);
@@ -1510,12 +1510,13 @@ export function registerHomeProjectCreationTests() {
});
expect((createButton as HTMLButtonElement).disabled).toBe(true);
expect(screen.queryByLabelText('陶泥儿首页对话')).toBeNull();
expect(screen.getByLabelText('最近项目').textContent).toContain(
expect(screen.getByLabelText('最近项目').textContent).not.toContain(
'选择一个项目继续创作',
);
expect(screen.getByLabelText('最近项目').textContent).not.toContain(
'正在创建工作区',
);
expect(screen.queryByText('正在创建工作区')).toBeNull();
await act(async () => {
rejectAutomaticProject?.(new Error('自动创建测试结束'));
@@ -1524,7 +1525,7 @@ export function registerHomeProjectCreationTests() {
await waitFor(() => {
expect((createButton as HTMLButtonElement).disabled).toBe(false);
});
expect(screen.getAllByText('自动创建测试结束')).not.toHaveLength(0);
expect(screen.queryByText('自动创建测试结束')).toBeNull();
});
it.each([
@@ -4814,7 +4814,7 @@ export function registerUserSurfaceBoundaryTests() {
}
export function registerProjectSupervisorSurfaceTests() {
it('routes a top workbench play request into the existing game.run_local confirmation', async () => {
it('runs a top workbench play request without a second confirmation', async () => {
const projectPath = '/tmp/top-play-request';
const supervisorHarness = createProjectSupervisorRuntimeHarness({
projectPath,
@@ -4832,6 +4832,19 @@ export function registerProjectSupervisorSurfaceTests() {
recentRunStopReason: null,
};
}
if (command === 'start_local_game_preview') {
return {
url: 'http://127.0.0.1:43127/',
port: 43127,
root: projectPath,
};
}
if (command === 'get_local_game_manifest') {
return createGameCreationAppManifest(
'top-play-request',
'顶部播放请求',
);
}
return supervisorHarness.invoke(command, args);
},
);
@@ -4851,14 +4864,17 @@ export function registerProjectSupervisorSurfaceTests() {
);
expect(
await screen.findByText('game.run_local', {}, { timeout: 3_000 }),
await screen.findByText(
/运行通过,已载入客户端运行视图/,
{},
{ timeout: 3_000 },
),
).not.toBeNull();
expect(handled).toHaveBeenCalledWith(7);
expect(screen.getByText(`启动 ${projectPath}/game/`)).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'start_local_game_preview',
expect.anything(),
);
expect(invoke).toHaveBeenCalledWith('start_local_game_preview', {
projectPath,
});
expect(screen.queryByText('game.run_local')).toBeNull();
});
it('starts a direct product preview without the legacy Canvas-only static smoke', async () => {
@@ -4899,9 +4915,6 @@ export function registerProjectSupervisorSurfaceTests() {
}),
);
expect(await screen.findByText('game.run_local')).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText(
'运行通过,已载入客户端运行视图:http://127.0.0.1:43126/',
@@ -116,8 +116,10 @@
- 资源总览的“资源依赖 / 资源类型”视图切换使用连通的分段按钮组,相邻选项共享边界并保持唯一选中语义。每个分段都必须有清晰的键盘焦点指示,焦点环不得被分段容器的圆角或 `overflow` 裁切。
- 右侧 Supervisor 对话中,用户消息使用右对齐、最大宽度受限的主题暖色气泡,assistant 消息保持左对齐;消息换行不得产生水平溢出,执行过程卡继续占满消息区可用宽度。气泡正文在 light / dark 平台主题下均须满足 WCAG AA 普通文本 `4.5:1` 对比度。
- 客户端正式产品仍只按最小 `1280×720` 横屏合同交付,并保留 `1280×800` 默认窗口与既有基线验收;更窄浏览器样式只负责不崩溃和开发兼容,不改成移动端创作工作台。
- 工作台顶部播放按钮在桌面视窗中水平居中;资源管理视窗触发播放时直接切换到运行视窗并启动本地预览,不再弹出 `game.run_local` 二次确认。
- 普通用户界面不默认展示内部错误码或本机绝对路径,也不展示 External Editor Base URL 或 Developer API Key;官方服务地址由客户端构建固定,远端编辑自动使用当前陶泥儿登录态。仅独立 standalone game-chat release 或显式高级自定义模式在隔离配置页维护 External v1 URL/Key,确需诊断的信息进入受控详情或开发模式。
- 创建模式素材画布的“素材名称”是用户可编辑的正式输出名称;“资源用途”是 manifest subtype,不向普通用户开放自由文本。新增资源默认“普通游戏美术”,可从普通游戏美术、统一视觉规范、游戏界面原型、核心美术图集四项中选择。图片精修继承源名称和用途,不显示创建模式保存设置;候选图片只从选中图片的“设为最终图”提交。精修顶栏只保留返回、导入、定位当前最终图、撤销和重做,删除进入图片上下文工具栏,通用 AI 生成只保留给创建模式。图片输出统一使用 PNG;创建模式工具动作与保存设置分层展示,“保存到项目”在 `1280×800` 和窄容器中都必须完整可见。
- 首页创作输入区与“最近项目”之间不展示共享项目状态文本,“最近项目”标题下也不追加解释性副标题;默认、成功、进行中或失败状态均不得在该位置形成文字行,项目管理页继续保留自己的状态反馈。
### 3.8 现有 Godot 项目