修复素材画布保存区与项目状态展示
调整素材保存区响应式布局,固定资源用途并保留精修继承语义。 收口普通用户项目路径展示,补充画布与工作台回归测试。 同步更新技术方案、产品需求文档与项目长期记忆。
This commit is contained in:
@@ -1212,6 +1212,18 @@ export function projectNameFromPath(projectPath: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export function projectWorkspaceStatusForDisplay(workspaceStatus: string) {
|
||||
const openedPrefix = '已打开:';
|
||||
if (!workspaceStatus.startsWith(openedPrefix)) {
|
||||
return workspaceStatus;
|
||||
}
|
||||
const projectPath = workspaceStatus.slice(openedPrefix.length).trim();
|
||||
if (!/[\\/]/u.test(projectPath)) {
|
||||
return workspaceStatus;
|
||||
}
|
||||
return `${openedPrefix}${projectNameFromPath(projectPath)}`;
|
||||
}
|
||||
|
||||
export function mergeProjectSupervisorConversation(
|
||||
projectRecords: LocalConversationMessageRecord[],
|
||||
supervisorRecords: LocalConversationMessageRecord[],
|
||||
|
||||
@@ -106,6 +106,13 @@ export type AssetCanvasCommitNotification = {
|
||||
eventId?: string;
|
||||
};
|
||||
|
||||
export const ASSET_CANVAS_KIND_OPTIONS = [
|
||||
{ value: 'game-art', label: '普通游戏美术' },
|
||||
{ value: 'icon-spec', label: '统一视觉规范' },
|
||||
{ value: 'ui-prototype', label: '游戏界面原型' },
|
||||
{ value: 'art-spritesheet', label: '核心美术图集' },
|
||||
] as const;
|
||||
|
||||
type RuntimeCanvasLayer = CanvasLayer & { mediaRef: ImageCanvasMediaRef };
|
||||
|
||||
type PendingGenerationIdentity = {
|
||||
@@ -303,6 +310,8 @@ export function AssetCanvasSurface({
|
||||
scope,
|
||||
sessionId,
|
||||
expectedHostRevision,
|
||||
initialAssetName = '画布素材',
|
||||
initialAssetKind = 'game-art',
|
||||
onCancel,
|
||||
onCommitted,
|
||||
onSaveAttempt,
|
||||
@@ -312,11 +321,15 @@ export function AssetCanvasSurface({
|
||||
scope: ImageCanvasHostScope;
|
||||
sessionId: string;
|
||||
expectedHostRevision: string;
|
||||
initialAssetName?: string;
|
||||
initialAssetKind?: string;
|
||||
onCancel?: () => void;
|
||||
onCommitted?: (input: AssetCanvasCommitNotification) => void;
|
||||
onSaveAttempt?: (input: AssetCanvasSaveAttempt) => void;
|
||||
renderImage?: RenderAssetCanvasImage;
|
||||
}) {
|
||||
const resolvedInitialAssetName = initialAssetName.trim() || '画布素材';
|
||||
const resolvedInitialAssetKind = initialAssetKind.trim() || 'game-art';
|
||||
const stableScope = useMemo<ImageCanvasHostScope>(
|
||||
() => ({
|
||||
projectId: scope.projectId,
|
||||
@@ -339,8 +352,8 @@ export function AssetCanvasSurface({
|
||||
const [backgroundColor, setBackgroundColor] = useState('#f4f4f5');
|
||||
const [selectedLayerIds, setSelectedLayerIds] = useState<string[]>([]);
|
||||
const [notice, setNotice] = useState('');
|
||||
const [assetName, setAssetName] = useState('画布素材');
|
||||
const [assetKind, setAssetKind] = useState('asset');
|
||||
const [assetName, setAssetName] = useState(resolvedInitialAssetName);
|
||||
const [assetKind, setAssetKind] = useState(resolvedInitialAssetKind);
|
||||
const [exportMediaType, setExportMediaType] = useState<
|
||||
'image/png' | 'image/jpeg' | 'image/webp'
|
||||
>('image/png');
|
||||
@@ -385,6 +398,12 @@ export function AssetCanvasSurface({
|
||||
const onWalletBalanceMayHaveChanged = useWalletStore(
|
||||
(state) => state.onWalletBalanceMayHaveChanged,
|
||||
);
|
||||
const assetSettingsDisabled =
|
||||
lifecycle.kind === 'canvas.saving' ||
|
||||
lifecycle.kind === 'canvas.generating';
|
||||
const inheritedUnknownAssetKind =
|
||||
stableScope.intent === 'refine' &&
|
||||
!ASSET_CANVAS_KIND_OPTIONS.some((option) => option.value === assetKind);
|
||||
|
||||
layersRef.current = layers;
|
||||
viewportRef.current = viewport;
|
||||
@@ -1425,33 +1444,56 @@ export function AssetCanvasSurface({
|
||||
className="asset-canvas-surface__savebar"
|
||||
aria-label="素材保存设置"
|
||||
>
|
||||
<input
|
||||
aria-label="素材名称"
|
||||
value={assetName}
|
||||
maxLength={80}
|
||||
onChange={(event) => setAssetName(event.target.value)}
|
||||
disabled={lifecycle.kind === 'canvas.generating'}
|
||||
/>
|
||||
<input
|
||||
aria-label="素材类型"
|
||||
value={assetKind}
|
||||
maxLength={64}
|
||||
onChange={(event) => setAssetKind(event.target.value)}
|
||||
disabled={lifecycle.kind === 'canvas.generating'}
|
||||
/>
|
||||
<select
|
||||
aria-label="导出格式"
|
||||
value={exportMediaType}
|
||||
onChange={(event) =>
|
||||
setExportMediaType(
|
||||
event.target.value as 'image/png' | 'image/jpeg' | 'image/webp',
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="image/png">PNG</option>
|
||||
<option value="image/jpeg">JPEG</option>
|
||||
<option value="image/webp">WebP</option>
|
||||
</select>
|
||||
<label className="asset-canvas-surface__save-field">
|
||||
<span>素材名称</span>
|
||||
<input
|
||||
aria-label="素材名称"
|
||||
value={assetName}
|
||||
maxLength={80}
|
||||
onChange={(event) => setAssetName(event.target.value)}
|
||||
disabled={assetSettingsDisabled}
|
||||
/>
|
||||
</label>
|
||||
<label className="asset-canvas-surface__save-field">
|
||||
<span>资源用途</span>
|
||||
<select
|
||||
aria-label="资源用途"
|
||||
value={assetKind}
|
||||
onChange={(event) => setAssetKind(event.target.value)}
|
||||
disabled={
|
||||
assetSettingsDisabled || stableScope.intent === 'refine'
|
||||
}
|
||||
>
|
||||
{inheritedUnknownAssetKind ? (
|
||||
<option value={assetKind}>原资源用途</option>
|
||||
) : null}
|
||||
{ASSET_CANVAS_KIND_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="asset-canvas-surface__save-field">
|
||||
<span>导出格式</span>
|
||||
<select
|
||||
aria-label="导出格式"
|
||||
value={exportMediaType}
|
||||
disabled={assetSettingsDisabled}
|
||||
onChange={(event) =>
|
||||
setExportMediaType(
|
||||
event.target.value as
|
||||
| 'image/png'
|
||||
| 'image/jpeg'
|
||||
| 'image/webp',
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="image/png">PNG</option>
|
||||
<option value="image/jpeg">JPEG</option>
|
||||
<option value="image/webp">WebP</option>
|
||||
</select>
|
||||
</label>
|
||||
<CanvasChromeButton
|
||||
label="保存到项目"
|
||||
icon={<Save size={15} aria-hidden="true" />}
|
||||
@@ -1459,7 +1501,8 @@ export function AssetCanvasSurface({
|
||||
onClick={() => void saveAsset()}
|
||||
disabled={
|
||||
lifecycle.kind === 'canvas.saving' ||
|
||||
lifecycle.kind === 'canvas.generating'
|
||||
lifecycle.kind === 'canvas.generating' ||
|
||||
assetName.trim().length === 0
|
||||
}
|
||||
>
|
||||
保存到项目
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
container-type: inline-size;
|
||||
overflow: hidden;
|
||||
border-radius: inherit;
|
||||
background: var(--platform-overlay-fill);
|
||||
@@ -24,6 +25,8 @@
|
||||
.asset-canvas-surface__toolbar-shell {
|
||||
z-index: 4;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: auto auto;
|
||||
min-width: 0;
|
||||
gap: 8px;
|
||||
padding: 8px 10px 9px;
|
||||
@@ -40,16 +43,33 @@
|
||||
.asset-canvas-surface__savebar {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(108px, 1.35fr) minmax(88px, 0.8fr) minmax(70px, auto)
|
||||
auto;
|
||||
minmax(0, 1.35fr) minmax(0, 1fr) minmax(86px, 0.55fr)
|
||||
max-content;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
align-items: end;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__save-field {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__save-field > span {
|
||||
overflow: hidden;
|
||||
color: var(--platform-text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__savebar input,
|
||||
.asset-canvas-surface__savebar select,
|
||||
.asset-canvas-surface__generation-form select {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 34px;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
@@ -87,6 +107,13 @@
|
||||
box-shadow: var(--platform-nav-active-shadow);
|
||||
}
|
||||
|
||||
.asset-canvas-surface .asset-canvas-surface__save {
|
||||
min-width: max-content;
|
||||
min-height: 34px;
|
||||
align-self: end;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.asset-canvas-surface
|
||||
.asset-canvas-surface__primary-action:not(:disabled):hover,
|
||||
.asset-canvas-surface .asset-canvas-surface__save:not(:disabled):hover {
|
||||
@@ -367,7 +394,11 @@
|
||||
}
|
||||
|
||||
.asset-canvas-surface__savebar {
|
||||
grid-template-columns: minmax(96px, 1fr) minmax(72px, 0.75fr) 70px;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.asset-canvas-surface__save-field:first-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__save {
|
||||
@@ -395,3 +426,15 @@
|
||||
padding: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 620px) {
|
||||
.asset-canvas-surface__savebar {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.asset-canvas-surface__save-field:first-child,
|
||||
.asset-canvas-surface__save {
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@ import type {
|
||||
ChatMessage,
|
||||
PendingUiConfirmation,
|
||||
} from '../../app/types';
|
||||
import { ProjectSupervisorRuntimePanel } from '../agent-runtime';
|
||||
import {
|
||||
ProjectSupervisorRuntimePanel,
|
||||
projectWorkspaceStatusForDisplay,
|
||||
} from '../agent-runtime';
|
||||
import { formatAgentCardRuntimeStatus } from '../project-summary/agentPresentation';
|
||||
import { taskStatusLabels } from '../project-summary/projectSummary';
|
||||
|
||||
@@ -127,7 +130,7 @@ export function ProjectSupervisorView({
|
||||
</button>
|
||||
</form>
|
||||
<small className="project-supervisor-workspace-status">
|
||||
{workspaceStatus}
|
||||
{projectWorkspaceStatusForDisplay(workspaceStatus)}
|
||||
</small>
|
||||
</div>
|
||||
<aside
|
||||
|
||||
+2
-4
@@ -32,6 +32,7 @@ import {
|
||||
formatAgentRuntimeTaskQueue,
|
||||
ProjectSupervisorRuntimePanel,
|
||||
projectSupervisorVisibleConversationText,
|
||||
projectWorkspaceStatusForDisplay,
|
||||
} from '../agent-runtime';
|
||||
import {
|
||||
formatAgentCardLlmStatus,
|
||||
@@ -283,15 +284,12 @@ export function ProjectWorkspaceChatPane({
|
||||
<div className="chat-header-title">
|
||||
<h1>AI 游戏创作</h1>
|
||||
<span>{devMode ? '开发模式' : currentProjectTitle}</span>
|
||||
{!devMode && localProject ? (
|
||||
<small>{localProject.projectPath}</small>
|
||||
) : null}
|
||||
<small>run: {agentRunStatus}</small>
|
||||
<small>preview: {previewStatus}</small>
|
||||
{mainProjectSummary ? (
|
||||
<small aria-label="项目摘要">{mainProjectSummary}</small>
|
||||
) : null}
|
||||
<small>{workspaceStatus}</small>
|
||||
<small>{projectWorkspaceStatusForDisplay(workspaceStatus)}</small>
|
||||
</div>
|
||||
<div className="chat-header-actions">
|
||||
<button type="button" onClick={showChatHelp}>
|
||||
|
||||
+2
-1
@@ -35,6 +35,7 @@ import {
|
||||
projectSupervisorCollaboratingAgentRuntimes,
|
||||
ProjectSupervisorRuntimeControls,
|
||||
projectSupervisorVisibleConversationText,
|
||||
projectWorkspaceStatusForDisplay,
|
||||
} from '../agent-runtime';
|
||||
import { RuntimeConfigDialog } from '../runtime-config/RuntimeConfigDialog';
|
||||
import { GameChatImageViewer } from './GameChatImageViewer';
|
||||
@@ -891,7 +892,7 @@ export function SupervisorChatOnlyView({
|
||||
? '已投递,正在同步 Agent Runner'
|
||||
: runtime
|
||||
? projectSupervisorChatRuntimeStatus(runtime)
|
||||
: workspaceStatus);
|
||||
: projectWorkspaceStatusForDisplay(workspaceStatus));
|
||||
const headerStatus = gameChatMode ? status : previewStatus || status;
|
||||
const runtimeEvents = useMemo(
|
||||
() => collectGameChatRuntimeEvents(runtime, runtimeByAgentId),
|
||||
|
||||
@@ -90,6 +90,8 @@ type AssetCanvasRoute = {
|
||||
flowId: string;
|
||||
sessionId: string;
|
||||
expectedHostRevision: string;
|
||||
initialAssetName: string;
|
||||
initialAssetKind: string;
|
||||
scope: ImageCanvasHostScope;
|
||||
host: TauriImageCanvasHostAdapter;
|
||||
};
|
||||
@@ -239,6 +241,11 @@ function isRasterImageResource(resource: ProjectResource) {
|
||||
);
|
||||
}
|
||||
|
||||
function assetNameFromLocalPath(localPath: string) {
|
||||
const fileName = localPath.split(/[\\/]/u).filter(Boolean).pop() ?? '';
|
||||
return fileName.replace(/\.[^.]+$/u, '').trim() || '画布素材';
|
||||
}
|
||||
|
||||
function isExtendedArtMediaResource(resource: ProjectResource) {
|
||||
const mediaType = resource.mediaType.toLowerCase();
|
||||
return (
|
||||
@@ -1173,6 +1180,9 @@ export default function ProjectDevelopmentView({
|
||||
pendingResourceFocusRef.current = null;
|
||||
setHiddenCommittedResourceId(null);
|
||||
setAssetCanvasNotice('正在打开素材创作画布…');
|
||||
const sourceAsset = sourceAssetId
|
||||
? manifest.assets.find((asset) => asset.id === sourceAssetId)
|
||||
: null;
|
||||
void invoke<{ revision: number }>('get_local_game_project_revision', {
|
||||
projectPath,
|
||||
})
|
||||
@@ -1197,6 +1207,14 @@ export default function ProjectDevelopmentView({
|
||||
flowId,
|
||||
sessionId,
|
||||
expectedHostRevision: String(status.revision),
|
||||
initialAssetName:
|
||||
intent === 'refine' && sourceAsset
|
||||
? assetNameFromLocalPath(sourceAsset.localPath)
|
||||
: '画布素材',
|
||||
initialAssetKind:
|
||||
intent === 'refine' && sourceAsset
|
||||
? sourceAsset.kind
|
||||
: 'game-art',
|
||||
scope,
|
||||
host: createTauriImageCanvasHostAdapter({
|
||||
projectPath,
|
||||
@@ -1215,7 +1233,7 @@ export default function ProjectDevelopmentView({
|
||||
}
|
||||
});
|
||||
},
|
||||
[advanceFocusGeneration, manifest.projectId, projectPath],
|
||||
[advanceFocusGeneration, manifest.assets, manifest.projectId, projectPath],
|
||||
);
|
||||
|
||||
const cancelAssetCanvas = useCallback(() => {
|
||||
@@ -1557,6 +1575,8 @@ export default function ProjectDevelopmentView({
|
||||
scope={assetCanvasRoute.scope}
|
||||
sessionId={assetCanvasRoute.sessionId}
|
||||
expectedHostRevision={assetCanvasRoute.expectedHostRevision}
|
||||
initialAssetName={assetCanvasRoute.initialAssetName}
|
||||
initialAssetKind={assetCanvasRoute.initialAssetKind}
|
||||
onCancel={cancelAssetCanvas}
|
||||
onSaveAttempt={handleAssetCanvasSaveAttempt}
|
||||
onCommitted={handleAssetCommitted}
|
||||
|
||||
@@ -15,9 +15,24 @@ import {
|
||||
projectSupervisorChatRuntimeStatus,
|
||||
projectSupervisorResponseStreamIdentity,
|
||||
projectSupervisorVisibleConversationText,
|
||||
projectWorkspaceStatusForDisplay,
|
||||
submitProjectSupervisorRuntimeTask,
|
||||
} from '../src/features/agent-runtime/model';
|
||||
|
||||
describe('普通用户工作区状态', () => {
|
||||
test('用项目名称替代 Unix 和 Windows 绝对路径', () => {
|
||||
expect(projectWorkspaceStatusForDisplay('已打开:/tmp/authorized-game')).toBe(
|
||||
'已打开:authorized-game',
|
||||
);
|
||||
expect(
|
||||
projectWorkspaceStatusForDisplay('已打开:C:\\projects\\canvas-game'),
|
||||
).toBe('已打开:canvas-game');
|
||||
expect(projectWorkspaceStatusForDisplay('正在读取项目')).toBe(
|
||||
'正在读取项目',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Game Chat stream identity and source', () => {
|
||||
test('response stream identity binds run, slot, and revision', () => {
|
||||
const base: AgentRuntimeResponseStream = {
|
||||
|
||||
@@ -1825,7 +1825,7 @@ export function registerAgentRuntimeCommandTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/memory typo');
|
||||
|
||||
@@ -5475,7 +5475,7 @@ export function registerDeveloperToolsTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/audit');
|
||||
@@ -5603,7 +5603,7 @@ export function registerDeveloperToolsTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
invoke.mockClear();
|
||||
|
||||
@@ -5686,7 +5686,7 @@ export function registerDeveloperToolsTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
invoke.mockClear();
|
||||
|
||||
@@ -5800,7 +5800,7 @@ export function registerDeveloperToolsTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
invoke.mockClear();
|
||||
|
||||
@@ -5898,7 +5898,7 @@ export function registerDeveloperToolsTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/audit');
|
||||
@@ -5960,7 +5960,7 @@ export function registerDeveloperToolsTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/audit');
|
||||
@@ -6011,7 +6011,7 @@ export function registerDeveloperToolsTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/audit');
|
||||
|
||||
@@ -92,6 +92,7 @@ import {
|
||||
GameChatReleaseApp,
|
||||
WorkspaceLauncher,
|
||||
} from '../../src/App';
|
||||
import { projectNameFromPath } from '../../src/features/agent-runtime/model';
|
||||
import ProjectDevelopmentView from '../../src/view/project-development';
|
||||
|
||||
const testAuthUser: AuthUser = {
|
||||
@@ -714,7 +715,9 @@ function createProjectSupervisorRuntimeHarness({
|
||||
async function openMainProject(projectPath: string) {
|
||||
submitChat(`/project ${projectPath}`);
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(await screen.findByText(`已打开:${projectPath}`)).not.toBeNull();
|
||||
expect(
|
||||
await screen.findByText(`已打开:${projectNameFromPath(projectPath)}`),
|
||||
).not.toBeNull();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1192,7 +1192,7 @@ export function registerCanvasAssetTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat(
|
||||
@@ -1717,7 +1717,7 @@ export function registerCanvasAssetTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
expect(await screen.findByText(/还没有最近一次 Agent run/)).not.toBeNull();
|
||||
|
||||
@@ -1821,7 +1821,7 @@ export function registerCanvasAssetTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/canvas bad\u0007canvas');
|
||||
@@ -2003,7 +2003,7 @@ export function registerCanvasAssetTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/sync-canvas-project canvas-project-1');
|
||||
@@ -2090,7 +2090,7 @@ export function registerCanvasAssetTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/generate-art 月光厨房弹幕主角');
|
||||
|
||||
@@ -241,7 +241,7 @@ export function registerProjectCommandTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/audit');
|
||||
@@ -2202,7 +2202,7 @@ export function registerProjectCommandTests() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '初始化' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByLabelText('本地项目目录'), {
|
||||
@@ -2771,7 +2771,7 @@ export function registerProjectCommandTests() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '初始化' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByLabelText('本地项目目录'), {
|
||||
@@ -3245,7 +3245,7 @@ export function registerProjectCommandTests() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '初始化' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByLabelText('本地项目目录'), {
|
||||
@@ -3333,7 +3333,7 @@ export function registerProjectCommandTests() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '初始化' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '静态入口自检' }));
|
||||
@@ -3448,7 +3448,7 @@ export function registerProjectCommandTests() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '初始化' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
fireEvent.click(logPanel.getByRole('button', { name: '命令日志' }));
|
||||
|
||||
@@ -416,7 +416,7 @@ export function registerProjectConversationTests() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', {
|
||||
@@ -509,7 +509,7 @@ export function registerProjectConversationTests() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
|
||||
expect(await screen.findByText('受保护历史需求')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
|
||||
expect(invoke).toHaveBeenCalledWith('read_local_conversation', {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
agentId: null,
|
||||
@@ -787,7 +787,7 @@ export function registerProjectConversationTests() {
|
||||
expect(
|
||||
screen.queryByText('项目对话保存失败:conversation append failed once'),
|
||||
).toBeNull();
|
||||
expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('confirms before saving slash conversation when policy requires it', async () => {
|
||||
@@ -955,7 +955,7 @@ export function registerProjectConversationTests() {
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('conversation.write')).toBeNull();
|
||||
});
|
||||
expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
|
||||
expect(invoke).not.toHaveBeenCalledWith(
|
||||
'append_local_conversation_message',
|
||||
expect.anything(),
|
||||
@@ -1233,7 +1233,7 @@ export function registerProjectConversationTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:/tmp/authorized-game'),
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
expect(await screen.findByText('正在拆解创作方向')).not.toBeNull();
|
||||
@@ -1714,7 +1714,7 @@ export function registerProjectConversationTests() {
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
|
||||
await screen.findByText('已打开:/tmp/authorized-game');
|
||||
await screen.findByText('已打开:authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
const dialog = await screen.findByLabelText('Agent 对话');
|
||||
expect(
|
||||
@@ -2109,7 +2109,7 @@ export function registerProjectConversationTests() {
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
|
||||
await screen.findByText('已打开:/tmp/authorized-game');
|
||||
await screen.findByText('已打开:authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
const input = await screen.findByLabelText('Agent 对话内容');
|
||||
invoke.mockClear();
|
||||
@@ -2212,7 +2212,8 @@ export function registerProjectConversationTests() {
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
|
||||
await screen.findByText('/tmp/authorized-game');
|
||||
await screen.findByText('已打开:authorized-game');
|
||||
expect(screen.queryByText('/tmp/authorized-game')).toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
|
||||
const agentDialog = await screen.findByLabelText('Agent 对话');
|
||||
@@ -3126,7 +3127,7 @@ export function registerProjectConversationTests() {
|
||||
fireEvent.click(
|
||||
within(createA as HTMLElement).getByRole('button', { name: '确认' }),
|
||||
);
|
||||
expect(await screen.findByText('已打开:/tmp/project-a')).not.toBeNull();
|
||||
expect(await screen.findByText('已打开:project-a')).not.toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(invoke).toHaveBeenCalledWith(
|
||||
'resume_game_creator_agent_runtime_tasks',
|
||||
@@ -3144,7 +3145,7 @@ export function registerProjectConversationTests() {
|
||||
fireEvent.click(
|
||||
within(createB as HTMLElement).getByRole('button', { name: '确认' }),
|
||||
);
|
||||
expect(await screen.findByText('已打开:/tmp/project-b')).not.toBeNull();
|
||||
expect(await screen.findByText('已打开:project-b')).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
resolveProjectAResume?.([]);
|
||||
@@ -3160,6 +3161,6 @@ export function registerProjectConversationTests() {
|
||||
'read_game_creator_agent_runtimes',
|
||||
{ projectPath: '/tmp/project-a' },
|
||||
);
|
||||
expect(screen.getByText('已打开:/tmp/project-b')).not.toBeNull();
|
||||
expect(screen.getByText('已打开:project-b')).not.toBeNull();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8063,7 +8063,7 @@ export function registerProjectWorkbenchNavigationTests() {
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
|
||||
await screen.findByText('已打开:/tmp/authorized-game');
|
||||
await screen.findByText('已打开:authorized-game');
|
||||
const revealButtons = screen.getAllByRole('button', { name: '显示目录' });
|
||||
fireEvent.click(revealButtons[revealButtons.length - 1]);
|
||||
|
||||
@@ -8143,7 +8143,7 @@ export function registerProjectWorkbenchNavigationTests() {
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
});
|
||||
|
||||
await screen.findByText('已打开:/tmp/authorized-game');
|
||||
await screen.findByText('已打开:authorized-game');
|
||||
invoke.mockClear();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '文件' }));
|
||||
@@ -9086,7 +9086,7 @@ export function registerProjectAgentStatusTests() {
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
|
||||
await screen.findByText('已打开:/tmp/authorized-game');
|
||||
await screen.findByText('已打开:authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
expect(await screen.findByLabelText('Agent 对话')).not.toBeNull();
|
||||
expect(screen.getByLabelText('Agent 最近证据').textContent).toContain(
|
||||
|
||||
+2
-2
@@ -4,9 +4,9 @@ import type { PreviewShortcutInvoke } from './invoke-mock';
|
||||
export async function assertProjectAndDesignShortcutFlow(
|
||||
invoke: PreviewShortcutInvoke,
|
||||
) {
|
||||
await screen.findByText('已打开:/tmp/authorized-game');
|
||||
await screen.findByText('已打开:authorized-game');
|
||||
expect(screen.getByText('未命名游戏原型')).not.toBeNull();
|
||||
expect(screen.getByText('/tmp/authorized-game')).not.toBeNull();
|
||||
expect(screen.queryByText('/tmp/authorized-game')).toBeNull();
|
||||
expect(screen.getByText('preview: 未启动')).not.toBeNull();
|
||||
const projectSummary = screen.getByLabelText('项目摘要');
|
||||
expect(projectSummary.textContent).toContain('任务:已完成 0/');
|
||||
|
||||
@@ -132,7 +132,12 @@ function memoryHost(input?: {
|
||||
let hostRevision = 0;
|
||||
let eventListener: ((event: LocalAssetCommittedEvent) => void) | null = null;
|
||||
const updates: ImageCanvasDraftCanvas[] = [];
|
||||
const commits: Array<{ commitId: string; idempotencyKey: string }> = [];
|
||||
const commits: Array<{
|
||||
commitId: string;
|
||||
idempotencyKey: string;
|
||||
assetName: string;
|
||||
assetKind: string;
|
||||
}> = [];
|
||||
const generationCalls: Array<{
|
||||
intentId: string;
|
||||
generationId: string;
|
||||
@@ -317,6 +322,8 @@ function memoryHost(input?: {
|
||||
commits.push({
|
||||
commitId: commitInput.commitId,
|
||||
idempotencyKey: commitInput.idempotencyKey,
|
||||
assetName: commitInput.name,
|
||||
assetKind: commitInput.assetKind,
|
||||
});
|
||||
await input?.commitGate?.promise;
|
||||
hostRevision += 1;
|
||||
@@ -372,6 +379,7 @@ function renderSurface(
|
||||
onCommitted = vi.fn(),
|
||||
onSaveAttempt = vi.fn(),
|
||||
onCancel = vi.fn(),
|
||||
initialAsset?: { name: string; kind: string },
|
||||
) {
|
||||
return {
|
||||
onCommitted,
|
||||
@@ -384,6 +392,8 @@ function renderSurface(
|
||||
scope={canvasScope}
|
||||
sessionId="22222222-2222-4222-8222-222222222222"
|
||||
expectedHostRevision="0"
|
||||
initialAssetName={initialAsset?.name}
|
||||
initialAssetKind={initialAsset?.kind}
|
||||
onCancel={onCancel}
|
||||
onCommitted={onCommitted}
|
||||
onSaveAttempt={onSaveAttempt}
|
||||
@@ -463,6 +473,78 @@ describe('Tauri 素材创作无限画布独立 Surface', () => {
|
||||
expect(screen.getByRole('button', { name: '取消' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('保留可编辑名称,并把新建资源用途限制为中文固定选项', async () => {
|
||||
const memory = memoryHost();
|
||||
renderSurface(memory.host);
|
||||
await screen.findByText('画布可编辑');
|
||||
|
||||
const assetName = screen.getByRole('textbox', { name: '素材名称' });
|
||||
const assetKind = screen.getByRole('combobox', { name: '资源用途' });
|
||||
expect((assetName as HTMLInputElement).value).toBe('画布素材');
|
||||
expect((assetKind as HTMLSelectElement).value).toBe('game-art');
|
||||
expect((assetKind as HTMLSelectElement).disabled).toBe(false);
|
||||
expect(
|
||||
Array.from((assetKind as HTMLSelectElement).options).map(
|
||||
(option) => option.text,
|
||||
),
|
||||
).toEqual([
|
||||
'普通游戏美术',
|
||||
'统一视觉规范',
|
||||
'游戏界面原型',
|
||||
'核心美术图集',
|
||||
]);
|
||||
|
||||
fireEvent.change(assetName, { target: { value: '主界面草图' } });
|
||||
fireEvent.change(assetKind, { target: { value: 'ui-prototype' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存到项目' }));
|
||||
|
||||
await waitFor(() => expect(memory.commits).toHaveLength(1));
|
||||
expect(memory.commits[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
assetName: '主界面草图',
|
||||
assetKind: 'ui-prototype',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('精修时继承源资源名称和用途,并锁定机器语义类型', async () => {
|
||||
const refineScope: ImageCanvasHostScope = {
|
||||
...scope,
|
||||
intent: 'refine',
|
||||
sourceAssetId: 'source-asset',
|
||||
};
|
||||
const memory = memoryHost({
|
||||
initialDraft: draftFixture(refineScope),
|
||||
});
|
||||
renderSurface(
|
||||
memory.host,
|
||||
refineScope,
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
{ name: '角色立绘', kind: 'illustration' },
|
||||
);
|
||||
await screen.findByText('画布可编辑');
|
||||
|
||||
const assetName = screen.getByRole('textbox', { name: '素材名称' });
|
||||
const assetKind = screen.getByRole('combobox', { name: '资源用途' });
|
||||
expect((assetName as HTMLInputElement).value).toBe('角色立绘');
|
||||
expect((assetName as HTMLInputElement).disabled).toBe(false);
|
||||
expect((assetKind as HTMLSelectElement).value).toBe('illustration');
|
||||
expect((assetKind as HTMLSelectElement).disabled).toBe(true);
|
||||
expect(screen.getByRole('option', { name: '原资源用途' })).toBeTruthy();
|
||||
|
||||
fireEvent.change(assetName, { target: { value: '角色立绘精修' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存到项目' }));
|
||||
await waitFor(() => expect(memory.commits).toHaveLength(1));
|
||||
expect(memory.commits[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
assetName: '角色立绘精修',
|
||||
assetKind: 'illustration',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('让生成、失败、保存与取消区域使用内部滚动而不被固定高度裁剪', () => {
|
||||
const css = readFileSync(
|
||||
resolve(
|
||||
@@ -487,9 +569,18 @@ describe('Tauri 素材创作无限画布独立 Surface', () => {
|
||||
expect(css).toMatch(
|
||||
/\.asset-canvas-surface__savebar\s*\{[^}]*grid-template-columns:/s,
|
||||
);
|
||||
expect(css).toMatch(
|
||||
/\.asset-canvas-surface__toolbar-shell\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\)[^}]*grid-template-rows:\s*auto auto/s,
|
||||
);
|
||||
expect(css).toMatch(
|
||||
/\.asset-canvas-surface \.asset-canvas-surface__save\s*\{[^}]*min-width:\s*max-content[^}]*white-space:\s*nowrap/s,
|
||||
);
|
||||
expect(css).toMatch(
|
||||
/@media \(max-width: 760px\)[\s\S]*?\.asset-canvas-surface__save\s*\{[^}]*grid-column:\s*1 \/ -1[^}]*width:\s*100%/s,
|
||||
);
|
||||
expect(css).toMatch(
|
||||
/@container \(max-width: 620px\)[\s\S]*?\.asset-canvas-surface__save\s*\{[^}]*grid-column:\s*1 \/ -1[^}]*width:\s*100%/s,
|
||||
);
|
||||
});
|
||||
|
||||
it('取消时先把草稿标记为 cancelled,再返回资源总览', async () => {
|
||||
|
||||
@@ -21,6 +21,8 @@ vi.mock('../src/features/asset-canvas/AssetCanvasSurface', async () => {
|
||||
sourceAssetId: string | null;
|
||||
};
|
||||
sessionId: string;
|
||||
initialAssetName?: string;
|
||||
initialAssetKind?: string;
|
||||
onCancel?: () => void;
|
||||
onSaveAttempt?: (attempt: {
|
||||
saveAttemptId: string;
|
||||
@@ -37,7 +39,7 @@ vi.mock('../src/features/asset-canvas/AssetCanvasSurface', async () => {
|
||||
ReactModule.createElement(
|
||||
'span',
|
||||
null,
|
||||
`${props.scope.intent}:${props.scope.sourceAssetId ?? 'none'}`,
|
||||
`${props.scope.intent}:${props.scope.sourceAssetId ?? 'none'}:${props.initialAssetName ?? 'unset'}:${props.initialAssetKind ?? 'unset'}`,
|
||||
),
|
||||
ReactModule.createElement(
|
||||
'button',
|
||||
@@ -275,7 +277,7 @@ describe('project resource live canvas integration', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '精修资源' }));
|
||||
expect(
|
||||
(await screen.findByLabelText('测试素材创作画布')).textContent,
|
||||
).toContain('refine:source-art');
|
||||
).toContain('refine:source-art:source-art:art-image');
|
||||
fireEvent.click(screen.getByRole('button', { name: '取消并返回' }));
|
||||
expect(
|
||||
await screen.findByRole('region', { name: /source-art\.png/ }),
|
||||
|
||||
Reference in New Issue
Block a user