diff --git a/.planning/phases/07-phase-06-mention-upload/07-02-PLAN.md b/.planning/phases/07-phase-06-mention-upload/07-02-PLAN.md
index 4ff5e98f..495ab134 100644
--- a/.planning/phases/07-phase-06-mention-upload/07-02-PLAN.md
+++ b/.planning/phases/07-phase-06-mention-upload/07-02-PLAN.md
@@ -28,10 +28,10 @@ must_haves:
artifacts:
- path: "frontend/src/components/workspace/artifacts/artifact-file-list.tsx"
provides: "ContextMenu 引用动作改为显式点击触发"
- contains: "ContextMenuItem reference handler"
+ contains: "onClick={() => {"
- path: "frontend/src/core/threads/hooks.ts"
provides: "skill_id 拼接入 submitText"
- contains: "selected skills mapping"
+ contains: "skill.skill_id"
- path: "frontend/src/core/messages/utils.ts"
provides: "XClaw 前缀剥离"
contains: "stripPriorityHintSuffix"
@@ -56,6 +56,10 @@ Output: 修复提交链路与右键引用交互,并补齐回归测试。
- 右键打开菜单时不会自动触发引用。
- 菜单项点击后才触发引用并回填输入区。
+
+ rg -n "ContextMenuItem|onSelect|onClick|dispatchMentionReference" frontend/src/components/workspace/artifacts/artifact-file-list.tsx frontend/src/components/workspace/messages/message-list-item.tsx
+
+ ContextMenu 引用行为仅由显式用户点击触发,右键打开菜单不再自动引用。
@@ -69,6 +73,10 @@ Output: 修复提交链路与右键引用交互,并补齐回归测试。
- 消息区仍不显示该后缀。
- 单测全部通过。
+
+ rg -n "XClaw优先使用|stripPriorityHintSuffix|composeSubmitText" frontend/src/core/threads/priority-hint.ts frontend/src/core/messages/utils.ts frontend/src/core/threads/hooks.ts
+
+ 前缀与剥离规则统一为 XClaw 版本,提交态与展示态语义保持一致。
@@ -81,12 +89,16 @@ Output: 修复提交链路与右键引用交互,并补齐回归测试。
- 拼接中 Skill 部分使用 id 列表。
- 发送按钮与回车路径行为一致。
+
+ rg -n "selectedSkills|skill_id|composeSubmitText" frontend/src/core/threads/hooks.ts
+ cd frontend && pnpm -s test:e2e --grep "DF-INPUT-008A|reference|context menu"
+
+ 提交提示中的 Skill 标识稳定使用 skill_id,且主要发送入口回归通过。
-- `cd frontend && node --test src/core/threads/hooks.test.ts`
- `cd frontend && pnpm -s typecheck`
- `cd frontend && pnpm -s test:e2e --grep "DF-INPUT-008A|reference|context menu"`
diff --git a/.planning/phases/07-phase-06-mention-upload/07-02-SUMMARY.md b/.planning/phases/07-phase-06-mention-upload/07-02-SUMMARY.md
new file mode 100644
index 00000000..e35def37
--- /dev/null
+++ b/.planning/phases/07-phase-06-mention-upload/07-02-SUMMARY.md
@@ -0,0 +1,59 @@
+---
+phase: 07-phase-06-mention-upload
+plan: 02
+subsystem: gap-closure
+tags: [context-menu, priority-hint, skill-id, references, e2e]
+requires:
+ - phase: 07-phase-06-mention-upload
+ provides: 07-01-SUMMARY.md
+provides:
+ - 修复右键打开 ContextMenu 时误触发“引用”的问题
+ - 优先提示前缀统一为“XClaw优先使用”并与展示层剥离规则对齐
+ - 提交态 Skill 拼接使用 skill_id,避免使用展示名 title
+affects: [frontend-chat-input, message-render, thread-submit-payload]
+tech-stack:
+ added: []
+ patterns:
+ - explicit-click-only context-menu reference action
+ - submit/display separation with stable id-based hint composition
+key-files:
+ created:
+ - .planning/phases/07-phase-06-mention-upload/07-02-SUMMARY.md
+ modified:
+ - frontend/src/components/workspace/artifacts/artifact-file-list.tsx
+ - frontend/src/components/workspace/messages/message-list-item.tsx
+ - frontend/src/core/threads/priority-hint.ts
+ - frontend/src/core/messages/utils.ts
+ - frontend/src/core/threads/hooks.ts
+ - frontend/src/core/threads/hooks.test.ts
+ - frontend/tests/e2e/input-and-compose.spec.ts
+key-decisions:
+ - "ContextMenu 引用动作仅绑定显式点击,移除 onSelect 触发路径。"
+ - "优先提示统一改为 XClaw 前缀,并同步更新消息展示剥离规则。"
+ - "Skill 拼接数据源统一使用 selectedSkills.skill_id。"
+requirements-completed: [P7-01, P7-02, P7-03, P7-04]
+duration: 35 min
+completed: 2026-04-17
+---
+
+# Phase 07 Plan 02 Summary
+
+完成了 Phase 07 的 3 个 UAT gap closure:引用误触发、提示前缀唯一化、Skill 提示标识稳定化。
+
+## Implemented
+
+- 将 artifact 列表与消息附件中的 `ContextMenuItem` 引用动作从 `onSelect` 改为 `onClick`,避免仅右键打开菜单就自动引用。
+- `priority-hint` 规则升级为 `XClaw优先使用...`,并保持“附件在前、Skill 在后、大小写不敏感去重”。
+- `stripPriorityHintSuffix` 同步匹配新前缀,确保消息区继续只展示用户原文。
+- `hooks.ts` 在两条发送链路中均改为使用 `selectedSkills.skill_id` 参与提交态拼接。
+- 单测与 E2E 断言同步更新到新前缀。
+
+## Verification
+
+- `node --test frontend/src/core/threads/hooks.test.ts`:7 passed
+- `cd frontend && pnpm -s typecheck`:passed
+- `cd frontend && pnpm -s test:e2e --grep "DF-INPUT-008A|reference|context menu"`:1 passed
+
+## Notes
+
+- 本计划为 `gap_closure: true`,直接对应 `07-UAT.md` 中 3 个已诊断缺口。
diff --git a/frontend/src/components/workspace/artifacts/artifact-file-list.tsx b/frontend/src/components/workspace/artifacts/artifact-file-list.tsx
index 8b49a1de..54ea9b16 100644
--- a/frontend/src/components/workspace/artifacts/artifact-file-list.tsx
+++ b/frontend/src/components/workspace/artifacts/artifact-file-list.tsx
@@ -152,7 +152,7 @@ export function ArtifactFileList({
{
+ onClick={() => {
dispatchMentionReference({
threadId,
filename: getFileName(file),
diff --git a/frontend/src/components/workspace/messages/message-list-item.tsx b/frontend/src/components/workspace/messages/message-list-item.tsx
index 745a6f59..7108e5b9 100644
--- a/frontend/src/components/workspace/messages/message-list-item.tsx
+++ b/frontend/src/components/workspace/messages/message-list-item.tsx
@@ -424,22 +424,22 @@ function RichFileCard({
/>
-
- {
- if (!file.path) return;
- dispatchMentionReference({
- threadId,
- filename: file.filename,
- path: file.path,
- ref_source: refSource,
- });
- }}
- >
- {t.common.reference}
-
-
+
+ {
+ if (!file.path) return;
+ dispatchMentionReference({
+ threadId,
+ filename: file.filename,
+ path: file.path,
+ ref_source: refSource,
+ });
+ }}
+ >
+ {t.common.reference}
+
+
);
}
diff --git a/frontend/src/core/messages/utils.ts b/frontend/src/core/messages/utils.ts
index 2690a4b2..fe174166 100644
--- a/frontend/src/core/messages/utils.ts
+++ b/frontend/src/core/messages/utils.ts
@@ -354,13 +354,13 @@ export function stripUploadedFilesTag(content: string): string {
/**
* Strip the appended priority-hint suffix from a message content.
* Suffix format:
- * - 优先使用【附件...】
- * - 优先使用【Skill...】
- * - 优先使用【附件...】和【Skill...】
+ * - XClaw优先使用【附件...】
+ * - XClaw优先使用【Skill...】
+ * - XClaw优先使用【附件...】和【Skill...】
*/
export function stripPriorityHintSuffix(content: string): string {
return content
- .replace(/\n?优先使用【[^】]+】(?:和【[^】]+】)?\s*$/u, "")
+ .replace(/\n?XClaw优先使用【[^】]+】(?:和【[^】]+】)?\s*$/u, "")
.trim();
}
diff --git a/frontend/src/core/threads/hooks.test.ts b/frontend/src/core/threads/hooks.test.ts
index 69609d57..adff358f 100644
--- a/frontend/src/core/threads/hooks.test.ts
+++ b/frontend/src/core/threads/hooks.test.ts
@@ -74,34 +74,34 @@ void test("buildFilesForSubmit keeps artifact mention path without re-upload", (
void test("buildPriorityHintText keeps attachments first and skills second", () => {
const result = buildPriorityHintText({
attachmentNames: ["spec.md"],
- skillTitles: ["文档生成"],
+ skillIds: ["skill.docs.generate"],
});
- assert.equal(result, "优先使用【spec.md】和【文档生成】");
+ assert.equal(result, "XClaw优先使用【spec.md】和【skill.docs.generate】");
});
void test("buildPriorityHintText outputs single category when the other is empty", () => {
assert.equal(
buildPriorityHintText({
attachmentNames: ["spec.md"],
- skillTitles: [],
+ skillIds: [],
}),
- "优先使用【spec.md】",
+ "XClaw优先使用【spec.md】",
);
assert.equal(
buildPriorityHintText({
attachmentNames: [],
- skillTitles: ["文档生成"],
+ skillIds: ["skill.docs.generate"],
}),
- "优先使用【文档生成】",
+ "XClaw优先使用【skill.docs.generate】",
);
});
void test("buildPriorityHintText deduplicates case-insensitively", () => {
const result = buildPriorityHintText({
attachmentNames: ["Spec.md", "spec.md", " SPEC.md "],
- skillTitles: ["Excel", "excel"],
+ skillIds: ["skill.excel", "SKILL.EXCEL"],
});
- assert.equal(result, "优先使用【Spec.md】和【Excel】");
+ assert.equal(result, "XClaw优先使用【Spec.md】和【skill.excel】");
});
void test("composeSubmitText appends hint only when needed", () => {
@@ -109,16 +109,16 @@ void test("composeSubmitText appends hint only when needed", () => {
composeSubmitText({
baseText: "请总结",
attachmentNames: ["spec.md"],
- skillTitles: [],
+ skillIds: [],
}),
- "请总结\n优先使用【spec.md】",
+ "请总结\nXClaw优先使用【spec.md】",
);
assert.equal(
composeSubmitText({
baseText: "请总结",
attachmentNames: [],
- skillTitles: [],
+ skillIds: [],
}),
"请总结",
);
diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts
index 15295c12..f0e81528 100644
--- a/frontend/src/core/threads/hooks.ts
+++ b/frontend/src/core/threads/hooks.ts
@@ -408,8 +408,8 @@ export function useThreadStream({
const referenceNames = (message.references ?? []).map(
(reference) => reference.filename,
);
- const selectedSkillTitles = (message.selectedSkills ?? []).map(
- (skill) => skill.title,
+ const selectedSkillIds = (message.selectedSkills ?? []).map(
+ (skill) => skill.skill_id,
);
const resolvedThreadId =
normalizeThreadId(threadId) ??
@@ -578,7 +578,7 @@ export function useThreadStream({
const submitText = composeSubmitText({
baseText: text,
attachmentNames: [...uploadedNames, ...referenceNames],
- skillTitles: selectedSkillTitles,
+ skillIds: selectedSkillIds,
});
await thread.submit(
@@ -692,8 +692,8 @@ export function useSubmitThread({
const referenceNames = (message.references ?? []).map(
(reference) => reference.filename,
);
- const selectedSkillTitles = (message.selectedSkills ?? []).map(
- (skill) => skill.title,
+ const selectedSkillIds = (message.selectedSkills ?? []).map(
+ (skill) => skill.skill_id,
);
const hasFiles = !!(message.files && message.files.length > 0);
@@ -766,7 +766,7 @@ export function useSubmitThread({
const submitText = composeSubmitText({
baseText: text,
attachmentNames: [...uploadedNames, ...referenceNames],
- skillTitles: selectedSkillTitles,
+ skillIds: selectedSkillIds,
});
await thread.submit(
diff --git a/frontend/src/core/threads/priority-hint.ts b/frontend/src/core/threads/priority-hint.ts
index 267d2a28..5f8bb6f3 100644
--- a/frontend/src/core/threads/priority-hint.ts
+++ b/frontend/src/core/threads/priority-hint.ts
@@ -14,13 +14,13 @@ function uniqueNormalizedValues(values: Array): string[] {
export function buildPriorityHintText({
attachmentNames,
- skillTitles,
+ skillIds,
}: {
attachmentNames: string[];
- skillTitles: string[];
+ skillIds: string[];
}): string {
const attachments = uniqueNormalizedValues(attachmentNames);
- const skills = uniqueNormalizedValues(skillTitles);
+ const skills = uniqueNormalizedValues(skillIds);
if (attachments.length === 0 && skills.length === 0) {
return "";
}
@@ -30,25 +30,25 @@ export function buildPriorityHintText({
const skillPart = skills.length > 0 ? `【${skills.join("、")}】` : "";
if (attachmentPart && skillPart) {
- return `优先使用${attachmentPart}和${skillPart}`;
+ return `XClaw优先使用${attachmentPart}和${skillPart}`;
}
- return `优先使用${attachmentPart || skillPart}`;
+ return `XClaw优先使用${attachmentPart || skillPart}`;
}
export function composeSubmitText({
baseText,
attachmentNames,
- skillTitles,
+ skillIds,
}: {
baseText: string;
attachmentNames: string[];
- skillTitles: string[];
+ skillIds: string[];
}): string {
const trimmedBase = baseText.trim();
if (!trimmedBase) return trimmedBase;
const priorityHint = buildPriorityHintText({
attachmentNames,
- skillTitles,
+ skillIds,
});
if (!priorityHint) return trimmedBase;
return `${trimmedBase}\n${priorityHint}`;
diff --git a/frontend/tests/e2e/input-and-compose.spec.ts b/frontend/tests/e2e/input-and-compose.spec.ts
index 17465c82..c5e07970 100644
--- a/frontend/tests/e2e/input-and-compose.spec.ts
+++ b/frontend/tests/e2e/input-and-compose.spec.ts
@@ -285,10 +285,10 @@ test.describe("聊天工作台 / 输入区与发送", () => {
const requestBody = request.postData() ?? "";
expect(requestBody).toContain(userInput);
- expect(requestBody).toContain("优先使用【");
+ expect(requestBody).toContain("XClaw优先使用【");
expect(requestBody).toContain(referenceName);
await expect(
- page.locator(".is-user").filter({ hasText: "优先使用【" }),
+ page.locator(".is-user").filter({ hasText: "XClaw优先使用【" }),
).toHaveCount(0);
});