合并主分支并重放资源栏目改动
Project CI / Repository checks (pull_request) Successful in 4m15s
Project CI / Frontend tests (pull_request) Successful in 4m47s
Project CI / Backend tests (pull_request) Successful in 5m49s
Project CI / Native shell tests (pull_request) Successful in 12m8s

合入已审查修复的 Game Agent 资源画板主线基线

保留资源栏目分页、悬浮导航与无限坐标交互

保留项目资源预览刷新与生成入口回归覆盖
This commit is contained in:
2026-08-24 16:36:54 +08:00
33 changed files with 4632 additions and 2057 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@genarrative/ai-game-creator-shell",
"private": true,
"version": "0.1.1",
"version": "0.1.2",
"type": "module",
"scripts": {
"dev": "node scripts/start-tauri-dev.mjs",
@@ -1733,12 +1733,12 @@ if (
}
if (
tauriConfig.version !== '0.1.1' ||
packageConfig.version !== '0.1.1' ||
cargoPackageVersion !== '0.1.1'
tauriConfig.version !== '0.1.2' ||
packageConfig.version !== '0.1.2' ||
cargoPackageVersion !== '0.1.2'
) {
throw new Error(
'AI game creator standard release must remain version 0.1.1 while game-chat uses its dedicated version',
'AI game creator standard release must remain version 0.1.2 while game-chat uses its dedicated version',
);
}
+1 -1
View File
@@ -1695,7 +1695,7 @@ dependencies = [
[[package]]
name = "genarrative-ai-game-creator-shell"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"agent-runtime-core",
"axum",
@@ -1,6 +1,6 @@
[package]
name = "genarrative-ai-game-creator-shell"
version = "0.1.1"
version = "0.1.2"
edition = "2021"
publish = false
@@ -64,6 +64,7 @@ const DIRECT_CODEX_ART_ASSET_PATHS: [&str; 3] = [
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
DIRECT_CODEX_SPRITESHEET_ASSET_PATH,
];
const DIRECT_CODEX_ART_AGENT_ID: &str = "direct-codex-art";
const DIRECT_CODEX_GAME_OUTPUTS: [(&str, &str, &str); 3] = [
("game/index.html", "game-entry", "text/html"),
("game/style.css", "game-style", "text/css"),
@@ -1872,7 +1873,7 @@ fn direct_taonier_art_generation_runtime_context(
_ => return Err("直连美术生成请求包含未声明的输出路径".to_string()),
};
Ok(PlatformArtGenerationRuntimeContext {
agent_id: "direct-codex-art".to_string(),
agent_id: DIRECT_CODEX_ART_AGENT_ID.to_string(),
task_id: format!("direct-codex-art-{stage}"),
session_id: project_id,
run_id: stage.to_string(),
@@ -2607,6 +2608,7 @@ async fn recover_direct_taonier_spritesheet_read_only_at(
json_string_field(resource, "objectKey").as_deref(),
&asset_object_id,
)?;
emit_game_creator_manifest_invalidated(root, DIRECT_CODEX_ART_AGENT_ID);
emit_direct_game_creator_progress(
root,
"art.spritesheet.recovered",
@@ -2744,6 +2746,7 @@ async fn generate_direct_taonier_art_asset_at(
}
Err(error) => return Err(direct_taonier_art_generation_failure(asset_label, error)),
};
emit_game_creator_manifest_invalidated(root, DIRECT_CODEX_ART_AGENT_ID);
if generated.asset.local_path != output_path {
return Err(format!(
"陶泥儿美术包生成返回后未形成可用的已登记平台素材合同 {output_path},已终止代码生成"
@@ -20,8 +20,7 @@ pub(crate) use canvas_generation::{
external_generation_submit_rejection_is_definitive,
platform_art_generation_error_needs_reconciliation, prepare_external_canvas_generation_context,
resolve_canvas_resource_download_with_access, submit_external_generation_request,
wait_for_external_generation_result, wait_for_external_generation_result_with_access,
ExternalCanvasGenerationContext,
wait_for_external_generation_result_with_access, ExternalCanvasGenerationContext,
ExternalGenerationInitialResponse,
};
pub(in crate::agent) use canvas_generation::{
@@ -261,10 +261,11 @@ pub(crate) use entrypoints::{
chat_with_game_creator_role_agent_stream_at,
chat_with_game_creator_role_agent_stream_for_session_at,
configure_game_creator_manifest_invalidation_event_sink, emit_direct_game_creator_progress,
emit_game_creator_agent_runtime_update, game_creator_agent_runtime_update_event,
generate_local_game_draft_at, install_game_creator_manifest_invalidation_event_sink,
read_game_creator_agent_runtime_at, read_game_creator_agent_runtime_for_session_at,
read_game_creator_agent_runtimes_at, set_game_creator_agent_runtime_update_app_handle,
emit_game_creator_agent_runtime_update, emit_game_creator_manifest_invalidated,
game_creator_agent_runtime_update_event, generate_local_game_draft_at,
install_game_creator_manifest_invalidation_event_sink, read_game_creator_agent_runtime_at,
read_game_creator_agent_runtime_for_session_at, read_game_creator_agent_runtimes_at,
set_game_creator_agent_runtime_update_app_handle,
start_game_creator_manifest_invalidation_event_sink,
validate_game_creator_manifest_invalidation_event_sink,
};
@@ -717,6 +717,14 @@ pub(crate) async fn generate_local_project_asset_canvas_image(
Ok(execution.result)
}
#[tauri::command]
pub(crate) async fn finalize_local_project_asset_canvas_generation_failure(
input: FinalizeAssetCanvasGenerationFailureInput,
) -> Result<FinalizeAssetCanvasGenerationFailureResult, String> {
let root = validated_local_project_directory_path(input.project_path.trim())?;
finalize_asset_canvas_generation_failure_at(&root, &input).await
}
#[tauri::command]
pub(crate) async fn recover_local_project_asset_canvas_generations(
app: tauri::AppHandle,
@@ -2370,6 +2370,7 @@ fn main() {
import_local_project_asset_canvas_images,
store_local_project_asset_canvas_media,
stage_local_project_asset_canvas_image,
finalize_local_project_asset_canvas_generation_failure,
archive_failed_local_project_asset_canvas_generation,
generate_local_project_asset_canvas_image,
recover_local_project_asset_canvas_generations,
@@ -2216,7 +2216,7 @@ fn stage_asset_canvas_image_with_token_at(
})
}
fn rebind_asset_canvas_staged_image_revision_at(
fn rebind_asset_canvas_staged_image_revision_locked(
root: &Path,
staged_image_token: &str,
expected_project_id: &str,
@@ -2224,11 +2224,6 @@ fn rebind_asset_canvas_staged_image_revision_at(
expected_previous_revision: u64,
next_revision: u64,
) -> Result<(), String> {
validate_uuid_v4(staged_image_token, "stagedImageToken")?;
validate_safe_revision(expected_previous_revision, "staging 原草稿 revision")?;
validate_safe_revision(next_revision, "staging 新草稿 revision")?;
validate_asset_canvas_project_identity(root, expected_project_id)?;
let _lock = acquire_asset_canvas_draft_lock(root)?;
let manifest = validate_asset_canvas_project_identity(root, expected_project_id)?;
let draft = read_asset_canvas_draft_locked(root, &manifest.project_id, draft_id)?
.ok_or_else(|| "素材画布草稿不存在".to_string())?;
@@ -2255,6 +2250,29 @@ fn rebind_asset_canvas_staged_image_revision_at(
)
}
fn rebind_asset_canvas_staged_image_revision_at(
root: &Path,
staged_image_token: &str,
expected_project_id: &str,
draft_id: &str,
expected_previous_revision: u64,
next_revision: u64,
) -> Result<(), String> {
validate_uuid_v4(staged_image_token, "stagedImageToken")?;
validate_safe_revision(expected_previous_revision, "staging 原草稿 revision")?;
validate_safe_revision(next_revision, "staging 新草稿 revision")?;
validate_asset_canvas_project_identity(root, expected_project_id)?;
let _lock = acquire_asset_canvas_draft_lock(root)?;
rebind_asset_canvas_staged_image_revision_locked(
root,
staged_image_token,
expected_project_id,
draft_id,
expected_previous_revision,
next_revision,
)
}
fn draft_media_relative_path(
draft_id: &str,
media_ref: &AssetCanvasMediaRef,
File diff suppressed because it is too large Load Diff
@@ -129,6 +129,8 @@ fn initialize_refine_fixture_with_resource_id(resource_id: Option<&str>) -> Fixt
kind: "illustration".to_string(),
media_type: "image/png".to_string(),
local_path: "assets/source.png".to_string(),
image_sequence_frames: None,
image_sequence_duration_ms: None,
source: GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Uploaded,
canvas_project_id: None,
@@ -748,6 +750,8 @@ fn legacy_refine_transaction_preserves_runtime_entry_for_next_commit() {
kind: "illustration".to_string(),
media_type: "image/png".to_string(),
local_path: "assets/source.png".to_string(),
image_sequence_frames: None,
image_sequence_duration_ms: None,
source: GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Uploaded,
canvas_project_id: None,
@@ -14,6 +14,8 @@ use std::path::Path;
const PROJECT_TEXT_PREVIEW_MAX_FILE_BYTES: u64 = 2 * 1024 * 1024;
const PROJECT_MEDIA_PREVIEW_MAX_FILE_BYTES: u64 = 32 * 1024 * 1024;
const PROJECT_RESOURCE_PREVIEW_READ_CHUNK_BYTES: usize = 64 * 1024;
const PROJECT_MEDIA_PREVIEW_MAX_DIMENSION: u32 = 8_192;
const PROJECT_MEDIA_PREVIEW_MAX_PIXELS: u64 = 32 * 1024 * 1024;
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
@@ -30,6 +32,10 @@ pub(crate) struct LocalProjectMediaPreview {
pub(crate) path: String,
pub(crate) media_type: String,
pub(crate) byte_len: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) pixel_width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) pixel_height: Option<u32>,
pub(crate) data_url: String,
}
@@ -196,14 +202,135 @@ pub(crate) fn load_local_project_media_preview_with_cancellation(
cancellation.check()?;
let media_type = detect_project_media_type(&normalized, &bytes, kind)?;
cancellation.check()?;
let dimensions = (kind == ProjectMediaPreviewKind::Art)
.then(|| detect_project_art_dimensions(&bytes, media_type))
.flatten();
Ok(LocalProjectMediaPreview {
path: normalized,
media_type: media_type.to_string(),
byte_len: bytes.len() as u64,
pixel_width: dimensions.map(|(width, _)| width),
pixel_height: dimensions.map(|(_, height)| height),
data_url: encode_project_resource_preview_data_url(media_type, &bytes, cancellation)?,
})
}
fn detect_project_art_dimensions(bytes: &[u8], media_type: &str) -> Option<(u32, u32)> {
let dimensions = match media_type {
"image/gif" => detect_gif_dimensions(bytes),
"image/bmp" => detect_bmp_dimensions(bytes),
"image/avif" => detect_ispe_dimensions(bytes),
"image/svg+xml" => detect_svg_dimensions(bytes),
_ => None,
}?;
let (width, height) = dimensions;
let pixels = u64::from(width).checked_mul(u64::from(height))?;
if width == 0
|| height == 0
|| width > PROJECT_MEDIA_PREVIEW_MAX_DIMENSION
|| height > PROJECT_MEDIA_PREVIEW_MAX_DIMENSION
|| pixels > PROJECT_MEDIA_PREVIEW_MAX_PIXELS
{
return None;
}
Some((width, height))
}
fn detect_gif_dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
if bytes.len() < 10 || !matches!(&bytes[..6], b"GIF87a" | b"GIF89a") {
return None;
}
Some((
u32::from(u16::from_le_bytes([bytes[6], bytes[7]])),
u32::from(u16::from_le_bytes([bytes[8], bytes[9]])),
))
}
fn detect_bmp_dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
if bytes.len() < 26 || &bytes[..2] != b"BM" {
return None;
}
let width = i32::from_le_bytes(bytes[18..22].try_into().ok()?);
let height = i32::from_le_bytes(bytes[22..26].try_into().ok()?);
Some((width.unsigned_abs(), height.unsigned_abs()))
}
fn detect_ispe_dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
let marker = b"ispe";
let index = bytes
.windows(marker.len())
.position(|window| window == marker)?;
if index < 4 || index.checked_add(16)? > bytes.len() {
return None;
}
Some((
u32::from_be_bytes(bytes[index + 8..index + 12].try_into().ok()?),
u32::from_be_bytes(bytes[index + 12..index + 16].try_into().ok()?),
))
}
fn detect_svg_dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
let text = std::str::from_utf8(bytes).ok()?;
let lower = text.to_ascii_lowercase();
let start = lower.find("<svg")?;
let end = lower[start..].find('>')? + start;
let root = &text[start..end];
let width = svg_length_attribute(root, "width");
let height = svg_length_attribute(root, "height");
if let (Some(width), Some(height)) = (width, height) {
return Some((width, height));
}
let view_box = svg_attribute(root, "viewbox")?;
let mut values = view_box
.split(|character: char| character.is_ascii_whitespace() || character == ',')
.filter_map(|value| value.parse::<f64>().ok());
let _min_x = values.next()?;
let _min_y = values.next()?;
let width = values.next()?;
let height = values.next()?;
Some((safe_svg_dimension(width)?, safe_svg_dimension(height)?))
}
fn svg_length_attribute(root: &str, name: &str) -> Option<u32> {
let value = svg_attribute(root, name)?;
let value = value.trim().strip_suffix("px").unwrap_or(value.trim());
safe_svg_dimension(value.parse::<f64>().ok()?)
}
fn svg_attribute<'a>(root: &'a str, name: &str) -> Option<&'a str> {
let lower = root.to_ascii_lowercase();
let mut offset = 0;
while let Some(relative) = lower[offset..].find(name) {
let start = offset + relative;
let before = start
.checked_sub(1)
.and_then(|index| lower.as_bytes().get(index));
let after = lower.as_bytes().get(start + name.len());
if before.is_some_and(|byte| byte.is_ascii_whitespace())
&& after.is_some_and(|byte| *byte == b'=' || byte.is_ascii_whitespace())
{
let rest = root[start + name.len()..].trim_start();
let rest = rest.strip_prefix('=')?.trim_start();
let quote = rest.chars().next()?;
if quote == '\'' || quote == '"' {
let value = &rest[quote.len_utf8()..];
let end = value.find(quote)?;
return Some(&value[..end]);
}
}
offset = start + name.len();
}
None
}
fn safe_svg_dimension(value: f64) -> Option<u32> {
if !value.is_finite() || value <= 0.0 || value > f64::from(PROJECT_MEDIA_PREVIEW_MAX_DIMENSION)
{
return None;
}
Some(value.round() as u32)
}
fn encode_project_resource_preview_data_url(
media_type: &str,
bytes: &[u8],
@@ -539,6 +666,8 @@ mod tests {
)
.expect("safe svg");
assert_eq!(preview.media_type, "image/svg+xml");
assert_eq!(preview.pixel_width, None);
assert_eq!(preview.pixel_height, None);
assert!(preview.data_url.starts_with("data:image/svg+xml;base64,"));
assert!(load_local_project_media_preview(
root.path(),
@@ -554,6 +683,54 @@ mod tests {
.is_err());
}
#[test]
fn media_preview_reports_safe_dimensions_for_extended_art_images() {
let root = tempfile::tempdir().expect("temp root");
fs::create_dir_all(root.path().join("assets")).expect("assets dir");
let mut gif = b"GIF89a".to_vec();
gif.extend_from_slice(&320_u16.to_le_bytes());
gif.extend_from_slice(&180_u16.to_le_bytes());
gif.extend_from_slice(&[0; 4]);
fs::write(root.path().join("assets/scene.gif"), gif).expect("gif");
let mut bmp = vec![0_u8; 26];
bmp[..2].copy_from_slice(b"BM");
bmp[18..22].copy_from_slice(&640_i32.to_le_bytes());
bmp[22..26].copy_from_slice(&(-360_i32).to_le_bytes());
fs::write(root.path().join("assets/scene.bmp"), bmp).expect("bmp");
let mut avif = vec![0_u8; 32];
avif[4..8].copy_from_slice(b"ftyp");
avif[8..12].copy_from_slice(b"avif");
avif[16..20].copy_from_slice(b"ispe");
avif[24..28].copy_from_slice(&1920_u32.to_be_bytes());
avif[28..32].copy_from_slice(&1080_u32.to_be_bytes());
fs::write(root.path().join("assets/scene.avif"), avif).expect("avif");
fs::write(
root.path().join("assets/scene.svg"),
r#"<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><path d="M0 0"/></svg>"#,
)
.expect("svg");
for (path, expected_type, expected_dimensions) in [
("assets/scene.gif", "image/gif", Some((320, 180))),
("assets/scene.bmp", "image/bmp", Some((640, 360))),
("assets/scene.avif", "image/avif", Some((1920, 1080))),
("assets/scene.svg", "image/svg+xml", Some((800, 600))),
] {
let preview =
load_local_project_media_preview(root.path(), path, ProjectMediaPreviewKind::Art)
.expect("extended image preview");
assert_eq!(preview.media_type, expected_type);
assert_eq!(
preview.pixel_width.zip(preview.pixel_height),
expected_dimensions
);
}
}
#[cfg(unix)]
#[test]
fn resource_preview_rejects_symlink_and_hardlink_files() {
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Genarrative AI Game Creator",
"version": "0.1.1",
"version": "0.1.2",
"identifier": "world.genarrative.ai-game-creator",
"build": {
"beforeDevCommand": "npm --prefix ../.. run agc:serve",
+1
View File
@@ -6418,6 +6418,7 @@ export function App({
}
} finally {
setChatAgentBusy(false);
setDirectCodexProgress('');
const activeTurn = activeDirectCodexTurnRef.current;
if (
!activeTurn ||
+24 -6
View File
@@ -365,7 +365,11 @@ export interface AgentRuntimeResult {
}
export type AgentRuntimeResponseStreamStatus =
'streaming' | 'ready' | 'committed' | 'discarded' | 'failed';
| 'streaming'
| 'ready'
| 'committed'
| 'discarded'
| 'failed';
export interface AgentRuntimeResponseStream {
schemaVersion: string;
@@ -486,13 +490,22 @@ export interface GameCreatorAgentLlmConfigStatus {
}
export type GameCreatorLlmApiKind =
'openai_responses' | 'openai_chat' | 'anthropic';
| 'openai_responses'
| 'openai_chat'
| 'anthropic';
export type GameCreatorAgentMode =
'codex_app_server' | 'codex_cli' | 'provider';
| 'codex_app_server'
| 'codex_cli'
| 'provider';
export type RuntimeLlmProviderPresetId =
'custom' | 'openai' | 'deepseek' | 'anthropic' | 'ark';
| 'custom'
| 'openai'
| 'deepseek'
| 'anthropic'
| 'ark';
export type RuntimeAgentLlmProviderPresetId =
'inherit' | RuntimeLlmProviderPresetId;
| 'inherit'
| RuntimeLlmProviderPresetId;
export interface GameCreatorLlmConfig {
apiKey: string;
@@ -795,7 +808,12 @@ export interface AgentProgressEvent {
}
export type GameCreatorDirectTurnUpdateStatus =
'accepted' | 'running' | 'streaming' | 'finalizing' | 'completed' | 'failed';
| 'accepted'
| 'running'
| 'streaming'
| 'finalizing'
| 'completed'
| 'failed';
export type GameCreatorDirectTurnActivity =
| 'request-accepted'
File diff suppressed because it is too large Load Diff
@@ -897,8 +897,8 @@
}
.asset-canvas-surface__quick-edit-card {
width: calc(100% - 16px);
max-height: min(20rem, calc(100% - 16px));
width: calc(100% - 24px);
max-height: min(20rem, calc(100% - 24px));
padding: 12px;
}
@@ -80,6 +80,15 @@ type GenerationCommandResult = {
}>;
};
type GenerationFailureCommandResult = {
disposition:
| 'failed'
| 'reconciliation-required'
| 'not-started'
| 'already-terminal';
draft: ImageCanvasDraft;
};
type GenerationProgressEvent = ImageCanvasGenerationProgress & {
schemaVersion: 'game-creator-asset-generation-progress.v1';
projectId: string;
@@ -249,6 +258,7 @@ export function createMockImageCanvasGenerationPort(): ImageCanvasGenerationPort
message: '当前阶段使用明确 mock,不会提交真实 AI 生成请求',
});
return {
settleGenerationFailure: unsupported,
archiveFailedGeneration: unsupported,
generateImage: unsupported,
recoverImages: async () => ({
@@ -328,6 +338,30 @@ export function createTauriImageCanvasHostAdapter(input: {
);
};
const generation: ImageCanvasGenerationPort = {
async settleGenerationFailure(failureInput) {
try {
const result = await invokeInput<GenerationFailureCommandResult>(
'finalize_local_project_asset_canvas_generation_failure',
{
...baseScope(failureInput.scope),
expectedDraftRevision: safeRevision(
failureInput.expectedDraftRevision,
'expectedDraftRevision',
),
generationId: failureInput.generationId,
intentId: failureInput.intentId,
errorCode: failureInput.errorCode,
reconciliationRequired: failureInput.reconciliationRequired,
},
);
return { status: 'ok', value: result };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return message.includes('draft-revision-conflict')
? conflict('draft-revision', null, null)
: failure(error);
}
},
async archiveFailedGeneration(archiveInput) {
try {
const result = await invokeInput<{
@@ -201,10 +201,10 @@ function gameChatRuntimeActivityTimes(runtime: AgentRuntimeState) {
function gameChatRuntimeIsExpectedWait(runtime: AgentRuntimeState) {
return Boolean(
runtime.pendingToolAction ||
runtime.userInputRequest ||
[runtime.status, runtime.phase, runtime.goalStatus].some((state) =>
GAME_CHAT_EXPECTED_WAIT_STATES.has(state ?? ''),
),
runtime.userInputRequest ||
[runtime.status, runtime.phase, runtime.goalStatus].some((state) =>
GAME_CHAT_EXPECTED_WAIT_STATES.has(state ?? ''),
),
);
}
@@ -984,11 +984,11 @@ export function SupervisorChatOnlyView({
!directCodex && Boolean(expectedRunId && runtime?.runId !== expectedRunId);
const running = Boolean(
chatAgentBusy ||
(!directCodex && synchronizingAcceptedRun) ||
(runtime &&
runtime.status !== 'needs-reconciliation' &&
runtime.phase !== 'needs-reconciliation' &&
!isAgentRuntimeTerminalState(runtime)),
(!directCodex && synchronizingAcceptedRun) ||
(runtime &&
runtime.status !== 'needs-reconciliation' &&
runtime.phase !== 'needs-reconciliation' &&
!isAgentRuntimeTerminalState(runtime)),
);
const gameChatInterruptionText =
!directCodex && gameChatMode && runtime
@@ -1113,18 +1113,18 @@ export function SupervisorChatOnlyView({
: null;
const expectedRuntimeWait = Boolean(
needsUserInput ||
pendingConfirmation ||
pendingCommand ||
(activeRuntimeLanes.length > 0 &&
activeRuntimeLanes.every(gameChatRuntimeIsExpectedWait)),
pendingConfirmation ||
pendingCommand ||
(activeRuntimeLanes.length > 0 &&
activeRuntimeLanes.every(gameChatRuntimeIsExpectedWait)),
);
const runtimeAppearsStalled = Boolean(
running &&
runtime &&
!runtimeTerminal &&
!expectedRuntimeWait &&
inactiveRuntimeMs !== null &&
inactiveRuntimeMs > GAME_CHAT_RUNTIME_STALL_THRESHOLD_MS,
runtime &&
!runtimeTerminal &&
!expectedRuntimeWait &&
inactiveRuntimeMs !== null &&
inactiveRuntimeMs > GAME_CHAT_RUNTIME_STALL_THRESHOLD_MS,
);
const runStateLabel = (() => {
if (gameChatInterruptionText) {
+7 -7
View File
@@ -5052,6 +5052,13 @@ iframe.preview-frame {
box-shadow: none;
}
.game-workbench-editor-shell {
border: 0;
border-radius: 0;
background: var(--game-workbench-stage-fill);
box-shadow: none;
}
.game-workbench-stage > .game-resource-manager {
grid-row: 3;
min-height: 0;
@@ -5062,13 +5069,6 @@ iframe.preview-frame {
box-shadow: var(--platform-nav-active-shadow);
}
.game-workbench-editor-shell {
border: 0;
border-radius: 0;
background: var(--game-workbench-stage-fill);
box-shadow: none;
}
.game-workbench-stage[data-resource-view-state^='resources.asset-canvas'],
.game-workbench-stage[data-resource-view-state='resources.ui-editor'] {
grid-template-rows: auto minmax(0, 1fr);
@@ -46,8 +46,8 @@ import type {
GameIterationVersion,
ProjectResourceCanvasLayoutMode,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import { canonicalAssetBaseName } from '../../features/asset-canvas/assetCanvasNaming';
import PRODUCT_LOGO from '../../../../../packages/shared/src/icons/taonier-product-ip.png';
import { canonicalAssetBaseName } from '../../features/asset-canvas/assetCanvasNaming';
import {
type AssetCanvasCommitNotification,
type AssetCanvasExitResult,
@@ -201,6 +201,15 @@ type ResourceEditorRoute = {
generationMode: 'create' | 'derive';
};
function defaultCreatedResourceName(
editKind: ResourceEditorRoute['capability']['editKind'],
) {
if (editKind === 'video') return '新视频';
if (editKind === 'sound-effect') return '新音效';
if (editKind === 'background-music') return '新背景音乐';
return '新资源';
}
type UiEditorRoute = {
resourceId: string;
resourceLabel: string;
@@ -2361,7 +2370,7 @@ export default function ProjectDevelopmentView({
}
function closeOnEscape(event: KeyboardEvent) {
if (event.key === 'Escape') {
restoreResourceListScrollRef.current = false;
restoreResourceListScrollRef.current = true;
setFocusedResourceId(null);
}
}
@@ -3855,7 +3864,7 @@ export default function ProjectDevelopmentView({
function closeResourceFocus() {
advanceFocusGeneration();
restoreResourceListScrollRef.current = false;
restoreResourceListScrollRef.current = true;
setFocusedResourceId(null);
}
@@ -4036,7 +4045,7 @@ export default function ProjectDevelopmentView({
) : null}
{mode === 'resources' && uiEditorRoute ? (
<div className="h-full min-h-0 min-w-0 w-full overflow-hidden rounded-2xl border border-(--platform-desktop-panel-border) bg-(--platform-desktop-panel-fill) shadow-(--platform-panel-shadow)">
<div className="game-workbench-editor-shell h-full min-h-0 min-w-0 w-full overflow-hidden">
<UiEditorPage
projectPath={projectPath}
resourceId={uiEditorRoute.resourceId}
@@ -4062,28 +4071,19 @@ export default function ProjectDevelopmentView({
) : mode === 'resources' && resourceEditorRoute ? (
<ResourceEditSurface
resourceLabel={
resourceEditorRoute.resource
? resourceEditorRoute.resource.label
: ((
{
video: '新视频',
'sound-effect': '新音效',
'background-music': '新背景音乐',
} as Record<string, string>
)[resourceEditorRoute.capability.editKind] ?? '新资源')
resourceEditorRoute.resource?.label ??
defaultCreatedResourceName(
resourceEditorRoute.capability.editKind,
)
}
editKind={resourceEditorRoute.capability.editKind}
generationMode={resourceEditorRoute.generationMode}
initialAssetName={
resourceEditorRoute.resource
? defaultDerivedResourceName(resourceEditorRoute.resource)
: ((
{
video: '新视频',
'sound-effect': '新音效',
'background-music': '新背景音乐',
} as Record<string, string>
)[resourceEditorRoute.capability.editKind] ?? '新资源')
: defaultCreatedResourceName(
resourceEditorRoute.capability.editKind,
)
}
semanticNotice={resourceEditorRoute.capability.semanticNotice}
onCancel={cancelResourceEditor}
@@ -4119,7 +4119,9 @@ export default function ProjectDevelopmentView({
) ? (
<button
type="button"
onClick={() => openCharacterAnimation(focusedResource)}
onClick={() =>
openCharacterAnimation(focusedResource)
}
>
</button>
@@ -907,7 +907,8 @@ export function registerClientHomeTests() {
];
const streamedReply = '流式正文已经完整结束。';
let streamHandler:
((event: { payload: Record<string, unknown> }) => void) | null = null;
| ((event: { payload: Record<string, unknown> }) => void)
| null = null;
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'read_local_conversation') {
@@ -1727,9 +1728,11 @@ export function registerHomeProjectCreationTests() {
expect(persistedTurnCall).toBeGreaterThanOrEqual(0);
expect(persistedTurnCall).toBeLessThan(directTurnCall);
const persistedTurnArgs = invoke.mock.calls[persistedTurnCall]?.[1] as
Record<string, unknown> | undefined;
| Record<string, unknown>
| undefined;
const directTurnArgs = invoke.mock.calls[directTurnCall]?.[1] as
Record<string, unknown> | undefined;
| Record<string, unknown>
| undefined;
expect(persistedTurnArgs).toEqual({
projectPath,
agentId: null,
@@ -461,8 +461,8 @@ export function registerProjectConversationTests() {
'未命名游戏原型',
);
let finishConversationRead:
((value: { path: string; agentId: null; messages: [] }) => void) | null =
null;
| ((value: { path: string; agentId: null; messages: [] }) => void)
| null = null;
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
File diff suppressed because it is too large Load Diff

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