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); });