WIP: AGC 资源画布与替换改造 V3.0 #316

Draft
suzmii wants to merge 131 commits from feat/agc-canvas-resource-workbench-v3 into master
7 changed files with 73 additions and 25 deletions
Showing only changes of commit b3989b0d5c - Show all commits
+6 -2
View File
@@ -10691,6 +10691,9 @@ export function App({
(asset) => asset.localPath && !asset.localPath.startsWith('.agent/'),
)
.slice(0, 8);
const chatProjectAssets = manifest.assets.filter(
(asset) => asset.localPath && !asset.localPath.startsWith('.agent/'),
);
const visibleMainProjectCheckpoints = projectCheckpoints.slice(0, 5);
const currentProjectTitle = localProject
? manifest.name.trim() || projectNameFromPath(localProject.projectPath)
@@ -10940,6 +10943,7 @@ export function App({
chatInput={chatInput}
chatReferences={chatReferences}
composerRef={chatComposerRef}
chatProjectAssets={chatProjectAssets}
messagesRef={supervisorChatMessagesRef}
onCancelConfirmation={cancelUiCommandConfirmation}
onChatInputChange={handleChatComposerChange}
@@ -10968,7 +10972,6 @@ export function App({
hiddenConversationCount={hiddenConversationCount}
needsUserInput={projectSupervisorNeedsUserInput}
visibleMessages={visibleMessages}
visibleMainProjectAssets={visibleMainProjectAssets}
workspaceStatus={workspaceStatus}
expectedRunId={projectSupervisorExpectedRunId}
/>
@@ -10981,6 +10984,7 @@ export function App({
chatInput={chatInput}
chatReferences={chatReferences}
composerRef={chatComposerRef}
chatProjectAssets={chatProjectAssets}
directCodex={directCodexProductRuntime}
directStatus={directCodexProductRuntime ? directCodexStatus : null}
directProcessDetail={
@@ -11011,7 +11015,6 @@ export function App({
: projectSupervisorTransientReply
}
visibleMessages={visibleMessages}
visibleMainProjectAssets={visibleMainProjectAssets}
visibleProfessionalAgentCards={visibleProfessionalAgentCards}
showProfessionalCollaboration={
!directCodexProductRuntime && orchestrationMode === 'professional-dag'
@@ -11062,6 +11065,7 @@ export function App({
chatInput={chatInput}
chatReferences={chatReferences}
composerRef={chatComposerRef}
chatProjectAssets={chatProjectAssets}
chatInputRef={chatInputRef}
confirmProjectCreateInNonEmptyFolder={
confirmProjectCreateInNonEmptyFolder
@@ -69,6 +69,7 @@ function directStatusTitle(status: string | null | undefined) {
type ProjectSupervisorViewProps = RuntimePanelProps & {
chatInput: string;
chatReferences: ChatReference[];
chatProjectAssets: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[];
composerRef?: Ref<ResourceReferenceInputHandle>;
directCodex?: boolean;
directStatus?: GameCreatorDirectTurnUpdateStatus | null;
@@ -103,12 +104,12 @@ type ProjectSupervisorViewProps = RuntimePanelProps & {
comment: string | null,
) => Promise<void>;
onMakeGameFromApprovedGdd?: () => Promise<void>;
visibleMainProjectAssets?: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[];
};
export function ProjectSupervisorView({
chatInput,
chatReferences,
chatProjectAssets,
composerRef,
directCodex = false,
directStatus = null,
@@ -140,7 +141,6 @@ export function ProjectSupervisorView({
onPlanGddRefresh,
onPlanGddDecision,
onMakeGameFromApprovedGdd,
visibleMainProjectAssets = [],
...runtimePanelProps
}: ProjectSupervisorViewProps) {
const [expandedProcessKey, setExpandedProcessKey] = useState<string | null>(
@@ -363,7 +363,7 @@ export function ProjectSupervisorView({
<ResourceReferenceInput
ref={composerRef}
ariaLabel={directCodex ? '陶泥儿对话内容' : '项目需求'}
assets={visibleMainProjectAssets}
assets={chatProjectAssets}
projectPath={projectPath}
disabled={
runtimePanelProps.controlBusy || needsUserInput || modelValidating
@@ -65,6 +65,7 @@ type ProjectWorkspaceChatPaneProps = {
chatAgentBusy: boolean;
chatInput: string;
chatReferences: ChatReference[];
chatProjectAssets: GameCreationAppAssetManifestEntry[];
composerRef?: Ref<ResourceReferenceInputHandle>;
chatInputRef: RefObject<HTMLDivElement | null>;
confirmProjectCreateInNonEmptyFolder: () => void;
@@ -211,6 +212,7 @@ export function ProjectWorkspaceChatPane({
chatAgentBusy,
chatInput,
chatReferences,
chatProjectAssets,
composerRef,
chatInputRef,
confirmProjectCreateInNonEmptyFolder,
@@ -945,7 +947,7 @@ export function ProjectWorkspaceChatPane({
ref={composerRef}
inputRef={chatInputRef}
ariaLabel="创作想法"
assets={visibleMainProjectAssets}
assets={chatProjectAssets}
projectPath={projectPath}
disabled={chatAgentBusy || projectSupervisorNeedsUserInput}
multiline={false}
@@ -206,7 +206,6 @@ function ResourceReferenceEditor({
width: number;
} | null>(null);
const rootRef = useRef<HTMLDivElement>(null);
const previewScopeIdRef = useRef(createProjectResourcePreviewScopeId());
const assetReferences = useMemo(
() =>
@@ -297,13 +296,6 @@ function ResourceReferenceEditor({
lastEmittedDraftRef.current = nextDraft;
}, [editor, references, value]);
useEffect(
() => () => {
cancelLocalProjectResourcePreviewScope(previewScopeIdRef.current);
},
[],
);
useEffect(
() =>
editor.registerCommand(
@@ -578,7 +570,6 @@ function ResourceReferenceEditor({
(asset) => asset.id === reference.resourceId,
)}
projectPath={projectPath}
scopeId={previewScopeIdRef.current}
/>
<span>
<strong>{reference.label}</strong>
@@ -637,17 +628,24 @@ function ResourceReferenceEditor({
function ResourcePickerThumbnail({
asset,
projectPath,
scopeId,
}: {
asset?: GameCreationAppAssetManifestEntry;
projectPath: string;
scopeId: string;
}) {
const previewScopeIdRef = useRef(createProjectResourcePreviewScopeId());
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [failed, setFailed] = useState(false);
const mediaType = asset?.mediaType.toLowerCase() ?? '';
const kind = asset?.kind.toLowerCase() ?? '';
useEffect(
() => () => {
cancelLocalProjectResourcePreviewScope(previewScopeIdRef.current);
previewScopeIdRef.current = createProjectResourcePreviewScopeId();
},
[],
);
useEffect(() => {
setPreviewUrl(null);
setFailed(false);
@@ -660,7 +658,7 @@ function ResourcePickerThumbnail({
void invoke<{ dataUrl: string }>('read_local_project_image_preview', {
projectPath,
relativePath: asset.localPath,
scopeId,
scopeId: previewScopeIdRef.current,
requestId: createProjectResourcePreviewRequestId(),
})
.then((result) => {
@@ -672,7 +670,7 @@ function ResourcePickerThumbnail({
return () => {
cancelled = true;
};
}, [asset, mediaType, projectPath, scopeId]);
}, [asset, mediaType, projectPath]);
if (previewUrl) {
return <img src={previewUrl} alt="" />;
@@ -45,6 +45,7 @@ type SupervisorChatOnlyViewProps = {
chatAgentBusy: boolean;
chatInput: string;
chatReferences: ChatReference[];
chatProjectAssets: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[];
composerRef?: Ref<ResourceReferenceInputHandle>;
directCodex?: boolean;
messagesRef: RefObject<HTMLDivElement | null>;
@@ -73,13 +74,13 @@ type SupervisorChatOnlyViewProps = {
visibleMessages: ChatMessage[];
workspaceStatus: string;
expectedRunId?: string | null;
visibleMainProjectAssets?: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[];
};
export function SupervisorChatOnlyView({
chatAgentBusy,
chatInput,
chatReferences,
chatProjectAssets,
composerRef,
directCodex = false,
messagesRef,
@@ -107,7 +108,6 @@ export function SupervisorChatOnlyView({
needsUserInput,
visibleMessages,
workspaceStatus,
visibleMainProjectAssets = [],
}: SupervisorChatOnlyViewProps) {
const shouldFollowLatestRef = useRef(true);
const running = Boolean(
@@ -312,7 +312,7 @@ export function SupervisorChatOnlyView({
<ResourceReferenceInput
ref={composerRef}
ariaLabel={directCodex ? '陶泥儿对话内容' : '项目总控对话内容'}
assets={visibleMainProjectAssets}
assets={chatProjectAssets}
projectPath={projectPath}
disabled={chatAgentBusy || needsUserInput}
rows={3}
+3 -3
View File
@@ -8780,12 +8780,12 @@ iframe.preview-frame {
.conversation-model-select {
position: absolute;
bottom: 10px;
right: 48px;
right: 86px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
max-width: calc(100% - 64px);
max-width: calc(100% - 120px);
pointer-events: auto;
}
.home-input-model-select {
@@ -9096,7 +9096,7 @@ iframe.preview-frame {
.project-supervisor-composer
.resource-reference-input-actions {
position: absolute;
right: 10px;
right: 48px;
bottom: 10px;
}
@@ -1,6 +1,7 @@
// @vitest-environment jsdom
import { cleanup, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { StrictMode } from 'react';
import { afterEach, describe, expect, test, vi } from 'vitest';
import type { GameCreationAppAssetManifestEntry } from '../../../packages/shared/src/contracts/gameCreationApp';
@@ -78,6 +79,49 @@ describe('ResourceReferenceInput', () => {
).not.toBeNull();
});
test('loads image thumbnails through the controlled native preview command', async () => {
const user = userEvent.setup();
const invoke = vi.fn().mockResolvedValue({
dataUrl: 'data:image/png;base64,iVBORw0KGgo=',
});
(
window as unknown as {
__TAURI__?: { core?: { invoke?: typeof invoke } };
}
).__TAURI__ = { core: { invoke } };
render(
<StrictMode>
<ResourceReferenceInput
value=""
references={[]}
onChange={vi.fn()}
assets={assets}
projectPath="C:/project"
ariaLabel="聊天"
/>
</StrictMode>,
);
await user.click(screen.getByRole('button', { name: '插入素材引用' }));
await waitFor(() => {
expect(
document.querySelector('.resource-reference-picker-list img'),
).not.toBeNull();
});
expect(invoke).toHaveBeenCalledWith(
'read_local_project_image_preview',
expect.objectContaining({
projectPath: 'C:/project',
relativePath: 'assets/hero.png',
}),
);
delete (
window as unknown as {
__TAURI__?: { core?: { invoke?: typeof invoke } };
}
).__TAURI__;
});
test('filters candidate references by name, id, kind, and media category', () => {
const hero = resourceReferenceFromAsset(assets[0]!, 'asset-picker');
const theme = resourceReferenceFromAsset(assets[2]!, 'asset-picker');