diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index dd1692763..493f71c6e 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -3736,6 +3736,7 @@ iframe.preview-frame { .game-workbench-stage { position: relative; display: grid; + grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto minmax(0, 1fr); } @@ -3840,7 +3841,9 @@ iframe.preview-frame { .game-resource-manager { display: flex; flex-direction: column; + min-width: 0; min-height: 0; + overflow: hidden; } .game-resource-search { @@ -3894,14 +3897,43 @@ iframe.preview-frame { .game-resource-canvas { position: relative; flex: 1; + width: 100%; + min-width: 0; min-height: 0; padding: 12px; - overflow: auto; + overflow-x: auto; + overflow-y: auto; + scrollbar-color: #d5a089 #f7e7df; + scrollbar-width: thin; background-color: #fffdfa; background-image: radial-gradient(#eaded8 0.8px, transparent 0.8px); background-size: 18px 18px; } +.game-resource-canvas::-webkit-scrollbar { + width: 10px; + height: 12px; +} + +.game-resource-canvas::-webkit-scrollbar-track { + border-radius: 999px; + background: #f7e7df; +} + +.game-resource-canvas::-webkit-scrollbar-thumb { + border: 2px solid #f7e7df; + border-radius: 999px; + background: #d5a089; +} + +.game-resource-canvas::-webkit-scrollbar-thumb:hover { + background: #c98566; +} + +.game-resource-canvas::-webkit-scrollbar-corner { + background: #f7e7df; +} + .game-resource-canvas-content { position: relative; display: grid; diff --git a/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts b/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts index c021ccfe6..82dcb4269 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts @@ -58,6 +58,11 @@ function fileName(path: string) { return path.split(/[\\/]/u).filter(Boolean).pop() || path; } +function isConcreteTaskArtifactPath(path: string) { + const normalizedPath = path.trim(); + return normalizedPath.length > 0 && !/[*?\[\]{}]/u.test(normalizedPath); +} + export function classifyProjectedResource(input: { path: string; mediaType: string; @@ -119,6 +124,9 @@ export function projectResourcesFromReadModels( continue; } for (const path of task.artifacts) { + if (!isConcreteTaskArtifactPath(path)) { + continue; + } const category = classifyProjectedResource({ path, mediaType: '' }); if (!category || category === 'audio') { continue; diff --git a/apps/ai-game-creator-shell/tests/projectResourceProjectionModel.test.ts b/apps/ai-game-creator-shell/tests/projectResourceProjectionModel.test.ts index 06acb6276..33d3a543c 100644 --- a/apps/ai-game-creator-shell/tests/projectResourceProjectionModel.test.ts +++ b/apps/ai-game-creator-shell/tests/projectResourceProjectionModel.test.ts @@ -56,6 +56,10 @@ describe('项目资源投影', () => { 'memory/game-design.md', 'assets/ui-preview.svg', 'build/game.bundle', + '.agent/passes/pass-*/groups/design/director.md', + 'memory/draft?.md', + 'assets/[ab].png', + 'memory/{draft,final}.md', ]; audioTask.status = 'completed'; audioTask.artifacts = ['audio/unregistered-bgm.wav']; @@ -151,6 +155,10 @@ describe('项目资源投影', () => { [ 'task:design-foundation:build/game.bundle', 'task:audio-asset-plan:audio/unregistered-bgm.wav', + 'task:design-foundation:.agent/passes/pass-*/groups/design/director.md', + 'task:design-foundation:memory/draft?.md', + 'task:design-foundation:assets/[ab].png', + 'task:design-foundation:memory/{draft,final}.md', 'asset:unknown-binary', 'attachment:imports/archive.zip', ].includes(id), diff --git a/apps/ai-game-creator-shell/tests/resourceCanvasLayoutContract.test.ts b/apps/ai-game-creator-shell/tests/resourceCanvasLayoutContract.test.ts index d90d6772c..376e5001a 100644 --- a/apps/ai-game-creator-shell/tests/resourceCanvasLayoutContract.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceCanvasLayoutContract.test.ts @@ -1,3 +1,6 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + import { describe, expect, it } from 'vitest'; import { @@ -6,6 +9,38 @@ import { } from '../../../packages/shared/src/contracts/gameCreationApp'; describe('resource canvas layout contract', () => { + it('keeps wide resource content inside an explicitly scrollable stage column', () => { + const styles = readFileSync( + resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + 'utf8', + ); + const stageRule = styles.match( + /\.game-workbench-stage\s*\{([^}]*)\}/s, + ); + const managerRule = styles.match( + /\.game-resource-manager\s*\{([^}]*)\}/s, + ); + const canvasRule = styles.match( + /\.game-resource-canvas\s*\{([^}]*)\}/s, + ); + + expect(stageRule?.[1]).toContain( + 'grid-template-columns: minmax(0, 1fr);', + ); + expect(managerRule?.[1]).toContain('min-width: 0;'); + expect(managerRule?.[1]).toContain('overflow: hidden;'); + expect(canvasRule?.[1]).toContain('width: 100%;'); + expect(canvasRule?.[1]).toContain('min-width: 0;'); + expect(canvasRule?.[1]).toContain('overflow-x: auto;'); + expect(canvasRule?.[1]).toContain('overflow-y: auto;'); + expect(styles).toMatch( + /\.game-resource-canvas::-webkit-scrollbar\s*\{[^}]*height:\s*12px/s, + ); + expect(styles).toMatch( + /\.game-resource-canvas::-webkit-scrollbar-thumb\s*\{[^}]*background:\s*#d5a089/s, + ); + }); + it('keeps revisions inside the JSON safe integer range', () => { const maxRevision = GAME_CREATION_RESOURCE_LAYOUT_MAX_SAFE_REVISION; expect(JSON.parse(JSON.stringify({ revision: maxRevision }))).toEqual({ diff --git a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md index dda6a90fc..d16aa0e27 100644 --- a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md +++ b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md @@ -48,6 +48,8 @@ - 新资源首次进入某个布局时才执行默认不重叠排版;已有坐标不得被自动排序覆盖。 - 依赖布局使用资源生成/引用关系;类型布局按资源大类、子类型、尺寸规格排序。 - 资源分区由资源投影固定,前端交互不能改变分类。 +- 已完成任务的 `artifacts` 只有在声明具体文件路径时才投影为资源;包含 `*`、`?`、`[]` 或 `{}` 的 glob / 模板路径只描述未来可能产物,不生成可点击资源卡。 +- 资源列表态右上角必须常驻“按依赖 / 按类型”切换。画布内容宽于主视窗时只在资源画布内部提供清晰的横向滚动条,不能撑宽主视窗、遮挡切换按钮或裁掉最右资源卡。 - 资源卡手动拖动、拖动持久化、拖动性能与冲突后的重新拖动提示全部暂缓,不作为当前产品合同或验收条件。 ### 3.4 数值微调 @@ -540,7 +542,8 @@ type ProjectAgentMudPointAttribution = { 4. 两个窗口基于同一 revision 执行资源自动协调写入时最多一个成功;失败方收到 `conflict` 与最新完整布局,界面不静默覆盖成功方结果。 5. 布局文件缺失的旧项目可以无迁移打开;损坏、未知 schema、身份冲突、超限和链接文件失败关闭,且原文件不被空布局覆盖。 6. 布局读写不改变 manifest、游戏项目 mutation revision、Runtime verification、Agent 权限与预览状态。 -7. `1280×800` 最小横屏下全部资源可通过分区滚动访问,不出现页面级横向或纵向溢出,右侧对话和底部 Agent 状态栏保持可见。 +7. `1280×800` 最小横屏下全部资源可通过资源画布内部滚动访问;横向滚动条清晰可操作,最右资源卡与安全留白可完整露出,不出现页面级横向或纵向溢出,右上角布局切换、右侧对话和底部 Agent 状态栏保持可见。 +8. 已完成任务中的具体文档 / 美术文件继续进入资源投影;包含 glob 元字符的产物模板不生成资源卡,也不会触发本地文件读取失败空态。 ### 7.3 P1 资源依赖关系图验收 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 326088fb9..5262e7915 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,5 +1,13 @@ # 决策记录 +## 2026-08-07 资源画布闭合横向收缩链并拒绝产物模板投影 + +- 背景:资源画布显式内容宽度超过中央主视窗时,绝对定位卡片的 min-content 宽度把 `.game-workbench-stage` 隐式 grid 列撑宽,外层 `overflow: hidden` 再把右上角布局切换和最右资源卡裁掉;点击卡片进入已有 `min-width: 0` 的聚焦态后按钮重新出现,容易被误判为条件渲染。与此同时,completed task 的 `.agent/passes/pass-*/.../director.md` 只是模板,却被投影成可点击文档并在读取时失败。 +- 布局决策:工作台主视窗显式使用 `grid-template-columns: minmax(0, 1fr)`,资源管理与资源画布逐层允许横向收缩并在管理容器处裁住外溢;资源画布自身固定 `width: 100%`、承担 `overflow-x / overflow-y: auto`,并为 Tauri WebKit 提供可见滚动轨道和滑块。布局切换继续常驻,不依赖资源选中态。 +- 投影决策:只有不含 `*`、`?`、`[]`、`{}` 的具体 task artifact 文件路径可以生成任务产物资源卡。过滤只作用于 task artifact 模板,不扩大到 manifest assets、已导入附件或 Agent 文本回执。 +- 验证方式:资源投影单测同时覆盖具体产物与四类 glob 元字符;CSS 合同锁定 stage grid 列、祖先 `min-width`、内部 overflow 和 WebKit scrollbar;既有布局模型继续验证最右卡片、列间距、安全 gutter 与 section 内边距全部进入 scroll width。 +- 关联:`apps/ai-game-creator-shell/src/styles.css`、`apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts`、工作台 PRD 与 AI 游戏创作实施计划。 + ## 2026-08-06 app-run 以独立开发 profile 支持多工作树并行运行 - 背景:多个 Git worktree 能隔离代码和分支,但不会隔离宿主机端口、Tauri identifier、AppData、数据库或本地 data dir。默认 AGC 固定使用 `3080 / 8082 / 8083 / 3101`,另一工作树运行时当前窗口可能被同名实例混淆,默认启动器则会为避免串台直接失败关闭。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 33eea3487..56fb973c2 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -14,6 +14,22 @@ - 关联:相关文件、文档、提交或 Issue ``` +## 绝对定位宽画布必须闭合所有祖先的 min-content 收缩链 + +- 现象:资源列表态右上角布局切换消失、最右资源卡被裁掉且画布没有有效横向滚动;点击任一资源进入聚焦态后按钮又出现,看起来像选中态条件渲染错误。 +- 原因:绝对定位卡片所在内容层有显式超宽尺寸,grid 子项及其 flex 后代缺少 `min-width: 0` 时,min-content 宽度会撑大主视窗隐式列;最外层 `overflow: hidden` 只负责裁切,子画布的 `overflow: auto` 因自身也被撑宽而无法形成正确滚动范围。 +- 处理:从 grid 轨道开始使用 `minmax(0, 1fr)`,中间资源管理容器和实际滚动容器逐层设置 `min-width: 0`;管理容器截住外溢,资源画布固定 `width: 100%` 并独立承担横纵滚动。Tauri WebKit 需要显式 scrollbar track / thumb 样式,不能只验证 DOM 中按钮存在。 +- 验证:CSS 合同同时锁定 grid 列、祖先收缩、内部 overflow 与 WebKit scrollbar;布局模型按最右卡片外边界断言 scroll width。真实窗口还应确认布局按钮始终可见,拖到横向终点能完整看到最右卡片和安全留白。 +- 关联:`apps/ai-game-creator-shell/src/styles.css`、`apps/ai-game-creator-shell/tests/resourceCanvasLayoutContract.test.ts`、`apps/ai-game-creator-shell/tests/resourceCanvasLayoutModel.test.ts`。 + +## manifest 产物 glob 只是模板,不能直接投影为资源文件 + +- 现象:资源区出现 `director.md` 等卡片,点击后提示文件读取失败;磁盘上不存在该文件,但 manifest task 看起来已经 completed。 +- 原因:task `artifacts` 中可能保存 `.agent/passes/pass-*/...` 之类未来轮次的路径模板。completed 只说明任务状态,不会把 glob 展开成某个具体文件;把字符串按扩展名直接分类会制造幽灵资源卡。 +- 处理:task artifact 投影前拒绝包含 `*`、`?`、`[]`、`{}` 的路径,只让具体文件路径进入分类和受控读取。不要把此规则泛化到 manifest 已登记资产、已导入附件或 Agent 文本回执。 +- 验证:同一 fixture 同时放入具体 Markdown / 图片路径和各类 glob 模板,断言前者保留、后者完全不进入资源 ID 集合。 +- 关联:`apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts`、`apps/ai-game-creator-shell/tests/projectResourceProjectionModel.test.ts`。 + ## 多工作树只隔离代码,不会自动隔离 AGC 运行资源 - 现象:两个工作树同时运行 `npm run agc` 时,后启动者因 `3080` 已占用而失败,或用户在两个同名窗口间误把另一工作树的页面当作当前修改结果。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 7101b3448..24924fcce 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -362,7 +362,7 @@ game-project/ - 页面骨架固定为左侧现有全局导航、中间主视窗、右侧陶泥儿对话和底部子 Agent 状态栏;不新建第二套客户端或平行项目页。 - 中间主视窗提供 `资源管理 / 运行` 切换。2026-08-04 起运行入口不再读取 `code-prototype` 任务状态,只读取 manifest 的正式 `runnableVersions + currentRunnableVersionId`;无版本时保持视觉不可用但仍可点击查看“当前无可运行版本”,不能使用会阻断说明交互的原生 `disabled` 或 `aria-disabled`。切回资源管理只修改前端展示态,不伪造后端预览暂停结果。 -- 资源管理从当前 `GameCreationAppManifest`(包含可选 `versions`)、合法 Agent 文本回执和已导入附件派生资源,固定按文档、项目版本、美术资源、音乐音效资源分区;未知任务产物不再兜底为版本,任务声明中的未登记音频也不冒充正式音频。`按依赖 / 按类型` 使用各自前端排列并在资源列表态、聚焦态常驻右上角;dependency 模式只额外绘制当前 manifest 与资源投影可证明的精确资源引用。排列与图层都不写回 manifest,不能推断或伪造缺失依赖。 +- 资源管理从当前 `GameCreationAppManifest`(包含可选 `versions`)、合法 Agent 文本回执和已导入附件派生资源,固定按文档、项目版本、美术资源、音乐音效资源分区;未知任务产物不再兜底为版本,任务声明中的未登记音频也不冒充正式音频。已完成任务的 `artifacts` 只投影不含 `*`、`?`、`[]`、`{}` glob 元字符的具体文件路径,模板路径不生成资源卡;manifest 已登记资产、已导入附件和 Agent 文本回执保持原投影边界。`按依赖 / 按类型` 使用各自前端排列并在资源列表态、聚焦态常驻右上角;dependency 模式只额外绘制当前 manifest 与资源投影可证明的精确资源引用。排列与图层都不写回 manifest,不能推断或伪造缺失依赖。 - 资源卡支持点击聚焦、搜索和类型筛选。2026-07-28 起完成两套二维坐标与本地 CAS sidecar;2026-07-31 起 dependency 模式增加不持久化的原生 SVG 关系图层。2026-08-03 mentor 决定暂缓资源卡拖动,当前卡片不挂载 Pointer Down / Move / Up / Cancel 拖动入口,只允许自动布局和点击聚焦。聚焦态替换中央主视窗内容,保留左侧导航、右侧对话和底部 Agent 状态栏,退出后恢复搜索、布局模式、滚动位置与选中资源;不提供工具栏、工具侧边栏或可拖动标题栏。阶段四已补齐安全本地文档、扩展美术媒体与音频聚焦,正文独立滚动,视频 / 音频使用内置媒体控件,失败显示空态;美术编辑、音频编辑 / 替换、版本替换或运行模块仍不在本阶段。 - 运行表现层嵌入当前项目的 loopback 游戏画面。P1 由专用可运行版本命令停止旧 preview、卸载 iframe、复核版本快照并启动目标版本;普通 `preview.start` 继续服务 Agent 当前工作树验证,不能作为工作台运行授权或旧版本切换入口。2026-08-04 起 P3 在该生命周期上接入运行会话与 Host/Game Bridge;P3 阶段自身未接入资源替换、下一版本、测试切片编排或参数调整,P2 / P6 的当前实现以本节后续对应专节为准。 - 右侧继续复用现有 Project Supervisor 会话、Runtime 澄清和确认链路;输入区展示 `严格审批 / 风险审批 / 无需审批` 独立面板。P0 只有严格审批可选;风险审批和无需审批保持视觉不可用但允许点击查看原因,不替代 Runtime 的逐动作权限、确认、sandbox 或 reconciliation 门禁。风险 Rank 算法记录在 `docs/project-memory/todos/【待解决】AI游戏创作高风险审批Rank-2026-07-20.md`,前端不得自行计算。 @@ -385,6 +385,7 @@ game-project/ - Tauri 命令固定为 `read_local_project_resource_canvas_layout` 与 `update_local_project_resource_canvas_layout`。写命令先只读确认有效 manifest,再通过持久 `.layout.lock` 入口获取句柄级跨窗口系统锁,并在锁内复核 projectId、重新读取当前 sidecar。Unix 使用 `flock`,Windows 使用不共享文件句柄;释放只通过句柄 Drop / 进程退出完成,不使用 mtime stale 回收,也不删除锁文件。其余写入继续复用安全路径、链接检查、容量上限、恢复副本与原子替换能力;不能只依赖 React 状态或进程内锁。 - 前端从当前项目开发大组件中拆出纯布局模型与持久 Hook。默认布局、碰撞检查、资源增删协调和 section 边界由纯模型负责;读取、异步身份、CAS、错误回滚和冲突载入由 Hook 负责。Hook 以 `projectPath + projectId + mode` epoch 隔离异步结果,资源变化不取消首读或在途保存;自动协调写入经同一 FIFO 串行提交,每笔都使用最近一次成功 / 冲突响应的权威 revision。资源卡是普通可点击按钮,不绑定卡片级 Pointer 拖动处理器;指针移动不修改 CSS 坐标、不调用 SVG preview、不提交手动布局 CAS。 - 新资源只在第一次进入某个 mode 时计算默认不重叠位置;全部现存坐标保持不变。搜索、筛选、窗口 resize 和 mode 切换不得重排或回写已有坐标,窄视图通过 section 画布范围与滚动访问,不裁切持久坐标。 +- 资源卡使用绝对坐标时,最宽内容不能参与主视窗 grid 的 min-content 列宽计算:主视窗列固定为 `minmax(0, 1fr)`,资源管理容器和资源画布逐层设置 `min-width: 0`,由资源画布自身承担横纵滚动。WebKit 横向滚动轨道与滑块必须显式可见;内容未超宽时保持 `auto`,不制造无效页面级滚动。 - type 默认布局固定按 `subtype -> mediaType -> label -> id` 排序。manifest 资产的 subtype 使用 `asset.kind`,任务产物、导入附件和 Agent 文本成果使用稳定的来源 fallback;subtype 必须进入资源协调签名,不能因 MIME 相同而退化成按名称混排。 - 自动协调保存失败时保留当前会话布局;CAS 冲突载入对方最新布局,需要继续协调时最多追加两次重试,持续跨窗口竞争时停止自旋。用户提示只说明“布局已在其他窗口更新”,不要求重新拖动。损坏、未知 schema、身份冲突、超限与链接文件失败关闭,不能用空布局覆盖原文件。 - 本布局持久化切片不包含资源关系线、资源替换、聚焦态持久化、缩放 / 平移、搜索 / 筛选条件、当前 mode,也不修改 `api-server` 或 SpacetimeDB。资源关系线与当前会话内中央聚焦已在后续独立前端切片接入,不改变本段 sidecar 合同;其余 P1 能力继续独立实施。