= useCallback(
- (_anchorElementRef, itemProps) => {
- const inputRect = rootRef.current?.getBoundingClientRect();
- if (!inputRect || itemProps.options.length === 0) {
- return null;
- }
- const viewportPadding = 12;
- const menuWidth = Math.min(
- Math.max(280, inputRect.width),
- Math.min(420, window.innerWidth - viewportPadding * 2),
- );
- const left = Math.min(
- Math.max(viewportPadding, inputRect.left),
- Math.max(
- viewportPadding,
- window.innerWidth - menuWidth - viewportPadding,
- ),
- );
- const availableAbove = Math.max(0, inputRect.top - viewportPadding - 8);
- const availableBelow = Math.max(
- 0,
- window.innerHeight - inputRect.bottom - viewportPadding - 8,
- );
- const openAbove = availableBelow < 160 && availableAbove > availableBelow;
- const maxHeight = Math.max(
- 120,
- Math.min(240, openAbove ? availableAbove : availableBelow),
- );
- const top = openAbove
- ? Math.max(viewportPadding, inputRect.top - maxHeight - 8)
- : inputRect.bottom + 8;
- return createPortal(
-
- {itemProps.options.map((option, index) => (
-
- ))}
-
,
- document.body,
- );
- },
- [rootRef],
- );
+ const renderMentionMenu: MenuRenderFn<
+ ResourceMentionOption | SkillMentionOption
+ > = useCallback((_anchorElementRef, itemProps) => {
+ const inputRect = rootRef.current?.getBoundingClientRect();
+ if (!inputRect || itemProps.options.length === 0) {
+ return null;
+ }
+ const viewportPadding = 12;
+ const menuWidth = Math.min(
+ Math.max(280, inputRect.width),
+ Math.min(420, window.innerWidth - viewportPadding * 2),
+ );
+ const left = Math.min(
+ Math.max(viewportPadding, inputRect.left),
+ Math.max(
+ viewportPadding,
+ window.innerWidth - menuWidth - viewportPadding,
+ ),
+ );
+ const availableAbove = Math.max(0, inputRect.top - viewportPadding - 8);
+ const availableBelow = Math.max(
+ 0,
+ window.innerHeight - inputRect.bottom - viewportPadding - 8,
+ );
+ const openAbove = availableBelow < 160 && availableAbove > availableBelow;
+ const maxHeight = Math.max(
+ 120,
+ Math.min(240, openAbove ? availableAbove : availableBelow),
+ );
+ const top = openAbove
+ ? Math.max(viewportPadding, inputRect.top - maxHeight - 8)
+ : inputRect.bottom + 8;
+ return createPortal(
+
+ {itemProps.options.map((option, index) => (
+
+ ))}
+
,
+ document.body,
+ );
+ }, [rootRef]);
const pickerReferences = useMemo(() => {
return scopeReferences.filter((reference) => {
@@ -1119,7 +1197,22 @@ function ResourceReferenceEditor({
onSelectOption={(option, textNode, closeMenu) =>
handleSelectMention(option, textNode, closeMenu)
}
- menuRenderFn={renderMentionMenu}
+ menuRenderFn={
+ renderMentionMenu as unknown as MenuRenderFn
+ }
+ anchorClassName="resource-reference-menu-anchor"
+ preselectFirstItem
+ />
+
+ options={skillOptions}
+ triggerFn={skillTriggerFn}
+ onQueryChange={setSkillQuery}
+ onSelectOption={(option, textNode, closeMenu) =>
+ handleSelectSkill(option, textNode, closeMenu)
+ }
+ menuRenderFn={
+ renderMentionMenu as unknown as MenuRenderFn
+ }
anchorClassName="resource-reference-menu-anchor"
preselectFirstItem
/>
diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/generated/DirectCodexUserContentPart.ts b/apps/ai-game-creator-shell/src/features/project-workspace/generated/DirectCodexUserContentPart.ts
index da509125d..e6551d847 100644
--- a/apps/ai-game-creator-shell/src/features/project-workspace/generated/DirectCodexUserContentPart.ts
+++ b/apps/ai-game-creator-shell/src/features/project-workspace/generated/DirectCodexUserContentPart.ts
@@ -5,6 +5,7 @@ import type { DirectCodexUserRuntimeRegionPart } from './DirectCodexUserRuntimeR
export type DirectCodexUserContentPart =
| { type: 'input_text'; text: string }
| { type: 'agc_resource_reference'; resourceId: string }
+ | { type: 'agc_skill_reference'; name: string }
| ({
type: 'agc_runtime_region_reference';
} & DirectCodexUserRuntimeRegionPart)
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 f6a8f6712..1b19c5c73 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
@@ -46,7 +46,16 @@ export type RuntimeRegionReference = {
source: 'runtime-picker';
};
-export type ChatReference = ResourceReference | RuntimeRegionReference;
+export type SkillReference = {
+ type: 'skill';
+ name: string;
+ description?: string;
+};
+
+export type ChatReference =
+ | ResourceReference
+ | RuntimeRegionReference
+ | SkillReference;
export type ChatComposerDraft = {
/** Lexical 顺序对应的 canonical user content;这是唯一草稿真相。 */
@@ -102,6 +111,9 @@ export function directCodexContentToPromptText(
if (part.type === 'agc_resource_reference') {
return `@${labels.get(part.resourceId) ?? part.resourceId}`;
}
+ if (part.type === 'agc_skill_reference') {
+ return `$${part.name}`;
+ }
if (part.type === 'agc_runtime_region_reference') {
return `@${part.label}`;
}
@@ -125,6 +137,9 @@ export function chatReferenceToContentPart(
if (reference.type === 'resource') {
return { type: 'agc_resource_reference', resourceId: reference.resourceId };
}
+ if (reference.type === 'skill') {
+ return { type: 'agc_skill_reference', name: reference.name };
+ }
return {
type: 'agc_runtime_region_reference',
label: reference.label,
@@ -371,6 +386,9 @@ function chatReferenceKey(reference: ChatReference) {
if (reference.type === 'resource') {
return `resource:${reference.resourceId}:${reference.source}`;
}
+ if (reference.type === 'skill') {
+ return `skill:${reference.name}`;
+ }
return `runtime-region:${runtimeRegionReferenceDiscriminators(reference)}`;
}
@@ -383,7 +401,9 @@ export function chatReferenceListKey(references: ChatReference[]) {
.map((reference) =>
reference.type === 'resource'
? `resource:${reference.resourceId}:${reference.source}:${reference.label}`
- : `runtime-region:${runtimeRegionReferenceDiscriminators(reference)}`,
+ : reference.type === 'skill'
+ ? `skill:${reference.name}`
+ : `runtime-region:${runtimeRegionReferenceDiscriminators(reference)}`,
)
.join('\u0001');
}