合并最新 master 到 M0 分支
冲突解决: - provider_request_builders:M0A-2 的固定 owner 收束边界与 master 新增的 动态目标验收图改在同一段 prompt 构建。保留 owner 分支(固定 owner 仍不 广告 project.verify / command.run_limited),把 master 的动态目标分支提到 owner 判断之外对两条路径生效(固定 owner 属 goal_contract_participant, 本就该收到禁止调用 agent.goal_contract 的边界声明),git 提交协议改为仅 非 owner 路径追加以维持 M0A-2 边界。工具裁剪三个块并列保留,双方新增 测试全部保留。 - SupervisorChatOnlyView:保留 M0B-2 的谱系分支,采纳 master 的 projectWorkspaceStatusForDisplay 包装,避免该 import 变成死引用。 - runtime_actions:两侧各加一个导出,取并集。 - main_loop_tests:保留 M0B-1 需要的项目预初始化与 runtime lane。 - decision-log / pitfalls:双方条目都保留,各自维持原序。 验证:cargo check --all-targets 通过;前端 typecheck 干净; provider_request_builders / game_chat 委派 / goal-control 共 17 条 Rust 用例通过;agentRuntimeModel 与 appSurface 共 391 条前端用例通过。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -252,6 +252,7 @@ import {
|
||||
type ProjectAgentResultSummary,
|
||||
type ProjectAgentRuntimeSummary,
|
||||
} from './view/project-development';
|
||||
import type { ProjectManifestSnapshotMetadata } from './view/project-development/projectResourceLiveUpdateModel';
|
||||
|
||||
const initialSupervisorMessageClaimsByPage = new WeakMap<Window, Set<string>>();
|
||||
const LEGACY_GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY =
|
||||
@@ -497,6 +498,7 @@ type AppProps = {
|
||||
onManifestChange?: (
|
||||
projectPath: string,
|
||||
manifest: GameCreationAppManifest,
|
||||
metadata?: ProjectManifestSnapshotMetadata,
|
||||
) => void;
|
||||
onPreviewChange?: (preview: GameCreationAppPreviewState | null) => void;
|
||||
onAgentRuntimeSummariesChange?: (
|
||||
@@ -10699,7 +10701,41 @@ export function App({
|
||||
if (!projectSupervisorOnly || !nextProjectPath || !onManifestChange) {
|
||||
return;
|
||||
}
|
||||
onManifestChange(nextProjectPath, manifest);
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
const before = await invoke<LocalGameProjectRevisionStatus>(
|
||||
'get_local_game_project_revision',
|
||||
{ projectPath: nextProjectPath },
|
||||
);
|
||||
const currentManifest = await invoke<GameCreationAppManifest>(
|
||||
'get_local_game_manifest',
|
||||
{ projectPath: nextProjectPath },
|
||||
);
|
||||
const after = await invoke<LocalGameProjectRevisionStatus>(
|
||||
'get_local_game_project_revision',
|
||||
{ projectPath: nextProjectPath },
|
||||
);
|
||||
if (before.revision !== after.revision) {
|
||||
continue;
|
||||
}
|
||||
if (!cancelled) {
|
||||
onManifestChange(nextProjectPath, currentManifest, {
|
||||
projectId: currentManifest.projectId,
|
||||
revision: after.revision,
|
||||
source: 'supervisor',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
})().catch(() => undefined);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
localProject?.projectPath,
|
||||
manifest,
|
||||
@@ -11266,6 +11302,7 @@ export function App({
|
||||
{runtimeConfigOpen ? (
|
||||
<RuntimeConfigDialog
|
||||
projectPath={localProject?.projectPath}
|
||||
showDeveloperEditorApi={devMode}
|
||||
onClose={() => setRuntimeConfigOpen(false)}
|
||||
onLog={(entry) => setCommandLog((current) => [...current, entry])}
|
||||
/>
|
||||
|
||||
@@ -57,6 +57,7 @@ export type LauncherProjectContext = {
|
||||
projectName: string;
|
||||
projectKind: LocalProjectKind;
|
||||
manifest: GameCreationAppManifest;
|
||||
projectRevision: number | null;
|
||||
mode: HomeAgentMode | null;
|
||||
initialPrompt: string;
|
||||
attachments: LauncherImportedAttachment[];
|
||||
|
||||
@@ -917,6 +917,9 @@ export async function submitProjectSupervisorRuntimeTask({
|
||||
|
||||
export function agentRuntimeSteerStatus(result: AgentRuntimeSteerResult) {
|
||||
const runId = result.runtime.state.runId;
|
||||
if (result.status === 'restarted') {
|
||||
return `目标已变化,正在新 Run 重新理解并执行:${runId}`;
|
||||
}
|
||||
if (result.providerInterrupted) {
|
||||
return `LLM 已判定需要改向,旧 Provider 已安全中断:${runId}`;
|
||||
}
|
||||
@@ -1256,6 +1259,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[],
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { Fragment, useCallback, useState } from 'react';
|
||||
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { launcherNotifications } from '../../app/constants';
|
||||
import { closeDialogOnEscape } from '../../app/dialogs';
|
||||
import { resolveTauriInvoke } from '../../app/tauri';
|
||||
import type { LocalGameProjectRevisionStatus } from '../../app/types';
|
||||
import HomeView from '../../view/home';
|
||||
import { type LauncherView, Sidebar } from '../../view/layout';
|
||||
import ProjectDevelopmentView from '../../view/project-development';
|
||||
import {
|
||||
createProjectManifestMergeState,
|
||||
mergeProjectManifestSnapshot,
|
||||
type ProjectManifestMergeState,
|
||||
type ProjectManifestSnapshot,
|
||||
type ProjectManifestSnapshotMetadata,
|
||||
} from '../../view/project-development/projectResourceLiveUpdateModel';
|
||||
import { RuntimeConfigDialog } from '../runtime-config/RuntimeConfigDialog';
|
||||
import { AccountWalletBar, AccountWalletDialogs } from './AccountWallet';
|
||||
import {
|
||||
@@ -56,23 +65,127 @@ export function WorkspaceLauncherShell({
|
||||
createHomeDraft,
|
||||
openProject,
|
||||
} = homeProject;
|
||||
const activeProjectContextRef = useRef(currentProjectContext);
|
||||
const manifestMergeRef = useRef<ProjectManifestMergeState | null>(null);
|
||||
activeProjectContextRef.current = currentProjectContext;
|
||||
|
||||
useEffect(() => {
|
||||
const current = activeProjectContextRef.current;
|
||||
manifestMergeRef.current =
|
||||
current?.projectRevision === null || !current
|
||||
? null
|
||||
: createProjectManifestMergeState({
|
||||
projectPath: current.projectPath,
|
||||
projectId: current.manifest.projectId,
|
||||
revision: current.projectRevision,
|
||||
manifest: current.manifest,
|
||||
source: 'initial',
|
||||
});
|
||||
}, [currentProjectContext?.createdAt, currentProjectContext?.projectPath]);
|
||||
|
||||
const applyManifestSnapshot = useCallback(
|
||||
(snapshot: ProjectManifestSnapshot) => {
|
||||
const current = activeProjectContextRef.current;
|
||||
if (
|
||||
!current ||
|
||||
current.projectPath !== snapshot.projectPath ||
|
||||
current.manifest.projectId !== snapshot.projectId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let previous = manifestMergeRef.current;
|
||||
if (
|
||||
!previous ||
|
||||
previous.projectPath !== current.projectPath ||
|
||||
previous.projectId !== current.manifest.projectId
|
||||
) {
|
||||
previous =
|
||||
current.projectRevision === null
|
||||
? null
|
||||
: createProjectManifestMergeState({
|
||||
projectPath: current.projectPath,
|
||||
projectId: current.manifest.projectId,
|
||||
revision: current.projectRevision,
|
||||
manifest: current.manifest,
|
||||
source: 'initial',
|
||||
});
|
||||
}
|
||||
if (!previous) {
|
||||
manifestMergeRef.current = createProjectManifestMergeState(snapshot);
|
||||
} else {
|
||||
const merged = mergeProjectManifestSnapshot(previous, snapshot);
|
||||
manifestMergeRef.current = merged.state;
|
||||
if (merged.decision !== 'accepted') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setCurrentProjectContext((active) =>
|
||||
active &&
|
||||
active.projectPath === snapshot.projectPath &&
|
||||
active.manifest.projectId === snapshot.projectId
|
||||
? {
|
||||
...active,
|
||||
manifest: snapshot.manifest,
|
||||
projectRevision: snapshot.revision,
|
||||
}
|
||||
: active,
|
||||
);
|
||||
},
|
||||
[setCurrentProjectContext],
|
||||
);
|
||||
const syncActiveProjectManifest = useCallback(
|
||||
(
|
||||
sourceProjectPath: string,
|
||||
manifest: NonNullable<typeof currentProjectContext>['manifest'],
|
||||
metadata?: ProjectManifestSnapshotMetadata,
|
||||
) => {
|
||||
setCurrentProjectContext((current) => {
|
||||
if (
|
||||
!current ||
|
||||
current.projectPath !== sourceProjectPath ||
|
||||
current.manifest === manifest
|
||||
) {
|
||||
return current;
|
||||
const current = activeProjectContextRef.current;
|
||||
if (!current || current.projectPath !== sourceProjectPath) {
|
||||
return;
|
||||
}
|
||||
if (metadata) {
|
||||
applyManifestSnapshot({
|
||||
projectPath: sourceProjectPath,
|
||||
manifest,
|
||||
...metadata,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
if (current.projectRevision === null) {
|
||||
setCurrentProjectContext((active) =>
|
||||
active?.projectPath === sourceProjectPath
|
||||
? { ...active, manifest }
|
||||
: active,
|
||||
);
|
||||
}
|
||||
return { ...current, manifest };
|
||||
});
|
||||
return;
|
||||
}
|
||||
void invoke<LocalGameProjectRevisionStatus>(
|
||||
'get_local_game_project_revision',
|
||||
{ projectPath: sourceProjectPath },
|
||||
)
|
||||
.then((status) => {
|
||||
applyManifestSnapshot({
|
||||
projectPath: sourceProjectPath,
|
||||
projectId: manifest.projectId,
|
||||
revision: status.revision,
|
||||
manifest,
|
||||
source: 'supervisor',
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
if (activeProjectContextRef.current?.projectRevision === null) {
|
||||
setCurrentProjectContext((active) =>
|
||||
active?.projectPath === sourceProjectPath
|
||||
? { ...active, manifest }
|
||||
: active,
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
[setCurrentProjectContext],
|
||||
[applyManifestSnapshot, setCurrentProjectContext],
|
||||
);
|
||||
|
||||
function showLauncherNotice(title: string) {
|
||||
@@ -175,6 +288,7 @@ export function WorkspaceLauncherShell({
|
||||
preview={activeProjectPreview}
|
||||
agentRuntimeSummaries={activeProjectAgentRuntimeSummaries}
|
||||
agentResults={activeProjectAgentResults}
|
||||
onManifestChange={syncActiveProjectManifest}
|
||||
onHomeOpen={() => setLauncherView('home')}
|
||||
onProjectsOpen={() => setLauncherView('projects')}
|
||||
supervisor={
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
ProjectAgentResultSummary,
|
||||
ProjectAgentRuntimeSummary,
|
||||
} from '../../view/project-development';
|
||||
import type { ProjectManifestSnapshotMetadata } from '../../view/project-development/projectResourceLiveUpdateModel';
|
||||
import { isAbsoluteProjectPath } from '../project-summary/projectSummary';
|
||||
|
||||
const RECENT_WORKSPACES_STORAGE_KEY =
|
||||
@@ -38,6 +39,7 @@ export type ProjectSupervisorComponentProps = {
|
||||
onManifestChange?: (
|
||||
projectPath: string,
|
||||
manifest: GameCreationAppManifest,
|
||||
metadata?: ProjectManifestSnapshotMetadata,
|
||||
) => void;
|
||||
onPreviewChange?: (preview: GameCreationAppPreviewState | null) => void;
|
||||
onAgentRuntimeSummariesChange?: (
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
InitLocalProjectResult,
|
||||
LauncherImportedAttachment,
|
||||
LauncherProjectContext,
|
||||
LocalGameProjectRevisionStatus,
|
||||
LocalProjectDirectoryStatus,
|
||||
PendingNonEmptyProject,
|
||||
TauriInvoke,
|
||||
@@ -96,6 +97,23 @@ export function useHomeProjectCreation({
|
||||
rememberRecentWorkspace(context.projectPath);
|
||||
}
|
||||
|
||||
async function readCurrentProjectRevision(
|
||||
invoke: TauriInvoke,
|
||||
nextProjectPath: string,
|
||||
) {
|
||||
try {
|
||||
const status = await invoke<LocalGameProjectRevisionStatus>(
|
||||
'get_local_game_project_revision',
|
||||
{ projectPath: nextProjectPath },
|
||||
);
|
||||
return Number.isSafeInteger(status.revision) && status.revision >= 0
|
||||
? status.revision
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function importHomeAttachments(
|
||||
invoke: TauriInvoke,
|
||||
nextProjectPath: string,
|
||||
@@ -213,6 +231,10 @@ export function useHomeProjectCreation({
|
||||
result.manifest.name || projectNameFromPath(result.projectPath),
|
||||
projectKind: 'web',
|
||||
manifest: result.manifest,
|
||||
projectRevision: await readCurrentProjectRevision(
|
||||
invoke,
|
||||
result.projectPath,
|
||||
),
|
||||
mode,
|
||||
initialPrompt:
|
||||
prompt.trim() ||
|
||||
@@ -277,6 +299,10 @@ export function useHomeProjectCreation({
|
||||
result.manifest.name || projectNameFromPath(result.projectPath),
|
||||
projectKind: 'web',
|
||||
manifest: result.manifest,
|
||||
projectRevision: await readCurrentProjectRevision(
|
||||
invoke,
|
||||
result.projectPath,
|
||||
),
|
||||
mode: null,
|
||||
initialPrompt: '',
|
||||
attachments: [],
|
||||
@@ -338,6 +364,10 @@ export function useHomeProjectCreation({
|
||||
projectNameFromPath(trimmedProjectPath),
|
||||
projectKind: directoryStatus.isGodotProject ? 'godot' : 'web',
|
||||
manifest: projectManifest,
|
||||
projectRevision: await readCurrentProjectRevision(
|
||||
invoke,
|
||||
trimmedProjectPath,
|
||||
),
|
||||
mode: null,
|
||||
initialPrompt: '',
|
||||
attachments: [],
|
||||
@@ -481,6 +511,10 @@ export function useHomeProjectCreation({
|
||||
result.manifest.name || projectNameFromPath(result.projectPath),
|
||||
projectKind: 'godot',
|
||||
manifest: result.manifest,
|
||||
projectRevision: await readCurrentProjectRevision(
|
||||
invoke,
|
||||
result.projectPath,
|
||||
),
|
||||
mode: null,
|
||||
initialPrompt: '',
|
||||
attachments: [],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,469 @@
|
||||
.asset-canvas-surface {
|
||||
--image-canvas-brand-accent: var(--platform-button-primary-solid);
|
||||
--image-canvas-brand-accent-strong: var(--platform-text-strong);
|
||||
--image-canvas-brand-border-soft: var(--platform-icon-border);
|
||||
--image-canvas-brand-border-strong: var(--platform-nav-active-border);
|
||||
--image-canvas-brand-focus-ring: var(--platform-input-focus-ring);
|
||||
--image-canvas-brand-soft: var(--platform-nav-active-fill);
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
container-type: inline-size;
|
||||
overflow: hidden;
|
||||
border-radius: inherit;
|
||||
background: var(--platform-overlay-fill);
|
||||
color: var(--platform-text-strong);
|
||||
}
|
||||
|
||||
.asset-canvas-surface--loading {
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__toolbar-shell {
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
padding: 8px 10px 9px;
|
||||
border-bottom: 1px solid var(--platform-line-soft);
|
||||
background: var(--platform-desktop-topbar-fill);
|
||||
}
|
||||
|
||||
.asset-canvas-surface__toolbar {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
flex: 0 0 auto;
|
||||
padding-block: 1px 3px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__savebar {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: 620px;
|
||||
grid-template-columns:
|
||||
minmax(150px, 1.35fr) minmax(118px, 1fr) minmax(88px, 0.55fr)
|
||||
max-content;
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
align-self: flex-end;
|
||||
align-items: end;
|
||||
gap: 8px;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__save-field {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__save-field > span {
|
||||
color: var(--platform-text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
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);
|
||||
border-radius: 8px;
|
||||
background: var(--platform-input-fill);
|
||||
color: var(--platform-text-base);
|
||||
font: inherit;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__savebar input,
|
||||
.asset-canvas-surface__savebar select {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__savebar input:focus-visible,
|
||||
.asset-canvas-surface__savebar select:focus-visible,
|
||||
.asset-canvas-surface__generation-form select:focus-visible,
|
||||
.asset-canvas-surface__generation-form textarea:focus-visible {
|
||||
border-color: var(--platform-surface-hover-border);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--platform-input-focus-ring);
|
||||
}
|
||||
|
||||
.asset-canvas-surface__import-input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.asset-canvas-surface .asset-canvas-surface__primary-action,
|
||||
.asset-canvas-surface .asset-canvas-surface__save {
|
||||
border-color: var(--platform-button-primary-border);
|
||||
background: var(--platform-button-primary-fill);
|
||||
color: var(--platform-button-primary-text);
|
||||
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 {
|
||||
border-color: var(--platform-button-primary-border);
|
||||
background: var(--platform-button-primary-fill);
|
||||
color: var(--platform-button-primary-text);
|
||||
}
|
||||
|
||||
.asset-canvas-surface[data-state='canvas.generating']
|
||||
.asset-canvas-surface__viewport,
|
||||
.asset-canvas-surface[data-state='canvas.generating']
|
||||
.asset-canvas-surface__viewport-tools {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__dialog-backdrop {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 16px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--platform-text-strong) 36%,
|
||||
transparent
|
||||
);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.asset-canvas-surface__generation-dialog {
|
||||
display: grid;
|
||||
width: min(560px, 100%);
|
||||
max-height: calc(100% - 32px);
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--platform-modal-border);
|
||||
border-radius: 16px;
|
||||
background: var(--platform-modal-fill);
|
||||
color: var(--platform-text-strong);
|
||||
box-shadow: var(--platform-panel-shadow);
|
||||
}
|
||||
|
||||
.asset-canvas-surface__generation-dialog > header,
|
||||
.asset-canvas-surface__generation-dialog footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__generation-dialog > header {
|
||||
border-bottom: 1px solid var(--platform-line-soft);
|
||||
}
|
||||
|
||||
.asset-canvas-surface__generation-form,
|
||||
.asset-canvas-surface__generation-confirmation {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__generation-form label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: var(--platform-text-base);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__generation-form textarea {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
background: var(--platform-input-fill);
|
||||
color: var(--platform-text-strong);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__generation-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__generation-form fieldset {
|
||||
display: grid;
|
||||
max-height: 180px;
|
||||
gap: 8px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--platform-subpanel-border);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__generation-form fieldset label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__generation-form fieldset input {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__generation-dialog footer {
|
||||
justify-content: flex-end;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__viewport {
|
||||
grid-row: 2;
|
||||
min-height: 0;
|
||||
background-color: var(--platform-neutral-bg);
|
||||
background-image: radial-gradient(
|
||||
var(--platform-line-soft) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 18px 18px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__operation-overlay {
|
||||
z-index: 12;
|
||||
grid-row: 2;
|
||||
grid-column: 1;
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
place-items: center;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--platform-text-strong) 30%,
|
||||
transparent
|
||||
);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.asset-canvas-surface__operation-card {
|
||||
display: grid;
|
||||
width: min(440px, 100%);
|
||||
max-height: 100%;
|
||||
min-height: 0;
|
||||
gap: 14px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--platform-modal-border);
|
||||
border-radius: 18px;
|
||||
padding: 22px;
|
||||
background: var(--platform-modal-fill);
|
||||
color: var(--platform-text-strong);
|
||||
box-shadow: var(--platform-panel-shadow);
|
||||
}
|
||||
|
||||
.asset-canvas-surface__operation-card strong {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__operation-card p {
|
||||
margin: 0;
|
||||
color: var(--platform-text-base);
|
||||
line-height: 1.55;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__operation-card progress {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
accent-color: var(--platform-accent);
|
||||
}
|
||||
|
||||
.asset-canvas-surface__operation-kicker {
|
||||
color: var(--platform-warm-text);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__operation-card--failed {
|
||||
border-color: var(--platform-button-danger-border);
|
||||
}
|
||||
|
||||
.asset-canvas-surface__operation-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__resize-handle {
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
bottom: -8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid var(--platform-input-fill);
|
||||
border-radius: 50%;
|
||||
background: var(--platform-accent);
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__layer-hit-target {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__layer-hit-target:focus-visible,
|
||||
.asset-canvas-surface__resize-handle:focus-visible {
|
||||
outline: 3px solid var(--platform-focus-ring);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__viewport-tools {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
right: 14px;
|
||||
bottom: 46px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__zoom {
|
||||
max-width: min(100%, 280px);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.asset-canvas-surface .genarrative-image-canvas__minimap {
|
||||
width: 132px;
|
||||
height: 84px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
background: var(--platform-input-fill);
|
||||
box-shadow: var(--platform-nav-active-shadow);
|
||||
}
|
||||
|
||||
.asset-canvas-surface .genarrative-image-canvas__minimap-layer {
|
||||
background: var(--platform-warm-bg);
|
||||
}
|
||||
|
||||
.asset-canvas-surface .genarrative-image-canvas__minimap-viewport {
|
||||
border: 1px solid var(--platform-accent);
|
||||
}
|
||||
|
||||
.asset-canvas-surface__status {
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
min-height: 34px;
|
||||
max-height: 96px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid var(--platform-line-soft);
|
||||
background: var(--platform-desktop-topbar-fill);
|
||||
color: var(--platform-text-base);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__status > span {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__status > span:first-child {
|
||||
color: var(--platform-text-strong);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.asset-canvas-surface {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__toolbar-shell {
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__savebar {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.asset-canvas-surface__save-field:first-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__save {
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__viewport-tools {
|
||||
right: 8px;
|
||||
bottom: 52px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__generation-options {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__operation-overlay {
|
||||
place-items: end stretch;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.asset-canvas-surface__operation-card {
|
||||
width: auto;
|
||||
border-radius: 16px;
|
||||
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%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export type {
|
||||
CanvasLayer,
|
||||
CanvasViewport,
|
||||
ImageCanvasAssetPort,
|
||||
ImageCanvasCompletionPort,
|
||||
ImageCanvasGenerationPort,
|
||||
ImageCanvasProjectPort,
|
||||
} from '@genarrative/image-canvas-core';
|
||||
export {
|
||||
CanvasViewport as SharedCanvasViewport,
|
||||
CanvasWorld as SharedCanvasWorld,
|
||||
LayerRenderer as SharedLayerRenderer,
|
||||
Minimap as SharedMinimap,
|
||||
SelectionOverlay as SharedSelectionOverlay,
|
||||
ZoomControls as SharedZoomControls,
|
||||
} from '@genarrative/image-canvas-react';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -41,6 +41,7 @@ import {
|
||||
projectSupervisorCollaboratingAgentRuntimes,
|
||||
ProjectSupervisorRuntimeControls,
|
||||
projectSupervisorVisibleConversationText,
|
||||
projectWorkspaceStatusForDisplay,
|
||||
} from '../agent-runtime';
|
||||
import { RuntimeConfigDialog } from '../runtime-config/RuntimeConfigDialog';
|
||||
import { GameChatImageViewer } from './GameChatImageViewer';
|
||||
@@ -1000,7 +1001,7 @@ export function SupervisorChatOnlyView({
|
||||
? '专业 Agent 正在收束'
|
||||
: projectedRuntime
|
||||
? projectSupervisorChatRuntimeStatus(projectedRuntime)
|
||||
: workspaceStatus);
|
||||
: projectWorkspaceStatusForDisplay(workspaceStatus));
|
||||
const headerStatus = gameChatMode ? status : previewStatus || status;
|
||||
const runtimeEvents = useMemo(
|
||||
() => collectGameChatRuntimeEvents(projectedRuntime, runtimeByAgentId),
|
||||
|
||||
@@ -522,10 +522,12 @@ function normalizeRuntimeConfigDraft(
|
||||
|
||||
export function RuntimeConfigDialog({
|
||||
projectPath,
|
||||
showDeveloperEditorApi = false,
|
||||
onClose,
|
||||
onLog,
|
||||
}: {
|
||||
projectPath?: string;
|
||||
showDeveloperEditorApi?: boolean;
|
||||
onClose: () => void;
|
||||
onLog?: (entry: string) => void;
|
||||
}) {
|
||||
@@ -1379,28 +1381,38 @@ export function RuntimeConfigDialog({
|
||||
})}
|
||||
</>
|
||||
) : null}
|
||||
<label>
|
||||
画板 API Base URL
|
||||
<input
|
||||
aria-label="画板 API Base URL"
|
||||
value={runtimeConfigDraft.editorApi.baseUrl}
|
||||
onChange={(event) =>
|
||||
updateRuntimeEditorConfig('baseUrl', event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
画板 API Key
|
||||
<input
|
||||
aria-label="画板 API Key"
|
||||
autoComplete="off"
|
||||
type="password"
|
||||
value={runtimeConfigDraft.editorApi.apiKey}
|
||||
onChange={(event) =>
|
||||
updateRuntimeEditorConfig('apiKey', event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
{showDeveloperEditorApi ? (
|
||||
<>
|
||||
<label>
|
||||
开发者 External Editor Base URL
|
||||
<input
|
||||
aria-label="开发者 External Editor Base URL"
|
||||
value={runtimeConfigDraft.editorApi.baseUrl}
|
||||
onChange={(event) =>
|
||||
updateRuntimeEditorConfig(
|
||||
'baseUrl',
|
||||
event.currentTarget.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
开发者 External Editor API Key
|
||||
<input
|
||||
aria-label="开发者 External Editor API Key"
|
||||
autoComplete="off"
|
||||
type="password"
|
||||
value={runtimeConfigDraft.editorApi.apiKey}
|
||||
onChange={(event) =>
|
||||
updateRuntimeEditorConfig(
|
||||
'apiKey',
|
||||
event.currentTarget.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
) : null}
|
||||
<section className="runtime-mcp-section" aria-label="MCP servers">
|
||||
<header className="runtime-mcp-header">
|
||||
<div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import '@genarrative/image-canvas-react/styles.css';
|
||||
import './styles.css';
|
||||
|
||||
import React from 'react';
|
||||
@@ -12,8 +13,7 @@ import {
|
||||
} from './App';
|
||||
|
||||
const initialSearchParams = new URLSearchParams(window.location.search);
|
||||
const gameChatReleaseMode =
|
||||
import.meta.env.VITE_AGC_GAME_CHAT_ONLY === 'true';
|
||||
const gameChatReleaseMode = import.meta.env.VITE_AGC_GAME_CHAT_ONLY === 'true';
|
||||
const supervisorChatMode =
|
||||
import.meta.env.DEV && initialSearchParams.has('supervisor-chat');
|
||||
const gameChatMode =
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,142 @@
|
||||
import { ChevronLeft, LoaderCircle, Sparkles } from 'lucide-react';
|
||||
import { type FormEvent, useState } from 'react';
|
||||
|
||||
import {
|
||||
type LocalProjectResourceEditKind,
|
||||
resourceEditPromptMaxLength,
|
||||
} from './resourceEditModel';
|
||||
|
||||
export type ResourceEditSubmitInput = {
|
||||
prompt: string;
|
||||
assetName: string;
|
||||
};
|
||||
|
||||
export function ResourceEditSurface({
|
||||
resourceLabel,
|
||||
editKind,
|
||||
initialAssetName,
|
||||
semanticNotice,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: {
|
||||
resourceLabel: string;
|
||||
editKind: LocalProjectResourceEditKind;
|
||||
initialAssetName: string;
|
||||
semanticNotice: string | null;
|
||||
onCancel: () => void;
|
||||
onSubmit: (input: ResourceEditSubmitInput) => Promise<void>;
|
||||
}) {
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [assetName, setAssetName] = useState(initialAssetName);
|
||||
const [attempted, setAttempted] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const inputLocked = attempted || submitting;
|
||||
const promptMaxLength = resourceEditPromptMaxLength(editKind);
|
||||
|
||||
async function submit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const normalizedPrompt = prompt.trim();
|
||||
const normalizedName = assetName.trim();
|
||||
if (!normalizedPrompt || !normalizedName || submitting) {
|
||||
return;
|
||||
}
|
||||
setAttempted(true);
|
||||
setSubmitting(true);
|
||||
setError('');
|
||||
try {
|
||||
await onSubmit({ prompt: normalizedPrompt, assetName: normalizedName });
|
||||
} catch (submitError) {
|
||||
setError(
|
||||
submitError instanceof Error
|
||||
? submitError.message
|
||||
: String(submitError),
|
||||
);
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
className="game-resource-editor"
|
||||
aria-labelledby="game-resource-editor-title"
|
||||
>
|
||||
<header className="game-resource-editor-titlebar">
|
||||
<button
|
||||
type="button"
|
||||
className="game-resource-editor-back"
|
||||
onClick={onCancel}
|
||||
disabled={submitting}
|
||||
>
|
||||
<ChevronLeft size={16} aria-hidden="true" />
|
||||
返回资源
|
||||
</button>
|
||||
<span>
|
||||
<small>编辑现有资源</small>
|
||||
<strong id="game-resource-editor-title">{resourceLabel}</strong>
|
||||
</span>
|
||||
</header>
|
||||
<form className="game-resource-editor-form" onSubmit={submit}>
|
||||
{editKind !== 'version' ? (
|
||||
<label>
|
||||
<span>派生资源名称</span>
|
||||
<input
|
||||
value={assetName}
|
||||
maxLength={120}
|
||||
disabled={inputLocked}
|
||||
onChange={(event) => setAssetName(event.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
<label>
|
||||
<span>编辑提示词</span>
|
||||
<textarea
|
||||
value={prompt}
|
||||
maxLength={promptMaxLength}
|
||||
rows={7}
|
||||
autoFocus
|
||||
disabled={inputLocked}
|
||||
placeholder="描述希望在现有资源基础上发生的变化"
|
||||
onChange={(event) => setPrompt(event.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
{semanticNotice ? (
|
||||
<p className="game-resource-editor-semantic-note">{semanticNotice}</p>
|
||||
) : null}
|
||||
{error ? (
|
||||
<p className="game-resource-editor-error" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="game-resource-editor-actions">
|
||||
{error ? (
|
||||
<button type="submit" disabled={submitting}>
|
||||
<Sparkles size={15} aria-hidden="true" />
|
||||
使用原请求重试
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting || !prompt.trim() || !assetName.trim()}
|
||||
>
|
||||
{submitting ? (
|
||||
<LoaderCircle
|
||||
className="game-resource-editor-spinner"
|
||||
size={15}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<Sparkles size={15} aria-hidden="true" />
|
||||
)}
|
||||
{submitting
|
||||
? '正在派生…'
|
||||
: editKind === 'version'
|
||||
? '创建子版本'
|
||||
: '生成派生资源'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+199
@@ -0,0 +1,199 @@
|
||||
import type { GameCreationAppManifest } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
|
||||
export type ProjectManifestSnapshotSource =
|
||||
| 'initial'
|
||||
| 'supervisor'
|
||||
| 'asset-command'
|
||||
| 'asset-event'
|
||||
| 'poll';
|
||||
|
||||
export type ProjectManifestSnapshot = {
|
||||
projectPath: string;
|
||||
projectId: string;
|
||||
revision: number;
|
||||
manifest: GameCreationAppManifest;
|
||||
source: ProjectManifestSnapshotSource;
|
||||
commitId?: string;
|
||||
eventId?: string;
|
||||
};
|
||||
|
||||
export type ProjectManifestSnapshotMetadata = Omit<
|
||||
ProjectManifestSnapshot,
|
||||
'projectPath' | 'manifest'
|
||||
>;
|
||||
|
||||
export type ProjectManifestMergeState = {
|
||||
projectPath: string;
|
||||
projectId: string;
|
||||
revision: number;
|
||||
manifestFingerprint: string;
|
||||
seenCommitRevisions: string[];
|
||||
seenEventIds: string[];
|
||||
};
|
||||
|
||||
export type ProjectManifestMergeDecision =
|
||||
| 'accepted'
|
||||
| 'duplicate'
|
||||
| 'stale-revision'
|
||||
| 'scope-mismatch'
|
||||
| 'revision-conflict';
|
||||
|
||||
const MAX_DEDUPE_IDENTITIES = 256;
|
||||
|
||||
function appendBounded(values: string[], value: string | undefined) {
|
||||
if (!value || values.includes(value)) {
|
||||
return values;
|
||||
}
|
||||
return [...values, value].slice(-MAX_DEDUPE_IDENTITIES);
|
||||
}
|
||||
|
||||
function manifestFingerprint(manifest: GameCreationAppManifest) {
|
||||
return JSON.stringify(manifest);
|
||||
}
|
||||
|
||||
function commitRevisionIdentity(snapshot: ProjectManifestSnapshot) {
|
||||
return snapshot.commitId
|
||||
? `${snapshot.projectId}:${snapshot.commitId}:${snapshot.revision}`
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function withDedupeIdentities(
|
||||
state: ProjectManifestMergeState,
|
||||
snapshot: ProjectManifestSnapshot,
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
seenCommitRevisions: appendBounded(
|
||||
state.seenCommitRevisions,
|
||||
commitRevisionIdentity(snapshot),
|
||||
),
|
||||
seenEventIds: appendBounded(state.seenEventIds, snapshot.eventId),
|
||||
};
|
||||
}
|
||||
|
||||
export function createProjectManifestMergeState(
|
||||
snapshot: ProjectManifestSnapshot,
|
||||
): ProjectManifestMergeState {
|
||||
return withDedupeIdentities(
|
||||
{
|
||||
projectPath: snapshot.projectPath,
|
||||
projectId: snapshot.projectId,
|
||||
revision: snapshot.revision,
|
||||
manifestFingerprint: manifestFingerprint(snapshot.manifest),
|
||||
seenCommitRevisions: [],
|
||||
seenEventIds: [],
|
||||
},
|
||||
snapshot,
|
||||
);
|
||||
}
|
||||
|
||||
export function mergeProjectManifestSnapshot(
|
||||
current: ProjectManifestMergeState,
|
||||
snapshot: ProjectManifestSnapshot,
|
||||
): {
|
||||
decision: ProjectManifestMergeDecision;
|
||||
state: ProjectManifestMergeState;
|
||||
} {
|
||||
if (
|
||||
snapshot.projectPath !== current.projectPath ||
|
||||
snapshot.projectId !== current.projectId ||
|
||||
snapshot.manifest.projectId !== snapshot.projectId
|
||||
) {
|
||||
return { decision: 'scope-mismatch', state: current };
|
||||
}
|
||||
if (snapshot.revision < current.revision) {
|
||||
return { decision: 'stale-revision', state: current };
|
||||
}
|
||||
|
||||
const fingerprint = manifestFingerprint(snapshot.manifest);
|
||||
if (snapshot.revision === current.revision) {
|
||||
if (fingerprint !== current.manifestFingerprint) {
|
||||
return { decision: 'revision-conflict', state: current };
|
||||
}
|
||||
return {
|
||||
decision: 'duplicate',
|
||||
state: withDedupeIdentities(current, snapshot),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
decision: 'accepted',
|
||||
state: withDedupeIdentities(
|
||||
{
|
||||
...current,
|
||||
revision: snapshot.revision,
|
||||
manifestFingerprint: fingerprint,
|
||||
},
|
||||
snapshot,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export type ResourceFocusIntent = {
|
||||
flowId: string;
|
||||
saveAttemptId: string;
|
||||
sessionId: string;
|
||||
draftId: string;
|
||||
commitId: string;
|
||||
projectPath: string;
|
||||
projectId: string;
|
||||
focusGeneration: number;
|
||||
resourceId: string | null;
|
||||
completed: boolean;
|
||||
};
|
||||
|
||||
export type ResourceFocusResolution =
|
||||
| 'invalid'
|
||||
| 'wait-projection'
|
||||
| 'wait-layout'
|
||||
| 'hidden'
|
||||
| 'wait-dom'
|
||||
| 'focus'
|
||||
| 'completed';
|
||||
|
||||
export function resolveResourceFocusIntent(
|
||||
intent: ResourceFocusIntent,
|
||||
input: {
|
||||
projectPath: string;
|
||||
projectId: string;
|
||||
flowId: string | null;
|
||||
focusGeneration: number;
|
||||
projected: boolean;
|
||||
dependencyLayoutSettled: boolean;
|
||||
dependencyPositioned: boolean;
|
||||
typeLayoutSettled: boolean;
|
||||
typePositioned: boolean;
|
||||
visible: boolean;
|
||||
domRendered: boolean;
|
||||
},
|
||||
): ResourceFocusResolution {
|
||||
if (intent.completed) {
|
||||
return 'completed';
|
||||
}
|
||||
if (
|
||||
intent.projectPath !== input.projectPath ||
|
||||
intent.projectId !== input.projectId ||
|
||||
intent.flowId !== input.flowId ||
|
||||
intent.focusGeneration !== input.focusGeneration
|
||||
) {
|
||||
return 'invalid';
|
||||
}
|
||||
if (!intent.resourceId || !input.projected) {
|
||||
return 'wait-projection';
|
||||
}
|
||||
if (
|
||||
!input.dependencyLayoutSettled ||
|
||||
!input.typeLayoutSettled ||
|
||||
!input.dependencyPositioned ||
|
||||
!input.typePositioned
|
||||
) {
|
||||
return 'wait-layout';
|
||||
}
|
||||
if (!input.visible) {
|
||||
return 'hidden';
|
||||
}
|
||||
if (!input.domRendered) {
|
||||
return 'wait-dom';
|
||||
}
|
||||
return 'focus';
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import type { ProjectResource } from './resourceProjectionModel';
|
||||
|
||||
export type LocalProjectResourceEditKind =
|
||||
| 'image-reference'
|
||||
| 'svg'
|
||||
| 'video'
|
||||
| 'sound-effect'
|
||||
| 'background-music'
|
||||
| 'text'
|
||||
| 'agent-result'
|
||||
| 'version';
|
||||
|
||||
export type ProjectResourceEditCapability =
|
||||
| {
|
||||
route: 'image-canvas';
|
||||
sourceAssetId: string;
|
||||
normalizeSource: false;
|
||||
mediaType: 'image/png' | 'image/jpeg' | 'image/webp';
|
||||
}
|
||||
| {
|
||||
route: 'image-canvas';
|
||||
sourceAssetId: null;
|
||||
normalizeSource: true;
|
||||
mediaType: 'image/png' | 'image/jpeg' | 'image/webp';
|
||||
}
|
||||
| {
|
||||
route: 'derive';
|
||||
editKind: LocalProjectResourceEditKind;
|
||||
sourceMediaType: string;
|
||||
semanticNotice: string | null;
|
||||
};
|
||||
|
||||
const extensionMediaTypes: Record<string, string> = {
|
||||
png: 'image/png',
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
webp: 'image/webp',
|
||||
gif: 'image/gif',
|
||||
svg: 'image/svg+xml',
|
||||
avif: 'image/avif',
|
||||
bmp: 'image/bmp',
|
||||
mp4: 'video/mp4',
|
||||
webm: 'video/webm',
|
||||
mov: 'video/quicktime',
|
||||
mp3: 'audio/mpeg',
|
||||
wav: 'audio/wav',
|
||||
ogg: 'audio/ogg',
|
||||
m4a: 'audio/mp4',
|
||||
aac: 'audio/aac',
|
||||
flac: 'audio/flac',
|
||||
opus: 'audio/opus',
|
||||
md: 'text/markdown',
|
||||
markdown: 'text/markdown',
|
||||
mdx: 'application/mdx',
|
||||
txt: 'text/plain',
|
||||
json: 'application/json',
|
||||
yaml: 'application/yaml',
|
||||
yml: 'application/yaml',
|
||||
toml: 'application/toml',
|
||||
html: 'text/html',
|
||||
htm: 'text/html',
|
||||
css: 'text/css',
|
||||
scss: 'text/css',
|
||||
less: 'text/css',
|
||||
js: 'text/javascript',
|
||||
jsx: 'text/javascript',
|
||||
mjs: 'text/javascript',
|
||||
cjs: 'text/javascript',
|
||||
ts: 'text/typescript',
|
||||
tsx: 'text/typescript',
|
||||
rs: 'text/x-rust',
|
||||
py: 'text/x-python',
|
||||
go: 'text/x-go',
|
||||
java: 'text/plain',
|
||||
kt: 'text/plain',
|
||||
kts: 'text/plain',
|
||||
c: 'text/plain',
|
||||
cc: 'text/plain',
|
||||
cpp: 'text/plain',
|
||||
h: 'text/plain',
|
||||
hpp: 'text/plain',
|
||||
cs: 'text/plain',
|
||||
swift: 'text/plain',
|
||||
php: 'text/plain',
|
||||
rb: 'text/plain',
|
||||
lua: 'text/plain',
|
||||
sh: 'text/plain',
|
||||
bash: 'text/plain',
|
||||
zsh: 'text/plain',
|
||||
sql: 'text/plain',
|
||||
graphql: 'text/plain',
|
||||
gql: 'text/plain',
|
||||
xml: 'text/plain',
|
||||
csv: 'text/csv',
|
||||
ini: 'text/plain',
|
||||
conf: 'text/plain',
|
||||
vue: 'text/plain',
|
||||
svelte: 'text/plain',
|
||||
};
|
||||
|
||||
function pathExtension(path: string) {
|
||||
return path.split(/[?#]/u, 1)[0]?.split('.').pop()?.toLowerCase() ?? '';
|
||||
}
|
||||
|
||||
export function canonicalProjectedResourceMediaType(resource: ProjectResource) {
|
||||
const declared = resource.mediaType.trim().toLowerCase();
|
||||
if (/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/u.test(declared)) {
|
||||
return declared;
|
||||
}
|
||||
return extensionMediaTypes[pathExtension(resource.path)] ?? declared;
|
||||
}
|
||||
|
||||
function isBackgroundMusic(resource: ProjectResource) {
|
||||
const semanticSource = `${resource.subtype} ${resource.path}`.toLowerCase();
|
||||
return ['background', 'bgm', 'music', 'theme'].some((marker) =>
|
||||
semanticSource.includes(marker),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveProjectResourceEditCapability(
|
||||
resource: ProjectResource,
|
||||
): ProjectResourceEditCapability {
|
||||
if (resource.version) {
|
||||
return {
|
||||
route: 'derive',
|
||||
editKind: 'version',
|
||||
sourceMediaType: 'application/vnd.genarrative.project-version+json',
|
||||
semanticNotice: null,
|
||||
};
|
||||
}
|
||||
if (resource.subtype === 'agent-result') {
|
||||
return {
|
||||
route: 'derive',
|
||||
editKind: 'agent-result',
|
||||
sourceMediaType: 'text/markdown',
|
||||
semanticNotice: null,
|
||||
};
|
||||
}
|
||||
|
||||
const mediaType = canonicalProjectedResourceMediaType(resource);
|
||||
if (['image/png', 'image/jpeg', 'image/webp'].includes(mediaType)) {
|
||||
return resource.manifestAssetId
|
||||
? {
|
||||
route: 'image-canvas',
|
||||
sourceAssetId: resource.manifestAssetId,
|
||||
normalizeSource: false,
|
||||
mediaType: mediaType as 'image/png' | 'image/jpeg' | 'image/webp',
|
||||
}
|
||||
: {
|
||||
route: 'image-canvas',
|
||||
sourceAssetId: null,
|
||||
normalizeSource: true,
|
||||
mediaType: mediaType as 'image/png' | 'image/jpeg' | 'image/webp',
|
||||
};
|
||||
}
|
||||
if (mediaType === 'image/svg+xml') {
|
||||
return {
|
||||
route: 'derive',
|
||||
editKind: 'svg',
|
||||
sourceMediaType: mediaType,
|
||||
semanticNotice: null,
|
||||
};
|
||||
}
|
||||
if (mediaType.startsWith('image/')) {
|
||||
return {
|
||||
route: 'derive',
|
||||
editKind: 'image-reference',
|
||||
sourceMediaType: mediaType,
|
||||
semanticNotice: null,
|
||||
};
|
||||
}
|
||||
if (mediaType.startsWith('video/')) {
|
||||
return {
|
||||
route: 'derive',
|
||||
editKind: 'video',
|
||||
sourceMediaType: mediaType,
|
||||
semanticNotice: null,
|
||||
};
|
||||
}
|
||||
if (resource.category === 'audio' || mediaType.startsWith('audio/')) {
|
||||
return {
|
||||
route: 'derive',
|
||||
editKind: isBackgroundMusic(resource)
|
||||
? 'background-music'
|
||||
: 'sound-effect',
|
||||
sourceMediaType: mediaType,
|
||||
semanticNotice: '音频将基于原资源语义派生重制,不会修改原音频波形。',
|
||||
};
|
||||
}
|
||||
return {
|
||||
route: 'derive',
|
||||
editKind: 'text',
|
||||
sourceMediaType: mediaType,
|
||||
semanticNotice: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function defaultDerivedResourceName(resource: ProjectResource) {
|
||||
const baseName = resource.label.replace(/\.[^.]+$/u, '').trim();
|
||||
return `${baseName || '资源'}-编辑版`;
|
||||
}
|
||||
|
||||
export function resourceEditPromptMaxLength(
|
||||
editKind: LocalProjectResourceEditKind,
|
||||
) {
|
||||
switch (editKind) {
|
||||
case 'background-music':
|
||||
return 140;
|
||||
case 'sound-effect':
|
||||
return 1_900;
|
||||
case 'video':
|
||||
return 4_000;
|
||||
default:
|
||||
return 32_000;
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,8 @@ export type ProjectResource = {
|
||||
version?: ProjectVersionResourceSummary;
|
||||
};
|
||||
|
||||
const documentExtension = /\.(md|markdown|mdx|txt|json|ya?ml|toml)$/iu;
|
||||
const documentExtension =
|
||||
/\.(md|markdown|mdx|txt|json|ya?ml|toml|html?|css|scss|less|m?[jt]sx?|cjs|rs|py|go|java|kt|kts|c|cc|cpp|h|hpp|cs|swift|php|rb|lua|sh|bash|zsh|sql|graphql|gql|xml|csv|ini|conf|vue|svelte)$/iu;
|
||||
const artExtension = /\.(png|jpe?g|webp|gif|svg|avif|bmp|mp4|webm|mov)$/iu;
|
||||
const audioExtension = /\.(mp3|wav|ogg|m4a|aac|flac|opus)$/iu;
|
||||
const artKind =
|
||||
@@ -120,7 +121,7 @@ export function projectResourcesFromReadModels(
|
||||
}
|
||||
for (const path of task.artifacts) {
|
||||
const category = classifyProjectedResource({ path, mediaType: '' });
|
||||
if (!category || category === 'audio') {
|
||||
if (!category) {
|
||||
continue;
|
||||
}
|
||||
resources.push({
|
||||
|
||||
+52
-17
@@ -28,6 +28,7 @@ type LayoutScope = {
|
||||
projectPath: string;
|
||||
projectId: string;
|
||||
mode: ProjectResourceCanvasLayoutMode;
|
||||
resourceSignature: string;
|
||||
};
|
||||
|
||||
type ManualLayoutWriteIntent = {
|
||||
@@ -54,8 +55,9 @@ function createScopeKey(
|
||||
projectPath: string,
|
||||
projectId: string,
|
||||
mode: ProjectResourceCanvasLayoutMode,
|
||||
resourceSignature: string,
|
||||
) {
|
||||
return JSON.stringify([projectPath, projectId, mode]);
|
||||
return JSON.stringify([projectPath, projectId, mode, resourceSignature]);
|
||||
}
|
||||
|
||||
export function createResourceSignature(resources: ResourceCanvasItem[]) {
|
||||
@@ -125,6 +127,7 @@ export function useProjectResourceCanvasLayout({
|
||||
mode,
|
||||
resources,
|
||||
initializationReady = true,
|
||||
renderFallbackWhileBlocked = false,
|
||||
rederiveAutomaticPositions = false,
|
||||
}: {
|
||||
projectPath: string;
|
||||
@@ -132,29 +135,36 @@ export function useProjectResourceCanvasLayout({
|
||||
mode: ProjectResourceCanvasLayoutMode;
|
||||
resources: ResourceCanvasItem[];
|
||||
initializationReady?: boolean;
|
||||
renderFallbackWhileBlocked?: boolean;
|
||||
rederiveAutomaticPositions?: boolean;
|
||||
}) {
|
||||
const scopeKey = createScopeKey(projectPath, projectId, mode);
|
||||
const resourceSignature = useMemo(
|
||||
() => createResourceSignature(resources),
|
||||
[resources],
|
||||
);
|
||||
const fallback = useMemo(
|
||||
() => {
|
||||
const empty = createEmptyResourceCanvasLayout(projectId, mode);
|
||||
return initializationReady
|
||||
? reconcileLayout(empty, resources, rederiveAutomaticPositions).layout
|
||||
: empty;
|
||||
}, [
|
||||
initializationReady,
|
||||
mode,
|
||||
projectId,
|
||||
rederiveAutomaticPositions,
|
||||
resources,
|
||||
]);
|
||||
const scopeKey = createScopeKey(
|
||||
projectPath,
|
||||
projectId,
|
||||
mode,
|
||||
resourceSignature,
|
||||
);
|
||||
const fallback = useMemo(() => {
|
||||
const empty = createEmptyResourceCanvasLayout(projectId, mode);
|
||||
return initializationReady || renderFallbackWhileBlocked
|
||||
? reconcileLayout(empty, resources, rederiveAutomaticPositions).layout
|
||||
: empty;
|
||||
}, [
|
||||
initializationReady,
|
||||
mode,
|
||||
projectId,
|
||||
rederiveAutomaticPositions,
|
||||
renderFallbackWhileBlocked,
|
||||
resources,
|
||||
]);
|
||||
const [layout, setLayout] = useState<ProjectResourceCanvasLayout>(fallback);
|
||||
const [notice, setNotice] = useState<LayoutNotice>('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [readyScopeKey, setReadyScopeKey] = useState<string | null>(null);
|
||||
const mountedRef = useRef(true);
|
||||
const layoutRef = useRef<ProjectResourceCanvasLayout>(fallback);
|
||||
const persistedLayoutRef = useRef<ProjectResourceCanvasLayout>(fallback);
|
||||
@@ -167,6 +177,7 @@ export function useProjectResourceCanvasLayout({
|
||||
projectPath,
|
||||
projectId,
|
||||
mode,
|
||||
resourceSignature,
|
||||
});
|
||||
const writeQueueRef = useRef<LayoutWriteIntent[]>([]);
|
||||
const activeWriteIntentRef = useRef<LayoutWriteIntent | null>(null);
|
||||
@@ -383,8 +394,7 @@ export function useProjectResourceCanvasLayout({
|
||||
result.layout,
|
||||
resourcesRef.current,
|
||||
rederiveAutomaticPositions,
|
||||
)
|
||||
.changed
|
||||
).changed
|
||||
) {
|
||||
enqueueResourceSyncRef.current(currentScope.epoch);
|
||||
}
|
||||
@@ -483,12 +493,14 @@ export function useProjectResourceCanvasLayout({
|
||||
projectPath,
|
||||
projectId,
|
||||
mode,
|
||||
resourceSignature,
|
||||
};
|
||||
scopeRef.current = scope;
|
||||
initializedScopeEpochRef.current = null;
|
||||
writeQueueRef.current = [];
|
||||
activeWriteIntentRef.current = null;
|
||||
redragRequiredScopeEpochRef.current = null;
|
||||
setReadyScopeKey(null);
|
||||
const emptyLayout = createEmptyResourceCanvasLayout(projectId, mode);
|
||||
if (!initializationReady) {
|
||||
persistedLayoutRef.current = emptyLayout;
|
||||
@@ -510,6 +522,7 @@ export function useProjectResourceCanvasLayout({
|
||||
const invoke = window.__TAURI__?.core?.invoke;
|
||||
if (!invoke) {
|
||||
initializedScopeEpochRef.current = epoch;
|
||||
setReadyScopeKey(scopeKey);
|
||||
pumpWritesRef.current();
|
||||
return undefined;
|
||||
}
|
||||
@@ -531,6 +544,7 @@ export function useProjectResourceCanvasLayout({
|
||||
}
|
||||
persistedLayoutRef.current = loaded;
|
||||
initializedScopeEpochRef.current = epoch;
|
||||
setReadyScopeKey(scopeKey);
|
||||
if (
|
||||
reconcileLayout(
|
||||
loaded,
|
||||
@@ -549,6 +563,7 @@ export function useProjectResourceCanvasLayout({
|
||||
}
|
||||
persistedLayoutRef.current = initialFallback;
|
||||
initializedScopeEpochRef.current = epoch;
|
||||
setReadyScopeKey(scopeKey);
|
||||
rebuildOptimisticLayout(epoch);
|
||||
setNotice('布局读取失败,已使用当前会话布局');
|
||||
pumpWritesRef.current();
|
||||
@@ -564,6 +579,7 @@ export function useProjectResourceCanvasLayout({
|
||||
projectPath,
|
||||
rebuildOptimisticLayout,
|
||||
rederiveAutomaticPositions,
|
||||
resourceSignature,
|
||||
scopeKey,
|
||||
]);
|
||||
|
||||
@@ -670,11 +686,30 @@ export function useProjectResourceCanvasLayout({
|
||||
scopeRef.current.key === scopeKey &&
|
||||
layout.projectId === projectId &&
|
||||
layout.mode === mode;
|
||||
const ready = scopeMatches && readyScopeKey === scopeKey;
|
||||
const allResourcesPositioned = resources.every((resource) =>
|
||||
layout.positions.some(
|
||||
(position) =>
|
||||
position.resourceId === resource.id &&
|
||||
position.section === resource.category,
|
||||
),
|
||||
);
|
||||
const settled =
|
||||
ready &&
|
||||
!saving &&
|
||||
activeWriteIntentRef.current === null &&
|
||||
!writeQueueRef.current.some(
|
||||
(intent) => intent.scopeEpoch === scopeRef.current.epoch,
|
||||
) &&
|
||||
allResourcesPositioned;
|
||||
|
||||
return {
|
||||
layout: scopeMatches ? layout : fallback,
|
||||
notice,
|
||||
saving,
|
||||
ready,
|
||||
settled,
|
||||
scopeIdentity: scopeKey,
|
||||
commitPosition,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user