diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/AttachmentReferenceChip.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/AttachmentReferenceChip.tsx
new file mode 100644
index 000000000..e533d6220
--- /dev/null
+++ b/apps/ai-game-creator-shell/src/features/project-workspace/AttachmentReferenceChip.tsx
@@ -0,0 +1,40 @@
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
+import { $getNodeByKey, type NodeKey } from 'lexical';
+import { Paperclip, X } from 'lucide-react';
+
+import type { DirectCodexUserAttachmentReferencePart } from './generated';
+
+export function AttachmentReferenceChip({
+ attachment,
+ nodeKey,
+}: {
+ attachment: DirectCodexUserAttachmentReferencePart;
+ nodeKey: NodeKey;
+}) {
+ const [editor] = useLexicalComposerContext();
+ return (
+
+
+ {attachment.name}
+
+
+ );
+}
diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/AttachmentReferenceNode.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/AttachmentReferenceNode.tsx
new file mode 100644
index 000000000..71b77dc85
--- /dev/null
+++ b/apps/ai-game-creator-shell/src/features/project-workspace/AttachmentReferenceNode.tsx
@@ -0,0 +1,107 @@
+import {
+ $applyNodeReplacement,
+ DecoratorNode,
+ type EditorConfig,
+ type LexicalNode,
+ type NodeKey,
+ type SerializedLexicalNode,
+ type Spread,
+} from 'lexical';
+import type { ReactNode } from 'react';
+
+import { AttachmentReferenceChip } from './AttachmentReferenceChip';
+import type { DirectCodexUserAttachmentReferencePart } from './generated';
+
+export type SerializedAttachmentReferenceNode = Spread<
+ {
+ attachment: DirectCodexUserAttachmentReferencePart;
+ type: 'attachment-reference';
+ version: 1;
+ },
+ SerializedLexicalNode
+>;
+
+/** 附件在纯文本里的唯一占位字符:一个附件 chip 就当一个字符。 */
+const OBJECT_REPLACEMENT = '\uFFFC';
+
+/**
+ * Inline canonical attachment part rendered inside the Lexical composer.
+ * The node stores the complete part so read-back preserves the project path and status.
+ */
+export class AttachmentReferenceNode extends DecoratorNode {
+ __attachment: DirectCodexUserAttachmentReferencePart;
+
+ static getType() {
+ return 'attachment-reference';
+ }
+
+ static clone(node: AttachmentReferenceNode) {
+ return new AttachmentReferenceNode(node.__attachment, node.__key);
+ }
+
+ static importJSON(serializedNode: SerializedAttachmentReferenceNode) {
+ return $createAttachmentReferenceNode(serializedNode.attachment);
+ }
+
+ constructor(
+ attachment: DirectCodexUserAttachmentReferencePart,
+ key?: NodeKey,
+ ) {
+ super(key);
+ this.__attachment = attachment;
+ }
+
+ exportJSON(): SerializedAttachmentReferenceNode {
+ return {
+ ...super.exportJSON(),
+ attachment: this.__attachment,
+ type: 'attachment-reference',
+ version: 1,
+ };
+ }
+
+ createDOM(_config: EditorConfig) {
+ return document.createElement('span');
+ }
+
+ updateDOM() {
+ return false;
+ }
+
+ getTextContent() {
+ return OBJECT_REPLACEMENT;
+ }
+
+ getTextContentSize() {
+ return OBJECT_REPLACEMENT.length;
+ }
+
+ isInline() {
+ return true;
+ }
+
+ isKeyboardSelectable() {
+ return true;
+ }
+
+ decorate() {
+ return (
+
+ );
+ }
+}
+
+export function $createAttachmentReferenceNode(
+ attachment: DirectCodexUserAttachmentReferencePart,
+) {
+ return $applyNodeReplacement(new AttachmentReferenceNode(attachment));
+}
+
+export function $isAttachmentReferenceNode(
+ node: LexicalNode | null | undefined,
+): node is AttachmentReferenceNode {
+ return node instanceof AttachmentReferenceNode;
+}
diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx
index 033931eb8..7a8759cb9 100644
--- a/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx
+++ b/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx
@@ -61,6 +61,11 @@ import {
createProjectResourcePreviewRequestId,
createProjectResourcePreviewScopeId,
} from '../../services/projectResourcePreviewTransport';
+import {
+ $createAttachmentReferenceNode,
+ $isAttachmentReferenceNode,
+ AttachmentReferenceNode,
+} from './AttachmentReferenceNode';
import {
chatPromptDraftKey,
readChatPromptPolishReminderDisabled,
@@ -199,6 +204,10 @@ function collectDraftParts(
content.push({ type: 'input_text', text: '\n' });
return;
}
+ if ($isAttachmentReferenceNode(node)) {
+ content.push({ type: 'agc_attachment_reference', ...node.__attachment });
+ return;
+ }
if ($isResourceReferenceNode(node)) {
references.push(node.__reference);
content.push(chatReferenceToContentPart(node.__reference));
@@ -422,6 +431,10 @@ function applyContentToRoot(
});
return;
}
+ if (part.type === 'agc_attachment_reference') {
+ paragraph.append($createAttachmentReferenceNode(part));
+ return;
+ }
const reference = referenceFromContentPart(part, assetsById);
if (reference) {
paragraph.append($createResourceReferenceNode(reference));
@@ -1442,7 +1455,7 @@ export const ResourceReferenceInput = forwardRef<
return (