Compare commits

..

34 Commits

Author SHA1 Message Date
k88936 4df4708ba1 Merge remote-tracking branch 'origin/master' into feat/ui-editor-v3
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 3m0s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 3m4s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 3m7s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 3m9s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m41s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m32s
Project CI / Frontend tests (pull_request) Failing after 4m47s
Project CI / Repository checks (pull_request) Failing after 4m39s
Project CI / Native shell tests (pull_request) Successful in 7m14s
Project CI / Backend tests (pull_request) Successful in 8m12s
Project CI / AI game creator shell web tests (pull_request) Failing after 2m54s
# Conflicts:
#	docs/project-memory/shared-memory/pitfalls.md
2026-09-16 14:57:26 +08:00
kdletters b3e9d0a906 发布 AGC 标准版 0.1.45
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m11s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m39s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m54s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 4m35s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m26s
Project CI / AI game creator shell Rust crates (push) Successful in 3m17s
Project CI / Frontend tests (push) Successful in 4m41s
Project CI / Repository checks (push) Successful in 3m53s
Project CI / Native shell tests (push) Successful in 6m43s
Project CI / Backend tests (push) Successful in 7m26s
Project CI / AI game creator shell web tests (push) Successful in 3m23s
- apps/ai-game-creator-shell/package.json 版本提升到 0.1.45
- apps/ai-game-creator-shell/src-tauri/tauri.conf.json 版本提升到 0.1.45
- apps/ai-game-creator-shell/src-tauri/Cargo.toml 版本提升到 0.1.45
- apps/ai-game-creator-shell/src-tauri/Cargo.lock 同步 genarrative-ai-game-creator-shell 版本
- package-lock.json 同步 apps/ai-game-creator-shell 工作区版本
2026-09-16 11:52:03 +08:00
kdletters a60328623d 调整开发调度管线备份策略
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m30s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m12s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m36s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m2s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m52s
Project CI / AI game creator shell Rust crates (push) Successful in 2m7s
Project CI / Frontend tests (push) Successful in 4m45s
Project CI / Repository checks (push) Successful in 4m42s
Project CI / Backend tests (push) Successful in 6m58s
Project CI / Native shell tests (push) Successful in 8m22s
Project CI / AI game creator shell web tests (push) Successful in 4m5s
恢复 Full Job 备份参数原样透传
开发调度统一传入 skip,默认不上传 OSS
保持 Full 与 AGC 使用同一固定 revision 参数
2026-09-16 11:43:22 +08:00
kdletters 9e63b76991 修正开发环境备份默认映射
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m7s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 4m56s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m37s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m45s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m18s
Project CI / AI game creator shell Rust crates (push) Successful in 2m48s
Project CI / Repository checks (push) Failing after 1m54s
Project CI / Frontend tests (push) Successful in 5m15s
Project CI / Backend tests (push) Failing after 7m46s
Project CI / AI game creator shell web tests (push) Failing after 4m21s
Project CI / Native shell tests (push) Successful in 8m53s
保留备份参数原有枚举顺序以通过运维门禁
开发目标未显式选择时将 async 映射为 skip
开发环境默认不上传 OSS,release 仍保留显式策略
2026-09-16 11:40:22 +08:00
kdletters a98ebcf68f 调整开发环境默认备份策略
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m30s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m35s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m55s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 6m0s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m4s
Project CI / AI game creator shell Rust crates (push) Successful in 2m33s
Project CI / Repository checks (push) Failing after 1m16s
Project CI / Frontend tests (push) Successful in 4m59s
Project CI / AI game creator shell web tests (push) Successful in 3m28s
Project CI / Native shell tests (push) Successful in 7m12s
Project CI / Backend tests (push) Successful in 8m33s
开发环境默认跳过 OSS 备份上传
需要冷备时显式选择 async
保持 release 发布的备份策略可配置
2026-09-16 11:37:56 +08:00
kdletters 576ff07a5e 新增定时版本调度管线并收回两条下游管线的定时与去重
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m9s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 5m11s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m34s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m1s
Project CI / AI game creator shell Rust crates (push) Successful in 2m36s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m20s
Project CI / Frontend tests (push) Successful in 4m23s
Project CI / Repository checks (push) Successful in 4m13s
Project CI / Native shell tests (push) Successful in 6m44s
Project CI / Backend tests (push) Successful in 8m22s
Project CI / AI game creator shell web tests (push) Successful in 3m28s
- 新增 Genarrative-Scheduled-Revision-Trigger:每小时用 git ls-remote 解析分支版本,与上一次触发过的 revision 不同才继续
- 触发时把同一个固定 COMMIT_HASH 同时传给 Full Build 与 AGC Windows Build,保证两条管线构建同一个版本
- Full Build 与 AGC Windows Build 去掉自带定时触发器与管线内版本去重
- 新增 scheduled-revision-trigger Job 配置 XML,使用本机 Git 入口与既有 SSH 凭据,定时器留在 Jenkinsfile
- 生产运维门禁改为校验调度管线约束,并拦住两条下游管线重新加 cron 或自行去重
- 同步开发运维文档、共享开发工作流与踩坑记录
2026-09-16 10:55:16 +08:00
kdletters 5aa616134c 修复 Hook 链继承仓库定位变量导致真实仓库被夹具污染
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m30s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m47s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 5m53s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m8s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m48s
Project CI / AI game creator shell Rust crates (push) Successful in 2m46s
Project CI / Frontend tests (push) Failing after 4m6s
Project CI / Repository checks (push) Successful in 3m57s
Project CI / Native shell tests (push) Successful in 7m40s
Project CI / Backend tests (push) Successful in 8m27s
Project CI / AI game creator shell web tests (push) Successful in 12m53s
- .husky/pre-commit 与 .husky/pre-push 入口清除 GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE 等仓库定位变量

- scripts/check-repository-ci.sh 增加同样的清理,保证本地门禁与 CI 走同一套隔离

- scripts/git-hooks.test.mjs 夹具 Git 调用前自检仓库归属并禁用 Hook,命令落到外部仓库时直接失败

- 守卫用例的子进程必须真的继承 GIT_DIR,避免隔离断言空转

- 更新开发运维文档与 shared-memory/pitfalls.md,记录链接工作树注入形态与配置修复步骤
2026-09-16 10:35:07 +08:00
kdletters 187b66c3b1 Jenkins 定时构建增加版本未变化去重
- Full Build 改为每小时触发,并按已解析 commit 与全部构建参数判断是否重复
- AGC Windows Build 增加每小时触发与同一套去重逻辑,跳过时不再安装依赖、构建和上传
- 跳过记录为 NOT_BUILT,越过跳过记录后最近一次实际构建成功才允许跳过,失败或中断可重试
- 生产运维门禁补充两个 Job 的去重约束与各阶段跳过检查
- 同步开发运维文档与共享开发工作流说明
2026-09-16 10:32:03 +08:00
k88936 1e992bcdf8 扩大未知 JavaScript 异常上报原则
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
明确所有未解决和非契约化 JS 异常必须继续抛出

说明用户 fallback 不得替代全局 error report 链路
2026-09-14 19:47:08 +08:00
k88936 8368aa262c 补充剪贴板异常上报约定
记录 UI fallback 后仍需抛出原始原生错误

明确未知异常必须进入 error report 链路
2026-09-14 19:43:08 +08:00
k88936 e9ddbf16da 重构预览网格步长计算
提取预览网格自适应步长到独立模块

补充缩放网格计算与背景样式测试

同步预览画布缩放交互文档
2026-09-14 19:40:13 +08:00
k88936 22ac1f4c0b 修复 UI 编辑器剪贴板写入权限
为主窗口补充 clipboard-manager 写文本权限

保留手动复制提示并继续抛出原始 Tauri 剪贴板错误
2026-09-14 19:39:34 +08:00
k88936 9910a0eec0 优化 UI 编辑器复制反馈布局
将复制失败提示与操作按钮分离排列
2026-09-14 19:06:15 +08:00
k88936 70981b9ca9 完善 UI 编辑器结果弹窗覆盖
增加普通保存成功与保存后生成失败的场景断言

验证路径弹窗和失败重试按钮的可见行为
2026-09-14 19:01:44 +08:00
k88936 8a0d5600b3 拆分 UI 编辑器路径复制按钮
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 2m27s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 2m23s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 2m5s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 2m8s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m57s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m15s
Project CI / Frontend tests (pull_request) Failing after 3m22s
Project CI / Repository checks (pull_request) Successful in 3m38s
Project CI / Native shell tests (pull_request) Successful in 6m42s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m12s
Project CI / Backend tests (pull_request) Successful in 8m24s
将剪贴板状态与写入逻辑提取为独立小组件

保持保存结果弹窗只负责结果布局与动作编排
2026-09-14 18:59:51 +08:00
k88936 184d88dbb8 修复多树预览受左侧选中图片限制
移除 activeImageId 对预览拖动和缩放手势的门控

按树 wrapper 提供几何上下文并允许任意树操作

保留 activeImageId 仅用于左侧图片与图片级检查
2026-09-14 18:59:51 +08:00
k88936 791794c0a6 记录 UI 编辑器保存结果弹窗约定
补充保存与代码生成结果弹窗及相对路径复制行为

记录复制失败、重试和保存并返回边界
2026-09-14 18:59:51 +08:00
k88936 9cfb47d945 补充 UI 编辑器保存结果测试
覆盖保存和生成成功弹窗与项目相对路径复制

覆盖保存失败、冲突和剪贴板失败反馈
2026-09-14 18:59:51 +08:00
k88936 27859d3e19 接入 UI 编辑器保存结果弹窗
让普通保存和保存并生成流程返回结构化结果

将成功与失败反馈接入独立弹窗并保留保存并返回直返行为
2026-09-14 18:59:51 +08:00
k88936 43f3780a38 新增 UI 编辑器保存结果弹窗
新增保存成功、代码生成成功和失败状态弹窗

提供项目相对路径复制按钮及复制失败提示
2026-09-14 18:59:51 +08:00
k88936 a1f9149c27 UI编辑器移除预览模式残留类型
完成三开关替代 render mode 的类型清理
2026-09-14 18:59:51 +08:00
k88936 f0bba18841 UI编辑器收口多树上下文菜单目标
预览菜单携带所属树 ID

避免非当前图片树的结构操作写错目标
2026-09-14 18:59:51 +08:00
k88936 f679cfa179 UI编辑器完善多树创建与文档契约
统一识别和合并结果的 createTree 流程

按选中节点所属树驱动 Inspector 编辑

补充多树预览与树级偏移权威文档
2026-09-14 18:59:50 +08:00
k88936 f68ffcfecd UI编辑器补充合并树偏移构造
为合并产出的节点补齐 NodeOffset
2026-09-14 18:59:50 +08:00
k88936 ec94a4fe00 UI编辑器重构多树预览与独立开关
预览画布同时渲染全部界面树并支持树级拖动

拆分显示框线、原图和组件开关

预览节点选择与左侧当前图片解耦
2026-09-14 18:59:50 +08:00
k88936 46f8592375 UI编辑器补充树级偏移模型
新增 Node offset min/max 数据结构

统一新树默认横向布局入口

增加树级偏移状态更新操作与实施计划
2026-09-14 18:59:50 +08:00
k88936 cdf2882302 补齐自动切分概览字体属性
向 SeparationOverview 传入 fonts 数据

修复 UI 编辑器 TS2741 类型错误
2026-09-14 18:59:50 +08:00
k88936 d156113e71 修复 UI 编辑器待办循环高亮动画
修复受控树选中回调清除 Inspector 高亮的问题

循环游标同时使用界面树与节点 ID

补充多节点循环和动画重启回归测试
2026-09-14 18:59:50 +08:00
k88936 a8f2ac17be 注释掉 InspectorSidebar 中的 LayoutEditor 组件以暂时禁用布局编辑功能 2026-09-14 18:59:50 +08:00
k88936 43ac9a5761 补充 UI 编辑器交互回归测试
从实际节点事件目标验证删除快捷键

验证 portal 右键菜单启用后会执行操作并关闭
2026-09-14 18:59:50 +08:00
k88936 e992e0b35c 压缩 UI 编辑器概览布局
将识别概览指标改为四列横向排列

将素材切分概览改为四列网格以减少纵向高度
2026-09-14 18:59:50 +08:00
k88936 e389b54a3a 修复 UI 预览画布交互
为无限视口添加跟随平移和缩放的点阵背景

恢复画布快捷缩放与初始适配比例控制

保留节点平移选择并屏蔽浏览器原生拖拽选择
2026-09-14 18:59:50 +08:00
k88936 84c780a6f8 重构 UI 编辑器全屏外壳
通过 portal 将资源编辑器挂载到 body 并覆盖标题栏以下区域

移除外层 FocusTrap,保留内部确认对话框的模态行为

统一左右浮动面板的顶部与底部留白
2026-09-14 18:59:50 +08:00
k88936 03f900fbe5 布局重构:
- 预览改为全区域背景层。
  - 左右面板、顶部流程条改为覆盖层,可遮挡预览。
  - 左右面板开合状态仅保存在当前 React 会话。
  - 增加左右面板“收起/展开”入口。
  - 编辑器关闭入口改为单独的 ×,继续通过 onBack 注入关闭逻辑。
  - 保留全局顶部栏与现有快捷键、业务状态、保存流程。
  - 底层页面仍由现有父级路由控制,不新增 URL 状态。
2026-09-14 18:59:50 +08:00
85 changed files with 2445 additions and 1750 deletions
+3
View File
@@ -1 +1,4 @@
# Git 在链接工作树里执行 Hook 时会注入 GIT_DIR 等仓库定位变量,优先级高于 cwd;
# 子进程(npm、lint-staged、测试夹具)会继承它们并写到真实仓库,故在入口统一清除。
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
npm run format:staged npm run format:staged
+3
View File
@@ -1 +1,4 @@
# Git 在链接工作树里执行 Hook 时会注入 GIT_DIR 等仓库定位变量,优先级高于 cwd;
# 钩子链(npm → check:repository-ci → 测试夹具)会继承它们并写到真实仓库,故在入口统一清除。
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
npm run check:pre-push-master -- "$@" npm run check:pre-push-master -- "$@"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@genarrative/ai-game-creator-shell", "name": "@genarrative/ai-game-creator-shell",
"private": true, "private": true,
"version": "0.1.29", "version": "0.1.45",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "node scripts/start-tauri-dev.mjs", "dev": "node scripts/start-tauri-dev.mjs",
+1 -1
View File
@@ -1725,7 +1725,7 @@ dependencies = [
[[package]] [[package]]
name = "genarrative-ai-game-creator-shell" name = "genarrative-ai-game-creator-shell"
version = "0.1.29" version = "0.1.45"
dependencies = [ dependencies = [
"agent-runtime-core", "agent-runtime-core",
"axum", "axum",
@@ -1,6 +1,6 @@
[package] [package]
name = "genarrative-ai-game-creator-shell" name = "genarrative-ai-game-creator-shell"
version = "0.1.29" version = "0.1.45"
edition = "2021" edition = "2021"
publish = false publish = false
@@ -1,11 +1,12 @@
{ {
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main", "identifier": "main",
"description": "AI 游戏创作主窗口允许读系统剪贴板图片,用于粘贴素材附件;允许弹出原生打开/保存对话框用于素材上传与导出。", "description": "AI 游戏创作主窗口允许读系统剪贴板,用于粘贴素材附件和复制生成文件路径;允许弹出原生打开/保存对话框用于素材上传与导出。",
"windows": ["client"], "windows": ["client"],
"permissions": [ "permissions": [
"clipboard-manager:allow-read-image", "clipboard-manager:allow-read-image",
"clipboard-manager:allow-read-text", "clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text",
"core:image:allow-rgba", "core:image:allow-rgba",
"core:image:allow-size", "core:image:allow-size",
"core:resources:allow-close", "core:resources:allow-close",
@@ -5,9 +5,8 @@ mod validation;
mod wire; mod wire;
pub(crate) use model::{ pub(crate) use model::{
DirectCodexUserAttachmentReferencePart, DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageEnvelope,
DirectCodexUserMessageEnvelope, DirectCodexUserMessageItem, DirectCodexUserRole, DirectCodexUserMessageItem, DirectCodexUserRole, DirectCodexUserRuntimeRegionPart,
DirectCodexUserRuntimeRegionPart,
}; };
pub(crate) use validation::validate_direct_codex_user_item; pub(crate) use validation::validate_direct_codex_user_item;
pub(crate) use wire::{ pub(crate) use wire::{
@@ -36,21 +36,6 @@ pub(crate) enum DirectCodexUserContentPart {
AgcResourceReference { resource_id: String }, AgcResourceReference { resource_id: String },
#[serde(rename = "agc_runtime_region_reference")] #[serde(rename = "agc_runtime_region_reference")]
AgcRuntimeRegionReference(DirectCodexUserRuntimeRegionPart), AgcRuntimeRegionReference(DirectCodexUserRuntimeRegionPart),
/// Uploaded project attachment kept inline in canonical content.
#[serde(rename = "agc_attachment_reference")]
AgcAttachmentReference(DirectCodexUserAttachmentReferencePart),
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))]
pub(crate) struct DirectCodexUserAttachmentReferencePart {
pub(crate) name: String,
pub(crate) media_type: String,
#[ts(type = "number")]
pub(crate) size: u64,
pub(crate) local_path: String,
pub(crate) status: String,
} }
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
@@ -20,16 +20,18 @@ pub(crate) fn validate_direct_codex_user_item(
if message.id.trim().is_empty() { if message.id.trim().is_empty() {
return Err("DirectProject user item 缺少稳定 id".to_string()); return Err("DirectProject user item 缺少稳定 id".to_string());
} }
// 有效输入只判一整条 content:单个纯空白 `input_text` 是合法 part —— 编辑器里的段落 if message.content.is_empty() {
// 分隔、软换行与 chip 后的分隔空格就是这样落进 canonical content 的,前端不为它过滤。
if !content_has_meaningful_input(&message.content) {
return Err("DirectProject user item content 不能为空".to_string()); return Err("DirectProject user item content 不能为空".to_string());
} }
let manifest = read_manifest_for_project(root)?; let manifest = read_manifest_for_project(root)?;
let mut reference_count = 0usize; let mut reference_count = 0usize;
for part in &message.content { for part in &message.content {
match part { match part {
DirectCodexUserContentPart::InputText { .. } => {} DirectCodexUserContentPart::InputText { text } => {
if text.trim().is_empty() {
return Err("DirectProject input_text 不能为空".to_string());
}
}
DirectCodexUserContentPart::AgcResourceReference { resource_id } => { DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
reference_count = reference_count.saturating_add(1); reference_count = reference_count.saturating_add(1);
validate_resource_id_and_manifest(&manifest, resource_id)?; validate_resource_id_and_manifest(&manifest, resource_id)?;
@@ -38,18 +40,6 @@ pub(crate) fn validate_direct_codex_user_item(
reference_count = reference_count.saturating_add(1); reference_count = reference_count.saturating_add(1);
validate_runtime_region_reference(&manifest, reference)?; validate_runtime_region_reference(&manifest, reference)?;
} }
DirectCodexUserContentPart::AgcAttachmentReference(reference) => {
if reference.name.trim().is_empty() {
return Err("附件缺少文件名".to_string());
}
if !reference.local_path.trim().is_empty() {
sanitize_attachment_local_path(&reference.local_path)
.ok_or_else(|| "附件项目路径无效".to_string())?;
}
if !matches!(reference.status.trim(), "imported" | "failed") {
return Err("附件状态无效".to_string());
}
}
} }
} }
if reference_count > MAX_DIRECT_CODEX_REFERENCES { if reference_count > MAX_DIRECT_CODEX_REFERENCES {
@@ -58,14 +48,6 @@ pub(crate) fn validate_direct_codex_user_item(
Ok(()) Ok(())
} }
/// 整条 content 是否还有有效输入:任何一段非空白文本、或任何一个非文本 part 都算。
pub(crate) fn content_has_meaningful_input(content: &[DirectCodexUserContentPart]) -> bool {
content.iter().any(|part| match part {
DirectCodexUserContentPart::InputText { text } => !text.trim().is_empty(),
_ => true,
})
}
pub(crate) fn validate_resource_id_and_manifest( pub(crate) fn validate_resource_id_and_manifest(
manifest: &GameCreationAppManifest, manifest: &GameCreationAppManifest,
resource_id: &str, resource_id: &str,
@@ -104,48 +86,3 @@ fn validate_runtime_region_reference(
} }
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use super::content_has_meaningful_input;
use crate::agent::direct_codex_user_item::model::DirectCodexUserContentPart;
fn input_text(text: &str) -> DirectCodexUserContentPart {
DirectCodexUserContentPart::InputText {
text: text.to_string(),
}
}
#[test]
fn only_all_blank_content_counts_as_empty_input() {
// 空数组与「整条只有空白」是同一种空输入。
assert!(!content_has_meaningful_input(&[]));
assert!(!content_has_meaningful_input(&[input_text(" \n ")]));
assert!(!content_has_meaningful_input(&[
input_text("\n"),
input_text(" "),
]));
}
#[test]
fn whitespace_parts_are_valid_next_to_meaningful_input() {
// 段落分隔 / 软换行 / chip 后的分隔空格都是合法的单个 part。
assert!(content_has_meaningful_input(&[
input_text("\n"),
input_text(""),
]));
assert!(content_has_meaningful_input(&[
input_text(""),
input_text("\n\n"),
]));
}
#[test]
fn non_text_parts_always_count_as_input() {
assert!(content_has_meaningful_input(&[
DirectCodexUserContentPart::AgcResourceReference {
resource_id: "asset-hero".to_string(),
},
]));
}
}
@@ -100,20 +100,6 @@ pub(crate) fn direct_codex_user_item_to_wire_input(
summary.push(']'); summary.push(']');
summary summary
} }
DirectCodexUserContentPart::AgcAttachmentReference(reference) => {
let mut summary = format!(
"[附件:名称={};类型={};大小={} 字节",
reference.name.trim(),
reference.media_type.trim(),
reference.size
);
if !reference.local_path.trim().is_empty() {
summary.push_str(&format!(";项目路径={}", reference.local_path.trim()));
}
summary.push_str(&format!(";状态={}", reference.status.trim()));
summary.push(']');
summary
}
}; };
input.push(serde_json::json!({ "type": "text", "text": text })); input.push(serde_json::json!({ "type": "text", "text": text }));
} }
@@ -144,8 +130,7 @@ pub(crate) fn direct_codex_user_item_to_prompt(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{direct_codex_user_item_to_response_item, direct_codex_user_item_to_wire_input}; use super::direct_codex_user_item_to_response_item;
use crate::agent::direct_codex_user_item::model::DirectCodexUserItem;
use serde_json::json; use serde_json::json;
use std::path::Path; use std::path::Path;
@@ -189,70 +174,4 @@ mod tests {
.expect_err("history item without type must fail"); .expect_err("history item without type must fail");
assert!(error.contains("缺少 type"), "{error}"); assert!(error.contains("缺少 type"), "{error}");
} }
#[test]
fn attachment_parts_remain_in_canonical_order_when_projected() {
let root = tempfile::tempdir().expect("temp project");
crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试")
.expect("init project");
let item = json!({
"type": "message",
"role": "user",
"id": "turn-1:user",
"content": [
{"type": "input_text", "text": "先看"},
{"type": "agc_attachment_reference", "name": "notes.txt", "mediaType": "text/plain", "size": 4, "localPath": "assets/notes.txt", "status": "imported"}
]
});
let projected = direct_codex_user_item_to_response_item(root.path(), &item)
.expect("user response item should project");
let content = projected["content"].as_array().expect("content array");
assert_eq!(content.len(), 2);
assert!(content[0]["text"].as_str().unwrap().contains("先看"));
assert!(content[1]["text"].as_str().unwrap().contains("notes.txt"));
}
#[test]
fn whitespace_only_text_parts_survive_validation() {
let root = tempfile::tempdir().expect("temp project");
crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试")
.expect("init project");
let item: DirectCodexUserItem = serde_json::from_value(json!({
"type": "message",
"role": "user",
"id": "turn-1:user",
"content": [
{"type": "input_text", "text": "先看"},
{"type": "input_text", "text": "\n"},
{"type": "input_text", "text": " "}
]
}))
.expect("deserialize user item");
let wire = direct_codex_user_item_to_wire_input(root.path(), &item)
.expect("whitespace-only part next to real text must pass");
let parts = wire.as_array().expect("wire input array");
assert_eq!(parts.len(), 3);
assert_eq!(parts[1]["text"].as_str(), Some("\n"));
assert_eq!(parts[2]["text"].as_str(), Some(" "));
}
#[test]
fn all_blank_content_is_rejected() {
let root = tempfile::tempdir().expect("temp project");
crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试")
.expect("init project");
let item: DirectCodexUserItem = serde_json::from_value(json!({
"type": "message",
"role": "user",
"id": "turn-1:user",
"content": [
{"type": "input_text", "text": "\n"},
{"type": "input_text", "text": " "}
]
}))
.expect("deserialize user item");
let error = direct_codex_user_item_to_wire_input(root.path(), &item)
.expect_err("all-blank content must fail closed");
assert!(error.contains("不能为空"), "{error}");
}
} }
@@ -31,9 +31,10 @@ pub(crate) fn normalize_direct_client_turn_id(
pub(crate) async fn chat_with_game_creator_direct_codex( pub(crate) async fn chat_with_game_creator_direct_codex(
project_path: String, project_path: String,
prompt: String, prompt: String,
user_item: DirectCodexUserItem, mut user_item: DirectCodexUserItem,
creation_type: Option<String>, creation_type: Option<String>,
client_turn_id: Option<String>, client_turn_id: Option<String>,
attachments: Option<Vec<DirectCodexTurnAttachment>>,
) -> Result<String, String> { ) -> Result<String, String> {
let root = Path::new(project_path.trim()); let root = Path::new(project_path.trim());
let turn_id = normalize_direct_client_turn_id(client_turn_id.as_deref())?; let turn_id = normalize_direct_client_turn_id(client_turn_id.as_deref())?;
@@ -42,7 +43,24 @@ pub(crate) async fn chat_with_game_creator_direct_codex(
redact_agent_runtime_error(root, &format!("恢复上一轮陶泥儿整包事务失败:{error}"), 500) redact_agent_runtime_error(root, &format!("恢复上一轮陶泥儿整包事务失败:{error}"), 500)
})?; })?;
let turn_emitter = DirectGameCreatorTurnUpdateEmitter::new(root, turn_id.clone()); let turn_emitter = DirectGameCreatorTurnUpdateEmitter::new(root, turn_id.clone());
let mut audit = DirectCodexTurnAudit::start(root, &turn_id, &prompt, &[]); let mut audit = DirectCodexTurnAudit::start(
root,
&turn_id,
&prompt,
attachments.as_deref().unwrap_or_default(),
);
let attachments = attachments.unwrap_or_default();
if !attachments.is_empty() {
let attachment_context =
render_direct_codex_user_prompt("", &attachments).map_err(|error| {
audit.finish(false);
error
})?;
let DirectCodexUserItem::Message(message) = &mut user_item;
message.content.push(DirectCodexUserContentPart::InputText {
text: attachment_context,
});
}
validate_direct_codex_user_item(root, &user_item).map_err(|error| { validate_direct_codex_user_item(root, &user_item).map_err(|error| {
audit.finish(false); audit.finish(false);
error error
@@ -172,6 +172,7 @@ mod materialize {
use crate::ui_editor::layout::node::{ use crate::ui_editor::layout::node::{
Node as LayoutNode, NodeMetadata, NodeSource, StageStatus, Node as LayoutNode, NodeMetadata, NodeSource, StageStatus,
}; };
use crate::ui_editor::layout::offset::NodeOffset;
use crate::ui_editor::layout::transform::Transform; use crate::ui_editor::layout::transform::Transform;
use crate::ui_editor::state::{State, UITree}; use crate::ui_editor::state::{State, UITree};
use crate::ui_editor::utils::{random_node_id, NodeId, UIDesignImageId}; use crate::ui_editor::utils::{random_node_id, NodeId, UIDesignImageId};
@@ -302,6 +303,7 @@ mod materialize {
component: None, component: None,
children_display_mode: ChildrenDisplayMode::Exclusive, children_display_mode: ChildrenDisplayMode::Exclusive,
children: members.into_iter().map(|member| member.node).collect(), children: members.into_iter().map(|member| member.node).collect(),
offset: NodeOffset::default(),
}, },
priority, priority,
src_ui_design, src_ui_design,
@@ -9,6 +9,7 @@ use crate::ui_editor::layout::children_display_mode::ChildrenDisplayMode;
use crate::ui_editor::layout::control_layout::ControlLayout; use crate::ui_editor::layout::control_layout::ControlLayout;
use crate::ui_editor::layout::dimension::UIRect; use crate::ui_editor::layout::dimension::UIRect;
use crate::ui_editor::layout::node::{Node as LayoutNode, NodeMetadata, NodeSource, StageStatus}; use crate::ui_editor::layout::node::{Node as LayoutNode, NodeMetadata, NodeSource, StageStatus};
use crate::ui_editor::layout::offset::NodeOffset;
use crate::ui_editor::layout::transform::Transform; use crate::ui_editor::layout::transform::Transform;
use crate::ui_editor::resource::ui_design_image::UIDesignImage; use crate::ui_editor::resource::ui_design_image::UIDesignImage;
use crate::ui_editor::state::{State, UITree}; use crate::ui_editor::state::{State, UITree};
@@ -322,6 +323,7 @@ fn convert_node(
component: source.component.clone().into_option(), component: source.component.clone().into_option(),
children_display_mode: ChildrenDisplayMode::Stack, children_display_mode: ChildrenDisplayMode::Stack,
children, children,
offset: NodeOffset::default(),
}) })
} }
@@ -858,6 +860,7 @@ pub(crate) async fn recognize_ui_impl_with_provider(
component: recognition_root.component.into_option(), component: recognition_root.component.into_option(),
children_display_mode: ChildrenDisplayMode::Stack, children_display_mode: ChildrenDisplayMode::Stack,
children, children,
offset: NodeOffset::default(),
}; };
// Tree identity and root identity are assigned by Rust, never chosen by the model. // Tree identity and root identity are assigned by Rust, never chosen by the model.
ui_trees.push(UITree { ui_trees.push(UITree {
@@ -2,4 +2,5 @@ pub mod children_display_mode;
pub mod control_layout; pub mod control_layout;
pub mod dimension; pub mod dimension;
pub mod node; pub mod node;
pub mod offset;
pub mod transform; pub mod transform;
@@ -1,6 +1,7 @@
use crate::ui_editor::component::Component; use crate::ui_editor::component::Component;
use crate::ui_editor::layout::children_display_mode::ChildrenDisplayMode; use crate::ui_editor::layout::children_display_mode::ChildrenDisplayMode;
use crate::ui_editor::layout::control_layout::ControlLayout; use crate::ui_editor::layout::control_layout::ControlLayout;
use crate::ui_editor::layout::offset::NodeOffset;
use crate::ui_editor::utils::NodeId; use crate::ui_editor::utils::NodeId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
@@ -14,6 +15,7 @@ pub struct Node {
pub component: Option<Component>, pub component: Option<Component>,
pub children_display_mode: ChildrenDisplayMode, pub children_display_mode: ChildrenDisplayMode,
pub children: Vec<Node>, pub children: Vec<Node>,
pub offset: NodeOffset,
} }
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)]
@@ -0,0 +1,18 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/ui-editor/types/"))]
pub struct NodeOffset {
pub min: [f32; 2],
pub max: [f32; 2],
}
impl Default for NodeOffset {
fn default() -> Self {
Self {
min: [0.0, 0.0],
max: [0.0, 0.0],
}
}
}
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "陶泥儿", "productName": "陶泥儿",
"version": "0.1.29", "version": "0.1.45",
"identifier": "world.genarrative.ai-game-creator", "identifier": "world.genarrative.ai-game-creator",
"build": { "build": {
"beforeDevCommand": "npm --prefix ../.. run agc:serve", "beforeDevCommand": "npm --prefix ../.. run agc:serve",
File diff suppressed because it is too large Load Diff
@@ -1,102 +0,0 @@
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import {
COMMAND_PRIORITY_HIGH,
type EditorState,
KEY_ENTER_COMMAND,
type Klass,
type LexicalNode,
} from 'lexical';
import type { ReactElement, ReactNode, Ref } from 'react';
import { useEffect } from 'react';
type RichTextInputProps = {
namespace: string;
nodes: Klass<LexicalNode>[];
initialEditorState?: EditorState | null;
contentEditable?: ReactElement<typeof ContentEditable>;
placeholder?: ReactElement;
containerClassName?: string;
containerRef?: Ref<HTMLDivElement>;
disabled?: boolean;
onChange?: (editorState: EditorState) => void;
onEnter?: () => void;
children?: ReactNode;
};
function SubmitOnEnter({ onEnter }: { onEnter?: () => void }) {
const [editor] = useLexicalComposerContext();
useEffect(() => {
if (!onEnter) return undefined;
return editor.registerCommand(
KEY_ENTER_COMMAND,
(event) => {
if (!event || event.shiftKey || event.isComposing) return false;
event.preventDefault();
onEnter();
return true;
},
COMMAND_PRIORITY_HIGH,
);
}, [editor, onEnter]);
return null;
}
function SetEditorEditable({ disabled }: { disabled: boolean }) {
const [editor] = useLexicalComposerContext();
useEffect(() => {
editor.setEditable(!disabled);
}, [disabled, editor]);
return null;
}
export default function RichTextInput({
namespace,
nodes,
initialEditorState,
contentEditable = <ContentEditable />,
placeholder,
containerClassName,
containerRef,
disabled = false,
onChange,
onEnter,
children,
}: RichTextInputProps) {
return (
<LexicalComposer
initialConfig={{
namespace,
nodes,
editorState: initialEditorState ?? undefined,
onError: (error) => {
throw error;
},
}}
>
<div
ref={containerRef}
className={containerClassName}
data-disabled={disabled ? 'true' : undefined}
>
<RichTextPlugin
contentEditable={contentEditable}
placeholder={placeholder}
ErrorBoundary={LexicalErrorBoundary}
/>
{children}
<SetEditorEditable disabled={disabled} />
<SubmitOnEnter onEnter={onEnter} />
{onChange ? <OnChangePlugin onChange={onChange} /> : null}
</div>
</LexicalComposer>
);
}
@@ -19,7 +19,6 @@ import {
import type { RefObject } from 'react'; import type { RefObject } from 'react';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import type { GameCreationAppAssetManifestEntry } from '../../../../../packages/shared/src/contracts/gameCreationApp';
import { resolveTauriInvoke } from '../../app/tauri'; import { resolveTauriInvoke } from '../../app/tauri';
import type { import type {
GameCreatorAppConfigView, GameCreatorAppConfigView,
@@ -193,12 +192,9 @@ export function ComposerPendingAttachments({
/** 队列 chip:回合运行中入队的消息,按 FIFO 顺序展示,可单条取消。 */ /** 队列 chip:回合运行中入队的消息,按 FIFO 顺序展示,可单条取消。 */
export function ComposerTurnQueue({ export function ComposerTurnQueue({
turns, turns,
assets,
onCancel, onCancel,
}: { }: {
turns: readonly QueuedChatTurn[]; turns: readonly QueuedChatTurn[];
/** 与聊天消息渲染同源的素材清单:chip 文案里的 `@` 引用按它展开成显示名。 */
assets: readonly GameCreationAppAssetManifestEntry[];
onCancel: (id: string) => void; onCancel: (id: string) => void;
}) { }) {
if (turns.length === 0) { if (turns.length === 0) {
@@ -215,11 +211,11 @@ export function ComposerTurnQueue({
{index + 1} {index + 1}
</span> </span>
<span className="project-supervisor-composer-queue-text"> <span className="project-supervisor-composer-queue-text">
{queuedChatTurnLabel(turn, assets)} {queuedChatTurnLabel(turn)}
</span> </span>
<button <button
type="button" type="button"
aria-label={`取消排队消息 ${queuedChatTurnLabel(turn, assets)}`} aria-label={`取消排队消息 ${queuedChatTurnLabel(turn)}`}
title="取消这条排队消息" title="取消这条排队消息"
onClick={() => onCancel(turn.id)} onClick={() => onCancel(turn.id)}
> >
@@ -79,7 +79,7 @@ import {
ResourceReferenceInput, ResourceReferenceInput,
type ResourceReferenceInputHandle, type ResourceReferenceInputHandle,
} from './ResourceReferenceInput'; } from './ResourceReferenceInput';
import type { ChatComposerDraft } from './resourceReferences'; import type { ChatComposerDraft, ChatReference } from './resourceReferences';
import { ToolCallGroup } from './ToolCallGroup'; import { ToolCallGroup } from './ToolCallGroup';
import { import {
formatClockTime, formatClockTime,
@@ -242,6 +242,8 @@ type ProjectSupervisorViewProps = RuntimePanelProps & {
attachments?: DirectCodexTurnAttachment[]; attachments?: DirectCodexTurnAttachment[];
/** 上传/校验附件的提示文案(失败与成功都用它,空串不渲染)。 */ /** 上传/校验附件的提示文案(失败与成功都用它,空串不渲染)。 */
attachmentNotice?: string; attachmentNotice?: string;
chatInput: string;
chatReferences: ChatReference[];
chatProjectAssets: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[]; chatProjectAssets: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[];
composerRef?: RefObject<ResourceReferenceInputHandle | null>; composerRef?: RefObject<ResourceReferenceInputHandle | null>;
directCodex?: boolean; directCodex?: boolean;
@@ -325,6 +327,8 @@ export function ProjectSupervisorView({
activeVersionId = null, activeVersionId = null,
attachments = [], attachments = [],
attachmentNotice = '', attachmentNotice = '',
chatInput,
chatReferences,
chatProjectAssets, chatProjectAssets,
composerRef, composerRef,
directCodex = false, directCodex = false,
@@ -889,7 +893,6 @@ export function ProjectSupervisorView({
> >
<ComposerTurnQueue <ComposerTurnQueue
turns={directCodex ? queuedTurns : []} turns={directCodex ? queuedTurns : []}
assets={chatProjectAssets}
onCancel={(id) => onCancelQueuedTurn?.(id)} onCancel={(id) => onCancelQueuedTurn?.(id)}
/> />
<ComposerPendingAttachments <ComposerPendingAttachments
@@ -913,6 +916,8 @@ export function ProjectSupervisorView({
Boolean(designView?.session.pendingClarification) Boolean(designView?.session.pendingClarification)
} }
rows={3} rows={3}
value={chatInput}
references={chatReferences}
showTriggerButton={!directCodex} showTriggerButton={!directCodex}
placeholder={ placeholder={
directCodex directCodex
@@ -55,7 +55,7 @@ import {
ResourceReferenceInput, ResourceReferenceInput,
type ResourceReferenceInputHandle, type ResourceReferenceInputHandle,
} from './ResourceReferenceInput'; } from './ResourceReferenceInput';
import type { ChatComposerDraft } from './resourceReferences'; import type { ChatComposerDraft, ChatReference } from './resourceReferences';
type ProjectWorkspaceChatPaneProps = { type ProjectWorkspaceChatPaneProps = {
activeVersionId?: string | null; activeVersionId?: string | null;
@@ -65,6 +65,8 @@ type ProjectWorkspaceChatPaneProps = {
cancelProjectCreateInNonEmptyFolder: () => void; cancelProjectCreateInNonEmptyFolder: () => void;
cancelUiCommandConfirmation: () => void; cancelUiCommandConfirmation: () => void;
chatAgentBusy: boolean; chatAgentBusy: boolean;
chatInput: string;
chatReferences: ChatReference[];
chatProjectAssets: GameCreationAppAssetManifestEntry[]; chatProjectAssets: GameCreationAppAssetManifestEntry[];
composerRef?: Ref<ResourceReferenceInputHandle>; composerRef?: Ref<ResourceReferenceInputHandle>;
chatInputRef: RefObject<HTMLDivElement | null>; chatInputRef: RefObject<HTMLDivElement | null>;
@@ -213,6 +215,8 @@ export function ProjectWorkspaceChatPane({
cancelProjectCreateInNonEmptyFolder, cancelProjectCreateInNonEmptyFolder,
cancelUiCommandConfirmation, cancelUiCommandConfirmation,
chatAgentBusy, chatAgentBusy,
chatInput,
chatReferences,
chatProjectAssets, chatProjectAssets,
composerRef, composerRef,
chatInputRef, chatInputRef,
@@ -958,6 +962,8 @@ export function ProjectWorkspaceChatPane({
projectPath={projectPath} projectPath={projectPath}
disabled={chatAgentBusy || projectSupervisorNeedsUserInput} disabled={chatAgentBusy || projectSupervisorNeedsUserInput}
multiline={false} multiline={false}
value={chatInput}
references={chatReferences}
placeholder="例如:像素风横版动作小游戏,或输入 @ 选择资源" placeholder="例如:像素风横版动作小游戏,或输入 @ 选择资源"
onChange={onChatComposerChange} onChange={onChatComposerChange}
/> />
File diff suppressed because it is too large Load Diff
@@ -33,7 +33,7 @@ import {
ResourceReferenceInput, ResourceReferenceInput,
type ResourceReferenceInputHandle, type ResourceReferenceInputHandle,
} from './ResourceReferenceInput'; } from './ResourceReferenceInput';
import type { ChatComposerDraft } from './resourceReferences'; import type { ChatComposerDraft, ChatReference } from './resourceReferences';
type RuntimeControlProps = ComponentProps< type RuntimeControlProps = ComponentProps<
typeof ProjectSupervisorRuntimeControls typeof ProjectSupervisorRuntimeControls
@@ -44,6 +44,8 @@ const CHAT_SCROLL_BOTTOM_THRESHOLD = 24;
type SupervisorChatOnlyViewProps = { type SupervisorChatOnlyViewProps = {
activeVersionId?: string | null; activeVersionId?: string | null;
chatAgentBusy: boolean; chatAgentBusy: boolean;
chatInput: string;
chatReferences: ChatReference[];
chatProjectAssets: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[]; chatProjectAssets: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[];
composerRef?: Ref<ResourceReferenceInputHandle>; composerRef?: Ref<ResourceReferenceInputHandle>;
directCodex?: boolean; directCodex?: boolean;
@@ -80,6 +82,8 @@ type SupervisorChatOnlyViewProps = {
export function SupervisorChatOnlyView({ export function SupervisorChatOnlyView({
activeVersionId = null, activeVersionId = null,
chatAgentBusy, chatAgentBusy,
chatInput,
chatReferences,
chatProjectAssets, chatProjectAssets,
composerRef, composerRef,
directCodex = false, directCodex = false,
@@ -324,6 +328,8 @@ export function SupervisorChatOnlyView({
projectPath={projectPath} projectPath={projectPath}
disabled={chatAgentBusy || needsUserInput} disabled={chatAgentBusy || needsUserInput}
rows={3} rows={3}
value={chatInput}
references={chatReferences}
placeholder={ placeholder={
directCodex ? '描述你的想法' : '给项目总控 Agent 发消息' directCodex ? '描述你的想法' : '给项目总控 Agent 发消息'
} }
@@ -4,33 +4,36 @@
* 回合运行中用户再次发送时,消息进入 FIFO 队列而不是被丢弃;当前回合结束后按入队顺序 * 回合运行中用户再次发送时,消息进入 FIFO 队列而不是被丢弃;当前回合结束后按入队顺序
* 依次发出。队列项能在输入盒上方单独取消。这里只放与 React 无关的纯逻辑,便于单测。 * 依次发出。队列项能在输入盒上方单独取消。这里只放与 React 无关的纯逻辑,便于单测。
*/ */
import type { GameCreationAppAssetManifestEntry } from '../../../../../packages/shared/src/contracts/gameCreationApp'; import type { DirectCodexTurnAttachment } from '../app-shell/directCodexTurnAttachments';
import type { DirectCodexUserItem } from './generated'; import type { DirectCodexUserContentPart } from './generated';
import { directCodexContentToPromptText } from './resourceReferences'; import type { ChatReference } from './resourceReferences';
/** 队列上限:满了以后拒绝入队并给出可读提示,而不是静默丢消息。 */ /** 队列上限:满了以后拒绝入队并给出可读提示,而不是静默丢消息。 */
export const MAX_QUEUED_CHAT_TURNS = 5; export const MAX_QUEUED_CHAT_TURNS = 5;
export type QueuedChatTurn = { export type QueuedChatTurn = {
id: string; id: string;
clientTurnId: string; prompt: string;
userItem: DirectCodexUserItem; attachments: DirectCodexTurnAttachment[];
references: ChatReference[];
content?: DirectCodexUserContentPart[];
createdAt: number; createdAt: number;
}; };
export function createQueuedChatTurn(input: { export function createQueuedChatTurn(input: {
id: string; id: string;
clientTurnId: string; prompt: string;
userItem: DirectCodexUserItem; attachments?: readonly DirectCodexTurnAttachment[];
references?: readonly ChatReference[];
content?: readonly DirectCodexUserContentPart[];
createdAt: number; createdAt: number;
}): QueuedChatTurn { }): QueuedChatTurn {
return { return {
id: input.id, id: input.id,
clientTurnId: input.clientTurnId, prompt: input.prompt,
userItem: { attachments: [...(input.attachments ?? [])],
...input.userItem, references: [...(input.references ?? [])],
content: [...input.userItem.content], content: [...(input.content ?? [])],
},
createdAt: input.createdAt, createdAt: input.createdAt,
}; };
} }
@@ -74,30 +77,16 @@ export function chatQueueFullNotice(): string {
return `队列已满(最多 ${MAX_QUEUED_CHAT_TURNS} 条),请等当前回合结束后再发送`; return `队列已满(最多 ${MAX_QUEUED_CHAT_TURNS} 条),请等当前回合结束后再发送`;
} }
/** /** 队列 chip 上显示的文字:单行、有长度上限。 */
* 队列 chip 上显示的文字:与真实消息同一个派生(`directCodexContentToPromptText`), export function queuedChatTurnLabel(turn: QueuedChatTurn): string {
* 再压成单行并限长。 const text = turn.prompt.trim().replace(/\s+/gu, ' ');
*
* `assets` 与聊天消息渲染同源(`manifest.assets`)且必填:`@` 引用按显示名展开,chip
* 与消息正文逐字一致,不会露出 `@asset:…` 这种内部 id。
*/
export function queuedChatTurnLabel(
turn: QueuedChatTurn,
assets: readonly GameCreationAppAssetManifestEntry[],
): string {
const text = directCodexContentToPromptText(turn.userItem.content, assets)
.trim()
.replace(/\s+/gu, ' ');
if (text) { if (text) {
return text.length > 24 ? `${text.slice(0, 24)}` : text; return text.length > 24 ? `${text.slice(0, 24)}` : text;
} }
const attachment = turn.userItem.content.find( if (turn.attachments.length > 0) {
(part) => part.type === 'agc_attachment_reference', return `附件 · ${turn.attachments[0]?.name ?? '未命名'}`;
);
if (attachment?.type === 'agc_attachment_reference') {
return `附件 · ${attachment.name || '未命名'}`;
} }
if (turn.userItem.content.some((part) => part.type !== 'input_text')) { if (turn.references.length > 0) {
return '素材引用'; return '素材引用';
} }
return '未命名消息'; return '未命名消息';

Some files were not shown because too many files have changed in this diff Show More