diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index cf9d63d4..872d7ab0 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -79,5 +79,5 @@ Plans: - [x] 07-02-PLAN.md — gap closure:修复 ContextMenu 自动引用、提示前缀唯一化、Skill 使用 id 拼接 --- -*Milestone status:* `in_progress` -*Next command:* `/gsd-execute-phase 6` +*Milestone status:* `complete` +*Next command:* `/gsd-new-milestone` diff --git a/.planning/STATE.md b/.planning/STATE.md index 746f139d..51750b06 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,7 +2,7 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone -status: v1.0 milestone in progress +status: v1.0 milestone complete last_updated: "2026-04-17T06:09:01.300Z" last_activity: 2026-04-17 progress: @@ -20,13 +20,13 @@ progress: See: .planning/PROJECT.md (updated 2026-04-07) **Core value:** Keep the frontend visually familiar while preserving and hardening new-system behavior end to end. -**Current focus:** Milestone v1.0 in progress +**Current focus:** Milestone v1.0 completed ## Workflow State -- Current workflow: milestone execution (v1.0) -- Next workflow: execute-phase -- Next command: /gsd-execute-phase 6 +- Current workflow: milestone complete (v1.0) +- Next workflow: new-milestone +- Next command: /gsd-new-milestone ## Artifacts @@ -53,6 +53,5 @@ See: .planning/PROJECT.md (updated 2026-04-07) |---|-------------|------|--------|-----------| | 260415-owq | 归档当前git diff为Phase 06验收后补丁:检查改动、更新06-UAT/06-VERIFICATION/06-SUMMARY(必要时)与STATE,再做原子提交 | 2026-04-15 | atomic | [260415-owq-git-diff-phase-06-06-uat-06-verification](./quick/260415-owq-git-diff-phase-06-06-uat-06-verification/) | | 260416-koe | 归档 Phase 06 明确指代(“这张图”)语义修复到 GSD 流程(已验收,通过人工确认,免验证) | 2026-04-16 | pending | [260416-koe-phase-06](./quick/260416-koe-phase-06/) | -| 260417-kcb | suggestion hover dropdown + child tooltip + bootstrap ids/sessionStorage | 2026-04-17 | pending | [260417-kcb-suggestion-hover-dropdown-child-tooltip-](./quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/) | -Last activity: 2026-04-17 - Completed quick task 260417-kcb: suggestion hover dropdown + child tooltip + bootstrap ids/sessionStorage +Last activity: 2026-04-17 diff --git a/.planning/milestones/v1.0-ROADMAP.md b/.planning/milestones/v1.0-ROADMAP.md index cf9d63d4..872d7ab0 100644 --- a/.planning/milestones/v1.0-ROADMAP.md +++ b/.planning/milestones/v1.0-ROADMAP.md @@ -79,5 +79,5 @@ Plans: - [x] 07-02-PLAN.md — gap closure:修复 ContextMenu 自动引用、提示前缀唯一化、Skill 使用 id 拼接 --- -*Milestone status:* `in_progress` -*Next command:* `/gsd-execute-phase 6` +*Milestone status:* `complete` +*Next command:* `/gsd-new-milestone` diff --git a/.planning/quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/260417-kcb-CONTEXT.md b/.planning/quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/260417-kcb-CONTEXT.md deleted file mode 100644 index dc107bca..00000000 --- a/.planning/quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/260417-kcb-CONTEXT.md +++ /dev/null @@ -1,40 +0,0 @@ -# Quick Task 260417-kcb: suggestion hover dropdown + child tooltip + bootstrap ids/sessionStorage - Context - -**Gathered:** 2026-04-17 -**Status:** Ready for planning - - -## Task Boundary - -实现 suggestion 多字内容展示: -- 悬浮 suggestion 显示 dropdown(内容来自 children) -- 悬浮 dropdown 菜单项显示 tooltip(内容来自 detail) -- 点击 suggestion:将 children 的 skill id 组合为数组并发送 bootstrap -- 点击 dropdown 菜单项:仅发送当前 id 进行 bootstrap -- 发起 bootstrap 时同步更新 sessionStorage - - - - -## Implementation Decisions - -### Dropdown Trigger -- 使用 suggestion hover 打开 dropdown,同时保留 suggestion click 行为。 - -### Tooltip Source -- tooltip 文案优先使用 children.detail,无 detail 时回退到 skill name。 - -### Bootstrap + Storage -- bootstrap 发起时先乐观写入内存和 sessionStorage;失败后通过既有 removeFailedSkills 回滚。 - -### the agent's Discretion -- 不变更现有 API 入参与消息结构语义,仅扩展可选字段与前端交互。 - - - - -## Specific Ideas - -新增 children.detail 可选属性以支撑菜单项 tooltip。 - - diff --git a/.planning/quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/260417-kcb-PLAN.md b/.planning/quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/260417-kcb-PLAN.md deleted file mode 100644 index 6f1bb2d5..00000000 --- a/.planning/quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/260417-kcb-PLAN.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -mode: quick-full -must_haves: - truths: - - suggestion hover 能看到 children dropdown - - dropdown 菜单项 hover 能看到 detail tooltip - - suggestion click 发送 children 全量 id bootstrap - - dropdown item click 仅发送当前 id bootstrap - - bootstrap 发起时 sessionStorage 立即同步,失败可回滚 - artifacts: - - frontend/src/components/workspace/input-box.tsx - - frontend/src/hooks/use-iframe-skill.ts - - frontend/src/core/i18n/locales/types.ts - - frontend/src/core/iframe-messages.ts - - frontend/src/core/i18n/locales/zh-CN.ts - key_links: [] ---- - -# Plan - -## Task 1: 类型与文案扩展 -- files: `frontend/src/core/i18n/locales/types.ts`, `frontend/src/core/iframe-messages.ts`, `frontend/src/core/i18n/locales/zh-CN.ts` -- action: 为 children 增加 `detail` 可选字段并补充中文建议数据。 -- verify: TypeScript 通过,i18n suggestions 可读取 detail。 -- done: completed - -## Task 2: suggestion hover dropdown + item tooltip -- files: `frontend/src/components/workspace/input-box.tsx` -- action: 引入 hover 打开 dropdown,菜单项展示 children,tooltip 展示 detail。 -- verify: 交互符合需求,主 suggestion 与子项点击行为分离。 -- done: completed - -## Task 3: bootstrap 触发时同步 sessionStorage -- files: `frontend/src/hooks/use-iframe-skill.ts` -- action: 在 bootstrap 请求发起前同步 selectedSkills 到内存与 sessionStorage,失败复用回滚逻辑。 -- verify: `pnpm -s typecheck` 通过。 -- done: completed diff --git a/.planning/quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/260417-kcb-RESEARCH.md b/.planning/quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/260417-kcb-RESEARCH.md deleted file mode 100644 index e906f015..00000000 --- a/.planning/quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/260417-kcb-RESEARCH.md +++ /dev/null @@ -1,12 +0,0 @@ -# Quick Task 260417-kcb - Research - -## Findings - -1. 当前 `SuggestionList` 已具备 children 优先 bootstrap 机制,改造点集中在 UI 展示和事件分流。 -2. 代码库已有 Radix `DropdownMenu` 与 `Tooltip` 封装,直接复用可最小化风险。 -3. `useIframeSkill` 已在成功后同步 sessionStorage;若需“发送 bootstrap 即更新”,可在请求前乐观更新并沿用失败回滚。 - -## Integration Notes - -- 受影响文件:`input-box.tsx`、`use-iframe-skill.ts`、i18n 类型与 zh-CN 文案。 -- 不涉及后端接口变更。 diff --git a/.planning/quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/260417-kcb-SUMMARY.md b/.planning/quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/260417-kcb-SUMMARY.md deleted file mode 100644 index 6aba2b82..00000000 --- a/.planning/quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/260417-kcb-SUMMARY.md +++ /dev/null @@ -1,15 +0,0 @@ -# Quick Task 260417-kcb - Summary - -## Delivered - -- 新增 `detail` 可选属性并在中文 suggestions 中补充 detail 文案。 -- suggestion 悬浮时可见 dropdown,内容来自 children。 -- dropdown 菜单项悬浮时显示 tooltip,内容来自 detail(无 detail 回退 name)。 -- 点击 suggestion:bootstrap 使用 children 的全部 id。 -- 点击 dropdown 菜单项:bootstrap 仅携带该菜单项 id。 -- bootstrap 发起时即更新 sessionStorage 与本地 selectedSkills,失败走回滚。 - -## Validation - -- `cd frontend && pnpm -s typecheck` passed -- `cd frontend && pnpm -s lint` failed(仓库已有历史 lint 问题,非本次引入) diff --git a/.planning/quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/260417-kcb-VERIFICATION.md b/.planning/quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/260417-kcb-VERIFICATION.md deleted file mode 100644 index f4276d37..00000000 --- a/.planning/quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/260417-kcb-VERIFICATION.md +++ /dev/null @@ -1,14 +0,0 @@ -status: passed - -# Verification - -- [x] suggestion hover shows dropdown children -- [x] dropdown item hover shows tooltip(detail) -- [x] suggestion click bootstraps all child ids -- [x] dropdown item click bootstraps only selected id -- [x] bootstrap start updates sessionStorage -- [x] TypeScript check passed - -## Residual Risk - -- 未在本地启动浏览器进行可视化手动回归;建议在真实页面快速 smoke test 一次。 diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx index 1639b188..142a80ee 100644 --- a/frontend/src/components/workspace/input-box.tsx +++ b/frontend/src/components/workspace/input-box.tsx @@ -937,7 +937,6 @@ function SuggestionList({ }) { const { t } = useI18n(); const { textInput } = usePromptInputController(); - const [openDropdownKey, setOpenDropdownKey] = useState(null); const suggestions = t.inputBox.suggestions; const promptSuggestions = suggestions.filter( ( @@ -1003,114 +1002,18 @@ function SuggestionList({ }, [bootstrapAndLockSkills, isBootstrapping, textInput], ); - - const handleSuggestionChildClick = useCallback( - ({ - childSkill, - suggestionTitle, - }: { - childSkill: SelectedSkillPayloadItem; - suggestionTitle: string; - }) => { - if (isBootstrapping) return; - setOpenDropdownKey(null); - const id = String(childSkill.id).trim(); - const name = childSkill.name?.trim() ?? ""; - if (!id || !name) return; - void bootstrapAndLockSkills({ - selectedSkills: [{ id, name, detail: childSkill.detail }], - title: suggestionTitle, - }); - }, - [bootstrapAndLockSkills, isBootstrapping], - ); - return ( {promptSuggestions.map((suggestion) => ( - (() => { - const childSkills = (suggestion.children ?? []) - .map((item) => ({ - id: String(item.id).trim(), - name: item.name?.trim() ?? "", - detail: item.detail?.trim() ?? "", - })) - .filter( - ( - item, - ): item is { - id: string; - name: string; - detail: string; - } => Boolean(item.id) && Boolean(item.name), - ); - - if (childSkills.length === 0) { - return ( - handleSuggestionClick(suggestion)} - /> - ); - } - - const dropdownKey = suggestion.suggestion; - return ( - setOpenDropdownKey(open ? dropdownKey : null)} - > - - handleSuggestionClick(suggestion)} - onMouseEnter={() => setOpenDropdownKey(dropdownKey)} - /> - - setOpenDropdownKey(dropdownKey)} - onMouseLeave={() => setOpenDropdownKey(null)} - > - - {suggestion.suggestion} - - - - {childSkills.map((item) => ( - - { - event.preventDefault(); - handleSuggestionChildClick({ - childSkill: item, - suggestionTitle: suggestion.suggestion, - }); - }} - > - {item.name} - - - ))} - - - - ); - })() + handleSuggestionClick(suggestion)} + /> ))} ); diff --git a/frontend/src/components/workspace/tooltip.tsx b/frontend/src/components/workspace/tooltip.tsx index 0bbb2e5b..ece59ba4 100644 --- a/frontend/src/components/workspace/tooltip.tsx +++ b/frontend/src/components/workspace/tooltip.tsx @@ -1,7 +1,5 @@ "use client"; -import { Children, type ComponentProps, isValidElement } from "react"; - import { Tooltip as TooltipPrimitive, TooltipContent, @@ -11,23 +9,15 @@ import { export function Tooltip({ children, content, - side, ...props }: { children: React.ReactNode; - side?: ComponentProps["side"]; content?: React.ReactNode; }) { - const hasSingleElementChild = - Children.count(children) === 1 && isValidElement(children); - const triggerChild = hasSingleElementChild ? children : {children}; - return ( - {triggerChild} - - {content} - + {children} + {content} ); } diff --git a/frontend/src/core/i18n/locales/types.ts b/frontend/src/core/i18n/locales/types.ts index 3a85dd44..b9e79878 100644 --- a/frontend/src/core/i18n/locales/types.ts +++ b/frontend/src/core/i18n/locales/types.ts @@ -3,7 +3,6 @@ import type { LucideIcon } from "lucide-react"; export interface SelectedSkillPayloadItem { id: string | number; name: string; - detail?: string; } export interface Translations { diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts index 2d3a10e1..f215c790 100644 --- a/frontend/src/core/i18n/locales/zh-CN.ts +++ b/frontend/src/core/i18n/locales/zh-CN.ts @@ -126,66 +126,31 @@ export const zhCN: Translations = { prompt: "为[主题/产品]撰写吸引人的自媒体文案,包括标题、正文和话题标签。", icon: PenLineIcon, - children: [ - { - id: "6057", - name: "生辰解语", - detail: - "四柱八字命理分析。\n当用户询问八字、四柱、命理、算命、Bazi、运势预测、命盘分析,\n或想了解其八字命盘、运势、大运、流年时,请使用此功能。", - }, - ], + children: [{ id: "6057", name: "生辰解语" }], }, { suggestion: "小红书种草", prompt: "编写[项目/功能]的需求文档,包含功能描述、用户故事和验收标准。", icon: CompassIcon, - children: [ - { - id: "6099", - name: "小红书笔记智造官", - detail: - "根据用户需求及提供资料,撰写小红书笔记内容(标题与正文)。\n生成图片卡片(封面及正文卡片),并支持发布小红书笔记。", - }, - ], + children: [{ id: "6099", name: "小红书笔记智造官" }], }, { suggestion: "精美报告", prompt: "编写[产品/功能]的使用指南,包含操作步骤、注意事项和常见问题。", icon: GraduationCapIcon, - children: [ - { - id: "6100", - name: "MD 转 PDF 助手", - detail: - "将 Markdown(.md)文件转换为专业排版的 PDF 文档。\n支持将 markdown 转换为 PDF。\n支持将 .md 文件转为 .pdf。\n支持从 markdown 生成 PDF 报告。\n适用于中文技术报告、学术论文、文档编写及各类专业排版需求。", - }, - ], + children: [{ id: "6100", name: "MD 转 PDF 助手" }], }, { suggestion: "excel数据处理", prompt: "对[Excel文件/数据]进行分析,生成数据洞察和可视化建议。", icon: MicroscopeIcon, - children: [ - { - id: "17", - name: "Excel处理", - detail: - "全面的电子表格创建、编辑与分析功能。\n支持公式运算、格式设置、数据分析和可视化呈现。\n当 Claude 需要处理各类电子表格文件(如 .xlsx、.xlsm、.csv、.tsv 等格式)时,可执行以下操作:\n(1) 创建包含公式与格式的新表格;\n(2) 读取或分析表格数据;\n(3) 在保留公式的前提下修改现有表格;\n(4) 在表格内进行数据分析与可视化处理;\n(5) 重新计算公式。", - }, - ], + children: [{ id: "17", name: "Excel处理" }], }, { suggestion: "营销策划", prompt: "针对[行业/产品]进行市场调研,分析市场规模、竞品和趋势。", icon: ShapesIcon, - children: [ - { - id: "217", - name: "产品营销背景", - detail: - "当用户需要创建或更新产品营销背景文档时使用。\n可用于沉淀目标用户、市场环境与竞争格局等关键信息。", - }, - ], + children: [{ id: "217", name: "产品营销背景" }], }, ], suggestionsCreate: [ diff --git a/frontend/src/core/iframe-messages.ts b/frontend/src/core/iframe-messages.ts index a9d6866f..9008f22d 100644 --- a/frontend/src/core/iframe-messages.ts +++ b/frontend/src/core/iframe-messages.ts @@ -68,7 +68,6 @@ export interface SelectedSkillMessage { export interface SelectedSkillPayloadItem { id: string | number; name: string; - detail?: string; } type UnknownRecord = Record; @@ -108,16 +107,8 @@ export function isSelectedSkillsMessage( if (!skill) return false; const id = skill.id; const name = skill.name; - const detail = skill.detail; const isValidId = typeof id === "string" || typeof id === "number"; - const isValidDetail = - detail === undefined || typeof detail === "string"; - return ( - isValidId && - typeof name === "string" && - name.trim().length > 0 && - isValidDetail - ); + return isValidId && typeof name === "string" && name.trim().length > 0; }); } diff --git a/frontend/src/hooks/use-iframe-skill.ts b/frontend/src/hooks/use-iframe-skill.ts index 90743a4e..bf4fcf82 100644 --- a/frontend/src/hooks/use-iframe-skill.ts +++ b/frontend/src/hooks/use-iframe-skill.ts @@ -30,26 +30,6 @@ function getThreadStorageKey(threadId?: string | null): string | null { return `${STORAGE_KEYS.byThreadPrefix}${normalized}`; } -function persistSkillsToSessionStorage( - skills: SkillData[], - threadId?: string | null, -) { - const threadKey = getThreadStorageKey(threadId); - if (skills.length === 0) { - window.sessionStorage.removeItem(STORAGE_KEYS.latest); - if (threadKey) { - window.sessionStorage.removeItem(threadKey); - } - return; - } - - const payload = JSON.stringify(skills); - window.sessionStorage.setItem(STORAGE_KEYS.latest, payload); - if (threadKey) { - window.sessionStorage.setItem(threadKey, payload); - } -} - function parseStoredSkills(raw: string | null): SkillData[] { if (!raw) return []; try { @@ -235,8 +215,21 @@ export function useIframeSkill( // 4. 选择变化时同步到 sessionStorage useEffect(() => { - // 空数组也要同步到存储,避免 UI 状态与缓存不一致 - persistSkillsToSessionStorage(selectedSkills, threadId); + const threadKey = getThreadStorageKey(threadId); + if (selectedSkills.length === 0) { + // 空数组也要同步到存储,避免 UI 状态与缓存不一致 + window.sessionStorage.removeItem(STORAGE_KEYS.latest); + if (threadKey) { + window.sessionStorage.removeItem(threadKey); + } + return; + } + + const payload = JSON.stringify(selectedSkills); + window.sessionStorage.setItem(STORAGE_KEYS.latest, payload); + if (threadKey) { + window.sessionStorage.setItem(threadKey, payload); + } }, [selectedSkills, threadId]); // 发送选择预定义 skill @@ -293,15 +286,6 @@ export function useIframeSkill( }); try { - // 发起 bootstrap 时立即同步缓存;失败会在 removeFailedSkills 中回滚。 - const normalizedSkills = selectedSkills.map((item) => ({ - skill_id: String(item.id), - title: item.name, - })); - setSelectedSkill(normalizedSkills[0] ?? null); - setSelectedSkills(normalizedSkills); - persistSkillsToSessionStorage(normalizedSkills, threadId); - const result = await bootstrapRemoteSkill({ thread_id: threadId, content_ids, @@ -324,12 +308,12 @@ export function useIframeSkill( } sendSelectSkill(selectedSkills); - const latestSkills = selectedSkills.map((item) => ({ + const normalizedSkills = selectedSkills.map((item) => ({ skill_id: String(item.id), title: item.name, })); - setSelectedSkill(latestSkills[0] ?? null); - setSelectedSkills(latestSkills); + setSelectedSkill(normalizedSkills[0] ?? null); + setSelectedSkills(normalizedSkills); toast.success(t.skills.loadSuccessWithTitle(title), { description: result.message || t.skills.createdFiles(result.created_files),