diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index b5ac0dc9f..e71af0adf 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -87,15 +87,10 @@ import { resolveChatProjectPath, } from './features/project-workspace/projectCommandPolicy'; import type { ResourceReferenceInputHandle } from './features/project-workspace/ResourceReferenceInput'; -import type { - ChatComposerDraft, - ChatReference, -} from './features/project-workspace/resourceReferences'; import { - directCodexContentToPromptText, + directCodexContentToLegacyContentDto, hasMeaningfulDirectCodexContent, RESOURCE_REFERENCE_INSERT_EVENT, - resourceReferenceFromAsset, type ResourceReferenceInsertEventDetail, } from './features/project-workspace/resourceReferences'; import { RuntimeConfigDialog } from './features/runtime-config/RuntimeConfigDialog'; @@ -122,7 +117,6 @@ import { DirectProjectChatView, type DirectProjectInitialTurn, } from './view/project-development/chat/DirectProjectChatView'; -import type { DirectCodexUserContentPart } from './view/project-development/chat/generated/DirectCodexUserContentPart'; import { PlanningChatView } from './view/project-development/planning/PlanningChatView'; import { planningStateNeedsRuntimeRefresh } from './view/project-development/planning/planningLane'; import { @@ -375,15 +369,7 @@ export function App({ ); } - const [chatInput, setChatInput] = useState(''); - const [chatReferences, setChatReferences] = useState([]); - const [chatContent, setChatContent] = useState( - [], - ); const chatComposerRef = useRef(null); - useEffect(() => { - setChatContent([]); - }, [localProject?.projectPath]); const [chatAgentBusy, setChatAgentBusy] = useState(false); const [planningRuntime, setPlanningRuntime] = useState(null); @@ -1883,8 +1869,6 @@ export function App({ setProjectPath(openedProject.projectPath); setWorkspaceProjectKind(projectKind); setLocalProject(openedProject); - setChatReferences([]); - setChatContent([]); setManifest(openedProject.manifest); setAgentRuntimeById({}); setMessages(conversationMessages); @@ -1926,42 +1910,6 @@ export function App({ } } - function handleChatComposerChange(draft: ChatComposerDraft) { - const content = draft.content; - setChatContent(content); - setChatInput(directCodexContentToPromptText(content, manifest.assets)); - setChatReferences( - content.flatMap((part) => { - if (part.type === 'agc_resource_reference') { - const asset = manifest.assets.find( - (item) => item.id === part.resourceId, - ); - return asset - ? [resourceReferenceFromAsset(asset, 'asset-picker')] - : []; - } - if (part.type === 'agc_runtime_region_reference') { - return [ - { - type: 'runtime-region' as const, - label: part.label, - runId: part.runId ?? undefined, - versionId: part.versionId ?? undefined, - elementTag: part.elementTag ?? undefined, - elementRole: part.elementRole ?? undefined, - text: part.text ?? undefined, - width: part.width ?? undefined, - height: part.height ?? undefined, - resourceIds: part.resourceIds, - source: 'runtime-picker' as const, - }, - ] as ChatReference[]; - } - return [] as ChatReference[]; - }), - ); - } - useEffect(() => { const handleResourceReferenceInsert = (event: Event) => { const detail = (event as CustomEvent) @@ -2688,12 +2636,13 @@ export function App({ function handlePlanningChatSubmit(event: FormEvent) { event.preventDefault(); - const prompt = chatInput.trim(); - const content = chatContent; - const canonicalPrompt = directCodexContentToPromptText( + const content = chatComposerRef.current?.getDraft().content ?? []; + const legacyContent = directCodexContentToLegacyContentDto( content, manifest.assets, ); + const prompt = legacyContent.text.trim(); + const canonicalPrompt = legacyContent.text; if (agentRuntimeNeedsUserInput(planningRuntimeRef.current)) { setProjectChatError('请先回答项目总控 Agent 当前的澄清问题'); return; @@ -2710,7 +2659,7 @@ export function App({ } planningChatShouldFollowLatestRef.current = true; const clientTurnId = createAgentChatRunId('planning-v2-turn'); - setChatInput(''); + chatComposerRef.current?.clear(); setMessages((current) => [ ...current, { @@ -2724,9 +2673,7 @@ export function App({ void executeChatAgentReply({ prompt: canonicalPrompt, clientTurnId }); return; } - setChatInput(''); - setChatReferences([]); - setChatContent([]); + chatComposerRef.current?.clear(); setMessages((current) => [ ...current, { @@ -2761,8 +2708,6 @@ export function App({ void; onEditorStateChange?: (editorState: EditorState) => void; initialContent?: DirectCodexUserContentPart[]; @@ -241,20 +238,6 @@ type DraftProjection = { content: DirectCodexUserContentPart[]; }; -/** 引用在草稿文本里的稳定键:resource 用资源 id,skill 用技能名,其余用显示名。 */ -function referenceDraftKey(reference: ChatReference) { - if (reference.type === 'resource') return reference.resourceId; - if (reference.type === 'skill') return reference.name; - return reference.label; -} - -/** 引用回读成草稿文本时的 token:skill 是 `$name`,resource 与 runtime region 是 `@显示名`。 */ -function referenceDraftToken(reference: ChatReference) { - return reference.type === 'skill' - ? `$${reference.name}` - : `@${reference.label}`; -} - /** 仅供编辑器内部派生引用(重建文本草稿时用);对外只暴露 canonical content。 */ function readDraftProjectionFromNodes(): DraftProjection { const references: ChatReference[] = []; @@ -269,31 +252,7 @@ function readDraftProjectionFromNodes(): DraftProjection { /** 按编辑器自己的口径读 canonical 草稿;只能在 Lexical 读/更新上下文里调用。 */ function readDraftFromNodes(): ChatComposerDraft { const projection = readDraftProjectionFromNodes(); - const labels = new Map( - projection.references.map((reference) => [ - referenceDraftKey(reference), - referenceDraftToken(reference), - ]), - ); - const text = projection.content - // 与 directCodexContentToPromptText 保持同一口径:附件引用是 `@名称`,不是 `$名称`。 - .map((part) => { - if (part.type === 'input_text') return part.text; - if (part.type === 'agc_resource_reference') { - return labels.get(part.resourceId) ?? `@${part.resourceId}`; - } - if (part.type === 'agc_runtime_region_reference') return `@${part.label}`; - if (part.type === 'agc_attachment_reference') return `@${part.name}`; - if (part.type === 'agc_skill_reference') return `$${part.name}`; - return ''; - }) - .join('') - .trim(); - return { - text, - references: projection.references, - content: projection.content, - }; + return { content: projection.content }; } // The pure projection is exported for submit-time reads and focused tests. @@ -302,7 +261,7 @@ export function readResourceReferenceDraft( editorState: EditorState | null, ): ChatComposerDraft { if (!editorState) { - return { text: '', references: [], content: [] }; + return { content: [] }; } return editorState.read(readDraftFromNodes); } @@ -399,20 +358,39 @@ function buildDraftSegments( return lines; } -function applyDraftToRoot(value: string, references: ChatReference[]) { - const root = $getRoot(); - root.clear(); - buildDraftSegments(value, references).forEach((segments) => { - const paragraph = $createParagraphNode(); - segments.forEach((segment) => { - if (segment.kind === 'text') { - if (segment.text) paragraph.append($createTextNode(segment.text)); - return; +/** + * 润色回写仍按文本中的 token 精确恢复资源/Skill/runtime chip;附件没有可安全反解析的 + * 稳定 token,因此保留原 attachment part 并放在新文本之后。 + * TODO: 提出“润色精确恢复附件原位置”的产品能力后,再把附件位置纳入协议,不猜字符串。 + */ +function applyPolishedTextToRoot( + value: string, + current: DraftProjection, + assetsById: ReadonlyMap, +) { + const lines = value.split(/\r?\n/u); + const rebuiltContent: DirectCodexUserContentPart[] = []; + buildDraftSegments(value, current.references).forEach( + (segments, lineIndex) => { + segments.forEach((segment) => { + if (segment.kind === 'text') { + if (segment.text) + rebuiltContent.push({ type: 'input_text', text: segment.text }); + return; + } + rebuiltContent.push(chatReferenceToContentPart(segment.reference)); + }); + if (lineIndex < lines.length - 1) { + rebuiltContent.push({ type: 'input_text', text: '\n' }); } - paragraph.append($createResourceReferenceNode(segment.reference)); - }); - root.append(paragraph); - }); + }, + ); + rebuiltContent.push( + ...current.content.filter( + (part) => part.type === 'agc_attachment_reference', + ), + ); + applyContentToRoot(rebuiltContent, assetsById); } function referenceFromContentPart( @@ -567,8 +545,6 @@ function $staleResourceReferenceNodes( } function ResourceReferenceEditor({ - value, - references, onChange, onEditorStateChange, initialContent, @@ -833,7 +809,7 @@ function ResourceReferenceEditor({ replaceText: (text: string) => { editor.update(() => { const current = readDraftProjectionFromNodes(); - applyDraftToRoot(text, current.references); + applyPolishedTextToRoot(text, current, assetsById); $getRoot().selectEnd(); }); }, @@ -873,39 +849,6 @@ function ResourceReferenceEditor({ }); }, [assetsById, editor, initialContent]); - // 受控聊天入口在清空/恢复草稿时同步编辑器;普通输入变更若已与当前内容一致则不重建, - // 避免每个按键都把光标跳到末尾。 - // - // 这里必须是 layout effect:`value` / `references` 只是宿主对编辑器草稿的回显,宿主状态 - // 更新会晚于编辑器的 Lexical 提交。用被动 effect 时,一次滞后渲染的回调可能在用户已经 - // 继续输入之后才执行,它读到的是旧 `value` 加新编辑器状态,就会把整份草稿重建掉 - // (编辑器再通过 onChange 把空草稿写回宿主,来回清空)。layout effect 与本次提交同帧执行, - // 读到的 props 与编辑器状态属于同一次提交。 - useLayoutEffect(() => { - if (value === undefined && references === undefined) return; - const desiredValue = value ?? ''; - const desiredRefs = references ?? []; - const current = readResourceReferenceDraft(editor.getEditorState()); - const currentRefKey = current.references - ?.map( - (reference) => - `${reference.type}:${reference.type === 'resource' ? reference.resourceId : reference.type === 'skill' ? reference.name : reference.label}`, - ) - .join('|'); - const desiredRefKey = desiredRefs - .map( - (reference) => - `${reference.type}:${reference.type === 'resource' ? reference.resourceId : reference.type === 'skill' ? reference.name : reference.label}`, - ) - .join('|'); - if (current.text === desiredValue && currentRefKey === desiredRefKey) - return; - editor.update(() => { - applyDraftToRoot(desiredValue, desiredRefs); - $getRoot().selectEnd(); - }); - }, [editor, references, value]); - // 资源改名后刷新已有引用 chip 的显示名:改写节点会触发 OnChangePlugin, // 把带新显示名的草稿同步回父级,chip 与候选列表都不会残留旧名。 // Lexical 的更新可能排到微任务里提交,这里额外挂一次更新监听兜底。 @@ -1026,8 +969,6 @@ function ResourceReferenceEditor({ const acknowledgedDraftKeyRef = useRef(null); // 拦截表单提交需要读到最新草稿,用 ref 保存本次渲染的草稿与派生值,避免闭包读到旧值。 const liveDraftRef = useRef({ - text: directCodexContentToPromptText(initialContent ?? [], assets), - references: [], content: initialContent ?? [], }); const [draftText, setDraftText] = useState(() => @@ -1043,11 +984,11 @@ function ResourceReferenceEditor({ (text: string) => { editor.update(() => { const current = readDraftProjectionFromNodes(); - applyDraftToRoot(text, current.references); + applyPolishedTextToRoot(text, current, assetsById); $getRoot().selectEnd(); }); }, - [editor], + [assetsById, editor], ); const readPromptText = useCallback( diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts b/apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts index fbd2c19be..dd0205f5b 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts +++ b/apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts @@ -10,6 +10,7 @@ import { buildGameCreationAppAssetTagLibrary, type GameCreationAppAssetTagLibraryEntry, } from '../../../../../packages/shared/src/contracts/gameCreationAppAssetTagLibrary'; +import type { DirectCodexUserAttachmentReferencePart } from '../../view/project-development/chat/generated/DirectCodexUserAttachmentReferencePart'; import type { DirectCodexUserContentPart } from '../../view/project-development/chat/generated/DirectCodexUserContentPart'; import type { DirectCodexUserItem } from '../../view/project-development/chat/generated/DirectCodexUserItem'; @@ -56,10 +57,15 @@ export type ChatReference = | SkillReference; export type ChatComposerDraft = { + /** Lexical 节点按顺序投影出的 canonical user content,唯一事实源。 */ + content: DirectCodexUserContentPart[]; +}; + +/** 仅供仍未迁移的旧调用方消费的单向投影,不得回传给 ResourceReferenceInput。 */ +export type DirectCodexLegacyContentDto = { text: string; references: ChatReference[]; - /** Lexical 顺序对应的 canonical user content,是草稿的唯一结构化来源。 */ - content: DirectCodexUserContentPart[]; + attachments: DirectCodexUserAttachmentReferencePart[]; }; export const RESOURCE_REFERENCE_INSERT_EVENT = 'agc-resource-reference-insert'; @@ -95,8 +101,6 @@ export function isResourceReferenceOverlayTarget(target: EventTarget | null) { } export const EMPTY_CHAT_COMPOSER_DRAFT: ChatComposerDraft = { - text: '', - references: [], content: [], }; @@ -177,10 +181,49 @@ export function directCodexUserItemFromContent( content: readonly DirectCodexUserContentPart[], id: string, ): DirectCodexUserItem { - return chatComposerDraftToDirectCodexUserItem( - { text: '', references: [], content: [...content] }, - id, - ); + return chatComposerDraftToDirectCodexUserItem({ content: [...content] }, id); +} + +/** canonical content → legacy text + reference + attachment DTO。只允许单向翻译。 */ +export function directCodexContentToLegacyContentDto( + content: readonly DirectCodexUserContentPart[], + assets: readonly GameCreationAppAssetManifestEntry[], +): DirectCodexLegacyContentDto { + const references: ChatReference[] = []; + content.forEach((part) => { + if (part.type === 'agc_resource_reference') { + const asset = assets.find((item) => item.id === part.resourceId); + if (asset) + references.push(resourceReferenceFromAsset(asset, 'asset-picker')); + return; + } + if (part.type === 'agc_runtime_region_reference') { + references.push({ + type: 'runtime-region', + label: part.label, + runId: part.runId ?? undefined, + versionId: part.versionId ?? undefined, + elementTag: part.elementTag ?? undefined, + elementRole: part.elementRole ?? undefined, + text: part.text ?? undefined, + width: part.width ?? undefined, + height: part.height ?? undefined, + resourceIds: part.resourceIds, + source: 'runtime-picker', + }); + return; + } + if (part.type === 'agc_skill_reference') { + references.push({ type: 'skill', name: part.name }); + } + }); + return { + text: directCodexContentToPromptText(content, assets), + references, + attachments: content.flatMap((part) => + part.type === 'agc_attachment_reference' ? [part] : [], + ), + }; } export function resourceDisplayName(asset: GameCreationAppAssetManifestEntry) { diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx b/apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx index 4c7dcba9f..53209e796 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx @@ -116,12 +116,9 @@ export function DirectProjectChatView({ attachmentNotice, cancelQueuedTurn, cancelTurn, - chatInput, - chatReferences, composerNotice, directEntries, directTurnRunning, - handleComposerDraft, historyHasMore, loadEarlierHistory, localMessages, @@ -229,14 +226,11 @@ export function DirectProjectChatView({ attachments={attachments} attachmentNotice={attachmentNotice} queuedTurns={queuedTurns} - chatInput={chatInput} - chatReferences={chatReferences} composerNotice={composerNotice} busy={busy} turnCancelling={turnCancelling} onCancelQueuedTurn={cancelQueuedTurn} onCancelTurn={() => void cancelTurn()} - onChatInputChange={handleComposerDraft} onRemoveAttachment={removeAttachment} onSubmit={submit} onUploadFiles={(files) => void uploadFiles(files)} diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx b/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx index d6b7f388f..254c6c1e2 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx @@ -1,5 +1,4 @@ import { ArrowUp, AtSign } from 'lucide-react'; -import type { FormEventHandler } from 'react'; import { useRef, useState } from 'react'; import type { @@ -14,11 +13,9 @@ import { ResourceReferenceInput, type ResourceReferenceInputHandle, } from '../../../../../features/project-workspace/ResourceReferenceInput'; -import type { - ChatComposerDraft, - ChatReference, -} from '../../../../../features/project-workspace/resourceReferences'; import type { DirectCodexTurnAttachment } from '../../conversation/directCodexTurnAttachments'; +import { directCodexAttachmentContentParts } from '../../conversation/directCodexTurnAttachments'; +import type { DirectCodexUserContentPart } from '../../generated/DirectCodexUserContentPart'; import type { QueuedChatTurn } from './chatComposerQueue'; import { ComposerAttachmentMenu, @@ -42,14 +39,11 @@ export function DirectProjectComposer({ attachments, attachmentNotice, queuedTurns, - chatInput, - chatReferences, composerNotice, busy, turnCancelling, onCancelQueuedTurn, onCancelTurn, - onChatInputChange, onRemoveAttachment, onSubmit, onUploadFiles, @@ -60,16 +54,13 @@ export function DirectProjectComposer({ attachments: DirectCodexTurnAttachment[]; attachmentNotice: string; queuedTurns: QueuedChatTurn[]; - chatInput: string; - chatReferences: ChatReference[]; composerNotice: string; busy: boolean; turnCancelling: boolean; onCancelQueuedTurn: (id: string) => void; onCancelTurn: () => void; - onChatInputChange: (draft: ChatComposerDraft) => void; onRemoveAttachment: (index: number) => void; - onSubmit: FormEventHandler; + onSubmit: (content: DirectCodexUserContentPart[]) => boolean; onUploadFiles: (files: readonly File[]) => void; }) { const composerRef = useRef(null); @@ -104,7 +95,15 @@ export function DirectProjectComposer({ const ready = modelSelectRef.current ? await modelSelectRef.current.ensureUsable() : modelReady; - if (ready) onSubmit(event); + if (ready) { + const content = [ + ...(composerRef.current?.getDraft().content ?? []), + ...directCodexAttachmentContentParts(attachments), + ]; + if (onSubmit(content)) { + composerRef.current?.clear(); + } + } } finally { modelValidateInFlightRef.current = false; setModelValidating(false); @@ -129,10 +128,7 @@ export function DirectProjectComposer({ projectPath={projectPath ?? ''} disabled={modelValidating} rows={3} - value={chatInput} - references={chatReferences} placeholder="描述你的想法,或 @ 引用素材" - onChange={onChatInputChange} showTriggerButton={false} />
diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts index 1576be7e5..d368e8bb5 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts @@ -1,4 +1,4 @@ -import { type FormEvent, useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { CONVERSATION_INITIAL_VISIBLE_COUNT, @@ -9,12 +9,9 @@ import type { ChatMessage, DirectTurnCancelView } from '../../../../app/types'; import { projectRuntimeVisibleError } from '../../../../features/agent-runtime'; import { uploadLocalFilesAsAttachments } from '../../../../features/app-shell/useHomeProjectCreation'; import { - type ChatComposerDraft, - type ChatReference, directCodexContentToPromptText, directCodexUserItemFromContent, hasMeaningfulDirectCodexContent, - resourceReferenceFromAsset, } from '../../../../features/project-workspace/resourceReferences'; import { captureAgentRuntimeError } from '../../../../services/errorReporting'; import { requestPlatformSessionRefresh } from '../../../../services/platformSession'; @@ -42,7 +39,6 @@ import { withDirectCodexSessionRefresh, } from '../conversation/directCodexSession'; import { - directCodexAttachmentContentParts, type DirectCodexTurnAttachment, toDirectCodexTurnAttachments, } from '../conversation/directCodexTurnAttachments'; @@ -117,11 +113,6 @@ export function useDirectProjectChatController({ const [statusNotice, setStatusNotice] = useState(''); const [turnCancelling, setTurnCancelling] = useState(false); const [turnBusy, setTurnBusy] = useState(false); - const [chatInput, setChatInput] = useState(''); - const [chatReferences, setChatReferences] = useState([]); - const [chatContent, setChatContent] = useState( - [], - ); const [localMessages, setLocalMessages] = useState([]); // 订阅(subscribe/consume/notify)与聊天 reducer 状态在自己的 hook 里: // controller 只读投影后的条目与回合忙态,不再直接持有线程状态。 @@ -151,7 +142,6 @@ export function useDirectProjectChatController({ setStatusNotice(''); setQueuedTurns([]); queuedTurnsRef.current = []; - setChatContent([]); setLocalMessages([]); setHistoryHasMore(false); historyOldestItemIdRef.current = null; @@ -311,44 +301,7 @@ export function useDirectProjectChatController({ ); } - function handleComposerDraft(draft: ChatComposerDraft) { - const content = draft.content; - setChatContent(content); - setChatInput(directCodexContentToPromptText(content, assets)); - setChatReferences( - content.flatMap((part) => { - if (part.type === 'agc_resource_reference') { - const asset = assets.find((item) => item.id === part.resourceId); - return asset - ? [resourceReferenceFromAsset(asset, 'asset-picker')] - : []; - } - if (part.type === 'agc_runtime_region_reference') { - return [ - { - type: 'runtime-region' as const, - label: part.label, - runId: part.runId ?? undefined, - versionId: part.versionId ?? undefined, - elementTag: part.elementTag ?? undefined, - elementRole: part.elementRole ?? undefined, - text: part.text ?? undefined, - width: part.width ?? undefined, - height: part.height ?? undefined, - resourceIds: part.resourceIds, - source: 'runtime-picker' as const, - }, - ] as ChatReference[]; - } - return [] as ChatReference[]; - }), - ); - } - function clearDraftInput() { - setChatInput(''); - setChatReferences([]); - setChatContent([]); setAttachments([]); setAttachmentNotice(''); } @@ -358,13 +311,8 @@ export function useDirectProjectChatController({ setComposerNotice(''); } - function submit(event: FormEvent) { - event.preventDefault(); - const content = [ - ...chatContent, - ...directCodexAttachmentContentParts(attachments), - ]; - if (!hasMeaningfulDirectCodexContent(content)) return; + function submit(content: DirectCodexUserContentPart[]): boolean { + if (!hasMeaningfulDirectCodexContent(content)) return false; if (turnBusyRef.current || currentTurnRunning) { const clientTurnId = createDirectProjectTurnId(); if ( @@ -378,14 +326,15 @@ export function useDirectProjectChatController({ ) { // 只清草稿:enqueueTurn 刚写入的「已加入发送队列」提示必须留给用户看到。 clearDraftInput(); + return true; } - return; + return false; } - const prompt = chatInput.trim(); + const prompt = directCodexContentToPromptText(content, assets).trim(); if (prompt === '/history') { clearPendingInput(); void reloadHistory(); - return; + return true; } const clientTurnId = createDirectProjectTurnId(); const userItem = directCodexUserItemFromContent( @@ -398,6 +347,7 @@ export function useDirectProjectChatController({ }; clearPendingInput(); startTurn(turn); + return true; } /** @@ -758,13 +708,9 @@ export function useDirectProjectChatController({ attachmentNotice, cancelQueuedTurn, cancelTurn, - chatContent, - chatInput, - chatReferences, composerNotice, directEntries, directTurnRunning: currentTurnRunning, - handleComposerDraft, historyHasMore, loadEarlierHistory, localMessages, diff --git a/apps/ai-game-creator-shell/src/view/project-development/planning/PlanningChatView.tsx b/apps/ai-game-creator-shell/src/view/project-development/planning/PlanningChatView.tsx index d145eaa2d..f87dbe78a 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/planning/PlanningChatView.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/planning/PlanningChatView.tsx @@ -22,10 +22,6 @@ import { ResourceReferenceInput, type ResourceReferenceInputHandle, } from '../../../features/project-workspace/ResourceReferenceInput'; -import type { - ChatComposerDraft, - ChatReference, -} from '../../../features/project-workspace/resourceReferences'; import { formatClockTime } from '../chat/components/ToolCallGroup/toolCallGroupPresentation'; import { DesignAgentPendingActions, @@ -85,8 +81,6 @@ function workspaceStatusForDisplay(workspaceStatus: string) { type PlanningChatViewProps = { activeVersionId?: string | null; - chatInput: string; - chatReferences: ChatReference[]; chatProjectAssets: import('../../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[]; composerRef?: RefObject; controlBusy: boolean; @@ -97,7 +91,6 @@ type PlanningChatViewProps = { messagesRef: RefObject; needsUserInput: boolean; onCancelConfirmation: () => void; - onChatInputChange: (draft: ChatComposerDraft) => void; onConfirmConfirmation: () => void; onScroll: UIEventHandler; onShowEarlierMessages: () => void; @@ -140,8 +133,6 @@ type PlanningChatViewProps = { export function PlanningChatView({ activeVersionId = null, - chatInput, - chatReferences, chatProjectAssets, composerRef, controlBusy, @@ -152,7 +143,6 @@ export function PlanningChatView({ messagesRef, needsUserInput, onCancelConfirmation, - onChatInputChange, onConfirmConfirmation, onScroll, onShowEarlierMessages, @@ -368,12 +358,9 @@ export function PlanningChatView({ projectPath={projectPath} disabled={composerDisabled} rows={3} - value={chatInput} - references={chatReferences} showTriggerButton={false} showPolishAction={false} placeholder="" - onChange={onChatInputChange} />
diff --git a/apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx b/apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx index 602127513..13115941e 100644 --- a/apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx @@ -343,6 +343,48 @@ describe('ResourceReferenceInput', () => { expect(draftText(draft)).toBe('@hero\n把这一版改成夜景'); }); + test('润色回写保留原附件 part,不把 attachment chip 丢出 canonical content', async () => { + const onChange = vi.fn<(draft: ChatComposerDraft) => void>(); + const reference = resourceReferenceFromAsset(assets[0]!, 'asset-picker'); + const attachment = { + type: 'agc_attachment_reference' as const, + name: '参考图.png', + mediaType: 'image/png', + size: 12, + localPath: 'assets/reference.png', + status: 'imported', + }; + const composerRef = createRef(); + render( + <> + + + , + ); + await settleComposer(); + + fireEvent.click(screen.getByRole('button', { name: '模拟润色' })); + await settleComposer(); + const content = onChange.mock.calls.at(-1)?.[0].content; + expect(content).toEqual([ + chatReferenceToContentPart(reference), + { type: 'input_text', text: ' 润色后的需求' }, + attachment, + ]); + }); + test('引用浮层打开时 Enter 不提交表单,关掉后恢复提交', async () => { const onSubmit = vi.fn(); const user = userEvent.setup();