Merge branch 'master' into codex/game-agent-resource-workbench
Project CI / Frontend tests (pull_request) Successful in 4m35s
Project CI / Repository checks (pull_request) Successful in 4m38s
Project CI / Backend tests (pull_request) Successful in 5m37s
Project CI / Native shell tests (pull_request) Successful in 14m12s

This commit is contained in:
2026-08-23 18:05:07 +08:00
11 changed files with 155 additions and 69 deletions
@@ -393,12 +393,23 @@ pub(crate) fn inspect_local_project_directory(
is_godot_project: godot_project_root.is_some(),
godot_project_root,
project_name: game_creator_project_name(root),
modified_at: project_directory_modified_at(root),
manifest_error: game_creator_project_manifest_error(root),
recent_run_status: recent_run_trace.as_ref().map(|trace| trace.status.clone()),
recent_run_stop_reason: recent_run_trace.map(|trace| trace.stop_reason),
})
}
fn project_directory_modified_at(root: &Path) -> Option<u64> {
root.metadata()
.ok()?
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()
.map(|duration| duration.as_millis().min(u128::from(u64::MAX)) as u64)
}
pub(crate) fn is_game_creator_project_directory(root: &Path) -> bool {
if !root.is_dir() {
return false;
@@ -179,6 +179,7 @@ struct LocalProjectDirectoryStatus {
is_godot_project: bool,
godot_project_root: Option<String>,
project_name: Option<String>,
modified_at: Option<u64>,
manifest_error: Option<String>,
recent_run_status: Option<String>,
recent_run_stop_reason: Option<String>,
@@ -1131,6 +1131,25 @@ fn validate_generation_draft_active(
Ok(())
}
fn validate_generation_draft_for_public_phase(
ledger: &AssetCanvasGenerationLedger,
draft: &AssetCanvasDraft,
) -> Result<(), String> {
if ledger.phase == GenerationLedgerPhase::AssetDurableCommitted
&& draft.status == AssetCanvasDraftStatus::Committed
{
if draft.project_id != ledger.project_id
|| draft.draft_id != ledger.draft_id
|| draft.intent != ledger.intent
|| draft.source_asset_id != ledger.source_asset_id
{
return Err("素材画布生成账本与草稿身份不一致".to_string());
}
return Ok(());
}
validate_generation_draft_active(ledger, draft)
}
fn ensure_generation_draft_active(
root: &Path,
ledger: &AssetCanvasGenerationLedger,
@@ -1150,7 +1169,7 @@ fn upsert_public_generation_record(
let _lock = acquire_asset_canvas_draft_lock(root)?;
let mut draft = read_asset_canvas_draft_locked(root, &ledger.project_id, &ledger.draft_id)?
.ok_or_else(|| "素材画布草稿不存在".to_string())?;
validate_generation_draft_active(ledger, &draft)?;
validate_generation_draft_for_public_phase(ledger, &draft)?;
let now = asset_canvas_now();
let output_asset_id = ledger
.commit_result
@@ -1397,6 +1397,7 @@ fn project_directory_status_distinguishes_missing_file_and_dir() {
is_godot_project: false,
godot_project_root: None,
project_name: None,
modified_at: None,
manifest_error: None,
recent_run_status: None,
recent_run_stop_reason: None,
@@ -1412,6 +1413,7 @@ fn project_directory_status_distinguishes_missing_file_and_dir() {
assert!(!file_status.is_godot_project);
assert_eq!(file_status.godot_project_root, None);
assert_eq!(file_status.project_name, None);
assert!(file_status.modified_at.is_some());
assert_eq!(file_status.manifest_error, None);
assert_eq!(file_status.recent_run_status, None);
fs::remove_file(&root).expect("remove file");
@@ -96,6 +96,7 @@ export interface LocalProjectDirectoryStatus {
isGodotProject: boolean;
godotProjectRoot: string | null;
projectName: string | null;
modifiedAt?: number | null;
manifestError?: string | null;
recentRunStatus: string | null;
recentRunStopReason: string | null;
@@ -61,6 +61,7 @@ export type RecentProjectRow = {
status: string;
projectKind: 'web' | 'godot' | 'unknown';
godotProjectRoot: string | null;
modifiedAt: number | null;
recentRunStatus: string | null;
recentRunStopReason: string | null;
canReveal: boolean;
@@ -262,6 +263,7 @@ export function buildRecentProjectRows(
? 'web'
: 'unknown',
godotProjectRoot: directoryStatus?.godotProjectRoot ?? null,
modifiedAt: directoryStatus?.modifiedAt ?? null,
recentRunStatus: directoryStatus?.recentRunStatus ?? null,
recentRunStopReason: directoryStatus?.recentRunStopReason ?? null,
canReveal,
@@ -1,11 +1,11 @@
import {
ArrowUp,
FileText,
FolderKanban,
FolderOpen,
Gamepad2,
Image,
type LucideIcon,
Plus,
Sparkles,
} from 'lucide-react';
import type { FormEvent } from 'react';
@@ -65,9 +65,30 @@ export type HomeProjectRow = {
path: string;
name: string;
status: string;
projectKind: 'web' | 'godot' | 'unknown';
modifiedAt: number | null;
canOpen: boolean;
};
function formatProjectUpdatedAt(modifiedAt: number | null) {
if (!modifiedAt || !Number.isFinite(modifiedAt) || modifiedAt <= 0) {
return '未记录';
}
const date = new Date(modifiedAt);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}/${month}/${day}`;
}
function projectTypeLabel(projectKind: HomeProjectRow['projectKind']) {
return projectKind === 'godot'
? 'Godot 游戏'
: projectKind === 'web'
? 'Web 游戏'
: '本地项目';
}
type HomeViewProps = {
hasPromo: boolean;
status: string;
@@ -143,26 +164,30 @@ export default function HomeView({
return (
<div className="platform-theme platform-theme--light min-h-screen bg-(image:--platform-body-fill) text-(--platform-text-strong)">
<section
className={`grid min-h-93.5 content-start justify-items-center gap-3.25 px-6 pb-4.5 pt-25.75 max-[760px]:min-h-0 max-[760px]:gap-4 max-[760px]:px-3.5 max-[760px]:pb-8.5 max-[760px]:pt-21 ${
hasPromo ? 'pt-33.75 max-[760px]:pt-21' : ''
className={`grid content-start justify-items-center gap-3.5 px-6 pb-4.5 pt-20 max-[760px]:gap-4 max-[760px]:px-3.5 max-[760px]:pb-8.5 max-[760px]:pt-21 ${
hasPromo ? 'pt-28 max-[760px]:pt-21' : ''
}`}
aria-label="首页输入"
>
<div className="flex items-center gap-3 text-center max-[760px]:flex-col max-[760px]:gap-2.5">
<span className="grid size-32 shrink-0 place-items-center rounded-full shadow-(--platform-profile-action-shadow)">
<img src={BRAND_ICON} alt="logo" className="size-32" />
<div className="flex w-[min(814px,calc(100vw-122px))] items-center gap-4 text-left max-[760px]:w-[min(100%,calc(100vw-76px))] max-[760px]:flex-col max-[760px]:items-start max-[760px]:gap-2.5">
<span className="grid size-30 shrink-0 place-items-center rounded-full bg-(--platform-subpanel-fill) shadow-(--platform-profile-action-shadow) max-[760px]:size-27">
<img
src={BRAND_ICON}
alt="logo"
className="size-30 max-[760px]:size-27"
/>
</span>
<div>
<h1 className="m-0 text-[27px] font-[750] tracking-normal text-(--platform-brand-logo-title) max-[760px]:text-[22px]">
<h1 className="m-0 text-[36px] font-[750] tracking-[-0.03em] text-(--platform-brand-logo-title) max-[760px]:text-[33px]">
</h1>
<p className="m-0 mt-3 text-[13px] text-(--platform-text-soft)">
{activeCreationType.placeholder}
<p className="m-0 mt-1.5 text-[19.5px] font-medium text-(--platform-warm-text)">
</p>
</div>
</div>
<div
className="flex w-[min(488px,calc(100vw-110px))] flex-wrap justify-center gap-2.25 max-[760px]:w-[min(100%,calc(100vw-76px))]"
className="flex w-[min(814px,calc(100vw-122px))] flex-wrap justify-start gap-3 max-[760px]:w-[min(100%,calc(100vw-76px))]"
role="group"
aria-label="创作类型"
>
@@ -171,10 +196,10 @@ export default function HomeView({
const isActive = item.creationType === homeCreationType;
return (
<button
className={`inline-flex min-h-6.75 cursor-pointer items-center gap-1.25 rounded-full border px-3 text-[12px] ${
className={`inline-flex min-h-7 cursor-pointer items-center gap-1.25 rounded-full border px-4 text-[12px] font-medium transition-colors ${
isActive
? 'border-(--platform-nav-active-border) bg-(image:--platform-nav-active-fill) text-(--platform-warm-text) shadow-(--platform-nav-active-shadow)'
: 'border-(--platform-subpanel-border) bg-(image:--platform-subpanel-fill) text-(--platform-neutral-text)'
? 'border-(--platform-button-primary-border) bg-(image:--platform-button-primary-fill) text-(--platform-button-primary-text) shadow-(--platform-nav-active-shadow)'
: 'border-(--platform-subpanel-border) bg-(image:--platform-subpanel-fill) text-(--platform-neutral-text) hover:border-(--platform-nav-active-border) hover:text-(--platform-warm-text)'
}`}
type="button"
key={item.creationType}
@@ -187,11 +212,8 @@ export default function HomeView({
);
})}
</div>
<span className="text-[12px] text-(--platform-text-soft)">
{status}
</span>
<form
className="grid min-h-20 w-[min(1054px,calc(100vw-122px))] grid-rows-[minmax(42px,auto)_auto] rounded-[18px] border border-(--platform-surface-border) bg-(--platform-input-fill) px-3 pb-2 pt-3.25 shadow-(--platform-panel-shadow) focus-within:bg-(--platform-input-fill-focus) focus-within:ring-4 focus-within:ring-(--platform-input-focus-ring) max-[760px]:w-[min(100%,calc(100vw-76px))]"
className="grid min-h-26 w-[min(814px,calc(100vw-122px))] grid-rows-[minmax(62px,auto)_auto] rounded-[16px] border border-(--platform-surface-border) bg-(--platform-input-fill) px-3.5 pb-2.5 pt-3.5 shadow-(--platform-panel-shadow) focus-within:bg-(--platform-input-fill-focus) focus-within:ring-4 focus-within:ring-(--platform-input-focus-ring) max-[760px]:w-[min(100%,calc(100vw-76px))]"
onSubmit={handleHomeSubmit}
>
<RichInputArea
@@ -205,20 +227,25 @@ export default function HomeView({
<div className="grid grid-cols-[1fr_auto] items-center gap-2.5 text-[12px] text-(--platform-text-soft)">
<UploadButton />
<button
className="grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-(image:--platform-button-primary-fill) p-0 text-(--platform-button-primary-text) shadow-(--platform-profile-action-shadow) disabled:cursor-not-allowed disabled:opacity-55"
className="grid size-7 cursor-pointer place-items-center rounded-full border-0 bg-(image:--platform-button-primary-fill) p-0 text-(--platform-button-primary-text) shadow-(--platform-profile-action-shadow) transition-transform hover:scale-105 disabled:cursor-not-allowed disabled:opacity-55"
type="submit"
aria-label="开启创作"
disabled={homeCreationBusy}
>
<Plus size={16} aria-hidden="true" />
<ArrowUp size={16} aria-hidden="true" />
</button>
</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
className="mx-auto grid w-[min(1054px,calc(100vw-122px))] gap-3.5 max-[760px]:w-[min(100%,calc(100vw-76px))]"
className="mx-auto grid w-[min(814px,calc(100vw-122px))] gap-3.5 max-[760px]:w-[min(100%,calc(100vw-76px))]"
aria-label="最近项目"
>
<header className="flex items-center justify-between gap-3">
@@ -249,27 +276,37 @@ export default function HomeView({
</div>
</header>
{recentProjectRows.length > 0 ? (
<div className="grid grid-cols-5 gap-3 overflow-hidden max-[760px]:grid-cols-1 max-[760px]:overflow-visible">
<div className="grid grid-cols-3 gap-4 overflow-hidden max-[760px]:grid-cols-1 max-[760px]:overflow-visible">
{recentProjectRows.map((project) => (
<article className="grid min-w-0" key={project.path}>
<button
className="grid h-auto w-full cursor-pointer content-start justify-items-start gap-1.5 overflow-hidden rounded-lg border border-(--platform-surface-border) bg-(image:--platform-subpanel-fill) p-3 text-left shadow-(--platform-panel-shadow) transition hover:border-(--platform-surface-hover-border) disabled:cursor-not-allowed disabled:opacity-[0.58] max-[760px]:h-33"
className="grid min-h-34 w-full cursor-pointer grid-cols-[108px_minmax(0,1fr)] items-stretch gap-3 overflow-hidden rounded-xl border border-(--platform-surface-border) bg-(image:--platform-subpanel-fill) p-2 text-left shadow-(--platform-panel-shadow) transition hover:border-(--platform-surface-hover-border) hover:shadow-(--platform-profile-action-shadow) disabled:cursor-not-allowed disabled:opacity-[0.58] max-[760px]:grid-cols-[120px_minmax(0,1fr)]"
type="button"
disabled={!project.canOpen}
onClick={() => onProjectOpen(project.path)}
>
<span className="grid size-10.5 place-items-center rounded-xl bg-(--platform-warm-bg) text-[16px] font-bold text-(--platform-warm-text)">
<span className="grid min-h-29 place-items-center rounded-lg bg-[linear-gradient(145deg,#f9bf7d,#e96831_52%,#853c2c)] text-[27px] font-bold tracking-[-0.08em] text-white shadow-inner">
{project.name.slice(0, 2)}
</span>
<strong className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[12px] font-medium text-(--platform-text-strong)">
{project.name}
</strong>
<small className="-mt-1.25 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[11px] text-(--platform-text-soft)">
{project.path}
</small>
<small className="-mt-1.25 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[11px] text-(--platform-text-soft)">
{project.status}
</small>
<span className="grid min-w-0 content-center gap-2.25 py-1">
<strong className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[14px] font-semibold leading-4.5 text-(--platform-text-strong)">
{project.name}
</strong>
<small className="grid grid-cols-[36px_minmax(0,1fr)] gap-x-3 gap-y-1 text-[10px] leading-3.5 text-(--platform-text-soft)">
<span className="whitespace-nowrap"></span>
<span className="truncate whitespace-nowrap text-(--platform-neutral-text)">
{projectTypeLabel(project.projectKind)}
</span>
<span className="whitespace-nowrap"></span>
<span className="truncate whitespace-nowrap text-(--platform-neutral-text)">
{formatProjectUpdatedAt(project.modifiedAt)}
</span>
<span className="whitespace-nowrap"></span>
<span className="truncate whitespace-nowrap text-(--platform-warm-text)">
{project.status}
</span>
</small>
</span>
</button>
</article>
))}
@@ -296,29 +333,23 @@ export default function HomeView({
</section>
<section
className="mx-auto mt-5.5 grid w-[min(1054px,calc(100vw-122px))] gap-3.5 max-[760px]:w-[min(100%,calc(100vw-76px))]"
className="mx-auto mt-5.5 grid w-[min(814px,calc(100vw-122px))] gap-3.5 max-[760px]:w-[min(100%,calc(100vw-76px))]"
aria-label="灵感推荐"
>
<header className="flex items-center justify-start gap-3">
<div className="grid gap-1">
<span className="inline-flex items-center gap-1.5">
<h2 className="m-0 text-[15px] text-(--platform-text-strong)">
</h2>
<Sparkles
className="text-(--platform-warm-text)"
size={15}
aria-hidden="true"
/>
</span>
<p className="mb-0 mt-1 text-[12px] text-(--platform-text-soft)">
</p>
</div>
<span className="inline-flex items-baseline gap-1.5">
<h2 className="m-0 text-[15px] text-(--platform-text-strong)">
</h2>
<Sparkles
className="text-(--platform-warm-text)"
size={15}
aria-hidden="true"
style={{ transform: 'translateY(1px)' }}
/>
</span>
</header>
<div className="grid min-h-24 place-items-center overflow-hidden rounded-lg border border-(--platform-surface-border) bg-(image:--platform-subpanel-fill) text-[11px] text-(--platform-text-soft)">
</div>
<div className="min-h-24 overflow-hidden rounded-lg border border-(--platform-surface-border) bg-(image:--platform-subpanel-fill)" />
</section>
</div>
);
@@ -26,12 +26,12 @@ import {
} from './harness';
export function registerClientHomeTests() {
it('keeps the empty inspiration section without reading the main-site showcase', async () => {
it('keeps the empty inspiration module without requesting the retired feed', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch');
renderLauncherAt('/?launcher');
const inspirationSection = screen.getByLabelText('灵感推荐');
expect(within(inspirationSection).getAllByText('暂无灵感')).toHaveLength(2);
expect(screen.getByLabelText('灵感推荐')).not.toBeNull();
expect(screen.queryByText('暂无灵感')).toBeNull();
await act(async () => {
await Promise.resolve();
});
@@ -1316,19 +1316,22 @@ export function registerHomeProjectCreationTests() {
expect(gameType.getAttribute('aria-pressed')).toBe('true');
expect(artType.getAttribute('aria-pressed')).toBe('false');
expect(documentType.getAttribute('aria-pressed')).toBe('false');
expect(screen.getAllByText('今天想把什么灵感做成游戏')).toHaveLength(2);
expect(screen.getByText('你的游戏创作管家')).not.toBeNull();
expect(screen.getAllByText('今天想把什么灵感做成游戏')).toHaveLength(1);
fireEvent.click(documentType);
expect(documentType.getAttribute('aria-pressed')).toBe('true');
expect(screen.getAllByText('今天有什么设计需要帮你整理')).toHaveLength(2);
expect(screen.getByText('你的游戏创作管家')).not.toBeNull();
expect(screen.getAllByText('今天有什么设计需要帮你整理')).toHaveLength(1);
fireEvent.click(screen.getByRole('button', { name: '开启创作' }));
expect(await screen.findByText('请输入方案需求或上传资料')).not.toBeNull();
expect(screen.getByText('请输入方案需求或上传资料')).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'create_automatic_local_game_project',
);
fireEvent.click(artType);
expect(gameType.getAttribute('aria-pressed')).toBe('false');
expect(artType.getAttribute('aria-pressed')).toBe('true');
expect(screen.getAllByText('今天想做什么样的美术素材')).toHaveLength(2);
expect(screen.getByText('你的游戏创作管家')).not.toBeNull();
expect(screen.getAllByText('今天想做什么样的美术素材')).toHaveLength(1);
const promptInput = screen.getByLabelText('创作想法');
nativeClipboardMock.text = '你好,今天多少号';
@@ -122,7 +122,7 @@
### 3.9 项目管理页
- 项目页采用“标题与状态 + 本地搜索 + 打开项目 / 新建项目 + 紧凑项目表格”的桌面信息架构。可以参考成熟项目管理器的信息层级和密度,但不得复制 Unity 等外部产品的品牌、Logo、深色皮肤、专有图标、列名或文案;页面继续使用 Genarrative 平台主题和陶泥儿客户端壳。
- 表格只展示现有权威数据:项目名称与工作区绝对路径、GameAgent / Godot 类型、目录 / manifest / 最近 Runtime 状态和行级操作。当前目录检查合同没有修改时间编辑器版本、收藏或云状态,前端不得伪造这些列。
- 表格只展示现有权威数据:项目名称与工作区绝对路径、GameAgent / Godot 类型、目录 / manifest / 最近 Runtime 状态和行级操作;项目管理表格不展示目录修改时间。首页“最近项目”卡片可使用目录检查返回的 `modifiedAt` 展示更新时间编辑器版本、收藏或云状态仍不在当前目录检查合同内,前端不得伪造这些列。
- 可打开项目的主行点击后进入项目;显示目录和从最近列表移除收进键盘可达的行尾更多菜单。Escape 关闭菜单并把焦点还给触发按钮;移除只修改本机 WebView 最近项目记录,不删除磁盘文件。
- 搜索只在已加载项目行中匹配名称、路径、类型、Godot 相对根和状态,不改 localStorage、不触碰项目目录、不新增后端或 Tauri 命令。无匹配状态提供清除搜索;真正空状态仍只引导用户使用顶部打开或新建,不追加第三个目录选择入口。
- 正式视觉验收只覆盖 `1280×720` 最小横屏和 `1280×800` 默认窗口:工具栏保持单行,表头与项目列对齐,项目列表内部滚动,document/body 不出现页面级横向或纵向溢出。截图必须使用含 Web、Godot 和无效状态的 populated fixture;视频必须演示搜索、清除、行尾菜单及可观察的项目操作结果,不能只录静止页面或无结果点击。
@@ -662,7 +662,9 @@ game-project/
- Agent 状态列表从 `.agent/manifest.json` 的任务 / 角色清单、`.agent/run.latest.json` / `.agent/runs/<runId>.json` 的 step、taskGraph、passPlans、lifecycleStatus,以及 `read_game_creator_agent_runtimes` 批量读取的 `.agent/runtime/agents/<taskId>.json` 和最近任务派生;v1 不新增独立状态数据库,也不承诺完整后台 runner。
- App 启动先检查平台登录态;登录后进入同一个客户端首页,不再有面向用户的启动器 / 主窗口切换概念。首页按 `做游戏` / `做素材` / `做方案` 保存 `game` / `art` / `doc` 初始意图,输入状态按文字与附件 token 的顺序保存,附件以文件名 token 内嵌在输入框中而非堆叠在下方;提交时才将 token 转为 LLM 可读的 `{1st attachment}` 引用。普通 `Enter` 在输入法组合态结束后自动在系统“文档/Genarrative GameAgent”下原子分配唯一工作区、初始化本地项目、导入附件,并把首条需求直接投递给 active `project-supervisor` Session 的后台 Runtime,随后写入最近项目并切到项目开发页;`Shift+Enter` 保留换行。Windows 使用系统 Documents 路径,Linux 使用 XDG DocumentsmacOS 使用用户 Documents;前端不得显示或持久化 `/tmp` 作为默认项目路径,精确 `/tmp/genarrative-ai-game-draft` 只允许由测试显式注入为 fixture。普通与 game-chat release 都必须在 Vite 生成最新 `frontendDist` 后、Tauri 嵌入资源前扫描实际构建产物;命中该旧 Linux 默认路径或无法安全遍历产物时构建失败,禁止复用 gitignored 的旧 `dist`。原“开启创作”加号按钮保持手动选择目录:点击后弹出原生目录选择,目标目录存在且非空时必须二次确认,再调用 `init_local_game_project` 完成同一后续链路。项目页和首页通用“打开项目”没有手填默认路径;picker 未携带已有工作区时由操作系统决定初始位置。缺少 active Session 时先创建并激活;成功后清空首页草稿,取消或创建失败时在首页回显状态,首页响应式断点与应用外壳统一为 `760px`。两条流程都不调用 `generate_local_game_draft``generate_platform_art_asset`、一次性 `chat_with_game_creator_agent` 或 legacy 项目对话 append。
- 首页发送、项目组目录选择和本地文件选择必须使用 Tauri 非阻塞原生 picker,并把选择器绑定到当前 `client` 窗口;禁止在同步 command 中调用 `blocking_pick_folder` / `blocking_pick_file` 阻塞 WebView 事件循环。选择器打开期间保留首页草稿可编辑,取消后恢复“开启创作”按钮并回显“已取消”。
- 当前尚未定义 GameAgent 独立的灵感数据源;首页保留“灵感推荐”区块并显示无数据状态,但不得展示或请求主站 `/creation` 的陶泥儿精选 `/api/editor/showcase/resources`。后续接入前必须先明确独立数据契约和交互验收。
- 当前尚未定义 GameAgent 独立的灵感数据源;首页保留“灵感推荐”模块及空白内容容器,但不显示无数据文案,也不得展示或请求主站 `/creation` 的陶泥儿精选 `/api/editor/showcase/resources`。后续接入前必须先明确独立数据契约和交互验收。
- 首页正式桌面版以约 `814px` 的居中内容栏组织品牌区、创作类型、输入框、最近项目与灵感推荐;品牌区和类型按钮在内容栏左缘对齐,输入框和下方模块仍保持整体居中。标题使用深色暖橙层级,固定副标题为“你的游戏创作管家”;选中的创作类型使用实心暖橙按钮,未选中项使用浅色描边,输入框使用更舒展的单行创作起始比例。
- 最近项目固定展示至多三个横向信息卡:左侧为本地渐变封面占位,右侧只投影项目名称、项目类型、目录自身真实修改时间和已有最近运行状态。目录检查通过 `modifiedAt` 提供目录自身 mtime,不递归扫描项目文件;没有封面资产时不得假装读取了项目封面,也不新增封面持久化字段。
- debug 构建启动后在用户 `client` 窗口之外额外打开 `developer` 窗口;该窗口用于开发者单独选择 Agent、管理对应 active/archived Session 并读取历史,用户消息和真实 Agent 回复只持久化到 `.agent/conversations/agents/<agentId>/` 下的规范 Session。普通用户窗口不得出现 `Agent 聊天` 导航、picker 或工具台入口。
- 首页最近项目只展示最近 3 个有效项目;项目组页在同一窗口使用紧凑桌面项目表格管理最近项目。顶部工具栏只放本地搜索、“打开项目”“新建项目”,不常驻路径输入框、目录显示按钮或独立 Godot 入口。两个项目动作都先打开绑定当前 `client` 的非阻塞原生目录选择器:“打开项目”读取已有 GameAgent 项目,或自动识别根目录 / 一层直接子目录中的唯一 Godot 工程并导入;普通未初始化目录提示改用“新建项目”。“新建项目”在用户选择工作区根后沿用非空目录确认,不自动重建无效历史路径。项目表格只投影名称、路径、GameAgent / Godot 类型、目录 / manifest / Runtime 状态;可打开行点击进入项目,显示目录和移除最近记录收进行尾更多菜单。搜索仅过滤当前行,不修改 storage;空状态不追加第三个目录选择入口。正式验收只覆盖 `1280×720` 最小横屏和 `1280×800` 默认窗口,列表内部滚动且无页面级溢出。
- 项目开发页保留左侧栏和顶部栏,顶部展示项目名、路径和最近 run 状态;中间只挂载 active `project-supervisor` Session 的正式对话面、Runtime 状态、确认/Needs input 和专业 Agent 协作只读状态,底部保留附件导入结果。真正的项目开发画布仍未落地;专业 Agent picker、完整计划和工具台继续留在开发入口。
+22 -8
View File
@@ -14,13 +14,11 @@ const developmentWorkflowDocPath =
'docs/project-memory/shared-memory/development-workflow.md';
const decisionLogDocPath = 'docs/project-memory/shared-memory/decision-log.md';
const rootPackageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const mobileShellConfigCheckSource = fs.readFileSync(
'apps/mobile-shell/scripts/check-config.mjs',
'utf8',
const mobileShellConfigCheckSource = normalizeSourceForGuardrail(
fs.readFileSync('apps/mobile-shell/scripts/check-config.mjs', 'utf8'),
);
const desktopShellConfigCheckSource = fs.readFileSync(
'apps/desktop-shell/scripts/check-config.mjs',
'utf8',
const desktopShellConfigCheckSource = normalizeSourceForGuardrail(
fs.readFileSync('apps/desktop-shell/scripts/check-config.mjs', 'utf8'),
);
const aiGameCreatorShellAppSource = fs.readFileSync(
'apps/ai-game-creator-shell/src/App.tsx',
@@ -152,6 +150,14 @@ function readSourceTree(entryPath, extension) {
return fs.readFileSync(entryPath, 'utf8');
}
function normalizeSourceForGuardrail(source) {
return source
.replace(/\s+/g, ' ')
.replace(/\s*([(),])\s*/g, '$1')
.replace(/,\s*\)/g, ')')
.trim();
}
function assertRootNativeShellCheckScripts() {
if (
rootPackageJson.scripts?.['check:native-shells'] !==
@@ -187,7 +193,11 @@ function assertNativeShellDependencyVersionGuardrails() {
"'eas-cli': '^20.3.0'",
"assertPackageLockVersion('apps/mobile-shell', 'eas-cli', '20.3.0')",
]) {
if (!mobileShellConfigCheckSource.includes(snippet)) {
if (
!mobileShellConfigCheckSource.includes(
normalizeSourceForGuardrail(snippet),
)
) {
throw new Error(
`mobile shell dependency guardrail drifted: missing ${snippet}`,
);
@@ -211,7 +221,11 @@ function assertNativeShellDependencyVersionGuardrails() {
"['tauri', '2.11.2']",
'tauri-plugin-single-instance = { version = "2.4.2", features = ["deep-link"] }',
]) {
if (!desktopShellConfigCheckSource.includes(snippet)) {
if (
!desktopShellConfigCheckSource.includes(
normalizeSourceForGuardrail(snippet),
)
) {
throw new Error(
`desktop shell dependency guardrail drifted: missing ${snippet}`,
);