修复资源画布溢出与模板资源投影
闭合资源画布横向收缩链并增加内部滚动条 过滤任务产物通配模板避免生成无效资源卡 补充布局与资源投影回归测试 同步工作台文档与共享排障记忆
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 资源依赖关系图验收
|
||||
|
||||
|
||||
@@ -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`,另一工作树运行时当前窗口可能被同名实例混淆,默认启动器则会为避免串台直接失败关闭。
|
||||
|
||||
@@ -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` 已占用而失败,或用户在两个同名窗口间误把另一工作树的页面当作当前修改结果。
|
||||
|
||||
@@ -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 能力继续独立实施。
|
||||
|
||||
Reference in New Issue
Block a user