diff --git a/docs/todo.md b/docs/todo.md index 49ab318..69627fa 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -75,7 +75,7 @@ > 排查性质(session-role-diagnose-only),走查定位根因 + 记录待办,未改代码。 -- [ ] B-260616-21 [P2] — **aichat 工具卡片「重复读取同一文件」表象(read_file 出现两条卡:一 0 行·7.1KB running、一 183 行·7.1KB completed)**。用户报对话记录里 `读取 .../api/ai.ts` 出现两次。**根因分析**(已走查前端流转 + 后端 emit 点):①前端 `useAiEvents.ts:202-206` `AiToolCallStarted` 分支直接 `lastMsg.toolCalls.push(info)`,**无 id 幂等守卫**(对比同文件 `startToolSlowTimer:58` 有 `_toolTimers.has(callId)` 守卫,Started 漏了同款判重)②`findToolCall`(`aiShared.ts:41-49`) 按 `id` 命中**首个**返回,`AiToolCallCompleted`(`:213`) 只 update 第一张卡 → 同 id 若被 push 两次,第二张卡永远 running,`ToolCard.vue:72` `parsed?.lines||0` 无 result 时显示 `0 行` ③后端 `audit.rs:538-549` `process_tool_calls` 对 tc_list 每 draft emit 一次 Started;read_file=Low risk(`:561`) **无去重**(去重仅 High,`:553-556` 注释明确 Low 不进)。**两候选根因**:**(A 最贴合现象)** 同一 `tool_call_id` 被**重复 emit Started**(后端 `process_tool_calls` 重入 / `stream_recv.rs` 生成重复 id / `agentic.rs` loop 重 process 同轮)→ push 两张卡 → Completed 只更首张(183 行)→ 次张残留 running(0 行);**(B)** agent loop 多轮 LLM 真读两次(不同 id)→ 但应两卡皆 183 行,与「0 行 + 183 行」现象不符,**排除为主因**。**待查**:后端同 id 重复 emit 的确切路径(`audit.rs:532` tc_list 来源 `tool_calls_acc` + `stream_recv.rs` id 生成 + `agentic.rs` 是否对同轮 tool_calls 重复 process)。**修复方向**(待定):①前端 Started push 前加 `findToolCall(event.id)` 幂等守卫(确定性低风险,对齐 startToolSlowTimer 守卫,治标兜底) ②后端查重复 emit 源治本(定位 stream_recv/process/loop 哪环生重复)。**关联** F-260616-05(agentic loop 工具去重缺失,batch53 仅 High 去重,read_file/search_files 等 Low 未去重;本现象若实为候选 A 则与 F-05 不同维度——F-05 是不同 id 重复调用,本条是同 id 重复 emit)。— src/composables/ai/useAiEvents.ts(:195-209 Started 分支) + src/composables/ai/aiShared.ts(:41 findToolCall) + src/components/ToolCard.vue(:72 lines 展示) + src-tauri/src/commands/ai/audit.rs(:538-549 emit Started)。 +- [ ] B-260616-21 [P1] — **aichat 工具卡片「重复读取同一文件」表象(read_file 出现两条卡:一 0 行·7.1KB running、一 183 行·7.1KB completed)**。用户报对话记录里 `读取 .../api/ai.ts` 出现两次。**根因分析**(已走查前端流转 + 后端 emit 点):①前端 `useAiEvents.ts:202-206` `AiToolCallStarted` 分支直接 `lastMsg.toolCalls.push(info)`,**无 id 幂等守卫**(对比同文件 `startToolSlowTimer:58` 有 `_toolTimers.has(callId)` 守卫,Started 漏了同款判重)②`findToolCall`(`aiShared.ts:41-49`) 按 `id` 命中**首个**返回,`AiToolCallCompleted`(`:213`) 只 update 第一张卡 → 同 id 若被 push 两次,第二张卡永远 running,`ToolCard.vue:72` `parsed?.lines||0` 无 result 时显示 `0 行` ③后端 `audit.rs:538-549` `process_tool_calls` 对 tc_list 每 draft emit 一次 Started;read_file=Low risk(`:561`) **无去重**(去重仅 High,`:553-556` 注释明确 Low 不进)。**两候选根因**:**(A 最贴合现象)** 同一 `tool_call_id` 被**重复 emit Started**(后端 `process_tool_calls` 重入 / `stream_recv.rs` 生成重复 id / `agentic.rs` loop 重 process 同轮)→ push 两张卡 → Completed 只更首张(183 行)→ 次张残留 running(0 行);**(B)** agent loop 多轮 LLM 真读两次(不同 id)→ 但应两卡皆 183 行,与「0 行 + 183 行」现象不符,**排除为主因**。**待查**:后端同 id 重复 emit 的确切路径(`audit.rs:532` tc_list 来源 `tool_calls_acc` + `stream_recv.rs` id 生成 + `agentic.rs` 是否对同轮 tool_calls 重复 process)。**修复方向**(待定):①前端 Started push 前加 `findToolCall(event.id)` 幂等守卫(确定性低风险,对齐 startToolSlowTimer 守卫,治标兜底) ②后端查重复 emit 源治本(定位 stream_recv/process/loop 哪环生重复)。**关联** F-260616-05(agentic loop 工具去重缺失,batch53 仅 High 去重,read_file/search_files 等 Low 未去重;本现象若实为候选 A 则与 F-05 不同维度——F-05 是不同 id 重复调用,本条是同 id 重复 emit)。— src/composables/ai/useAiEvents.ts(:195-209 Started 分支) + src/composables/ai/aiShared.ts(:41 findToolCall) + src/components/ToolCard.vue(:72 lines 展示) + src-tauri/src/commands/ai/audit.rs(:538-549 emit Started)。 ### 💡 2026-06-16 新需求(已分析·待实施) @@ -604,14 +604,14 @@ - [x] ✅(batch51·2026-06-16·workflow wdfffj1ti) UX-2025-09 [P2] — **编辑已发送消息并重新生成**(§1.1)。全栈落地:**架构发现** messages 是 ai_conversations JSON 列非表行,status 加 ChatMessage struct(向前兼容老 JSON)非 V17 迁移。df-ai provider.rs status+is_active / context.rs sanitize 过滤 truncated+replace+truncate / commands.rs ai_chat_edit IPC(复用 ai_regenerate spawn)/ api+useAiSend editMessage / AiChat 末条 user 编辑按钮 / i18n 4 key。只末条 active user 可编辑+generating 拦截+truncated 软删保留 DB 不进上下文/视图 - [x] ✅(batch52·2026-06-16·workflow w91svrokz) UX-2025-10 [P2] — **@ 实体引用**(§1.4)。全栈落地:AiChat @ popover(复用技能架构 .ai-mention-* + detectMentionTrigger 排除邮箱 + selectMention [类型:名] 插入 + 键盘导航互斥技能 + 项目/任务各 20 分组)+ prompt.rs 任务段注入(state.tasks.list_active,**签名零变化**降级避碰 ai_chat_send 领地)+ i18n 5 key。降级:实体 id 未透传(凭名称+状态对齐),最近文件联想无 store 略 - [x] UX-2025-11 [P2] — **输入框高度提升**(§1.5)。textarea 最大高度 120px→200px(~10 行),超过后再内部滚动。或可拖拽 resize handle。— src/components/AiChat.vue (第⑬批销账 06-15) -- [ ] UX-2025-12 [P2] — **历史消息分页懒加载**(§2.3)。⏸️ **暂缓**:当前典型对话 <100 条消息,v-for 全量渲染无卡顿;等用户反馈长对话卡顿再做。switchConversation 首次加载最近 50 条,滚顶加载更多(需后端 offset/limit 支持 messages 查询)。或前端虚拟滚动替代。关联 B-260629(messages 无上限)。— src/stores/ai.ts + src/composables/ai/useAiConversations.ts + 后端 conversation.rs +- [ ] UX-2025-12 [P2] — **历史消息分页懒加载**(§2.3)。**▶ 已打开推进(2026-06-17 用户决定不再等痛点,与 UX-19 一并纳入)**。方案:switchConversation 首次加载最近 50 条,滚顶加载更多(需后端 messages 查询加 offset/limit 支持)。关联 B-260629(messages 无上限)。**决策点(待定)——与 UX-19(虚拟滚动)二选一**解决长对话渲染卡顿:分页(后端配合,简单稳定,但切换/搜索跨页体验割裂)vs 虚拟滚动(纯前端,体验流畅,但流式末条挂载复杂)。**倾向 UX-19 虚拟滚动优先**(纯前端零后端改动+体验优),本项分页作为虚拟滚动不足时的补充方案。— src/stores/ai.ts + src/composables/ai/useAiConversations.ts + 后端 conversation.rs - [x] UX-2025-13 [P2] — **消息时间戳**(§2.4)。气泡下方极小字号(dim 色)展示相对时间(复用 time.ts 已有 common.justNow 等 key);hover tooltip 展示完整时间。— src/components/AiChat.vue (第⑬批销账 06-15) - [x] ✅(第㉒批 2026-06-16) UX-2025-14 [P2] — **Provider 切换反馈**(§5.1)。cycleProvider(L739-748)主体切换逻辑保留,末尾加 showProviderSwitched:复用 AiChat 自管 showToast(info 型,model 名插值 aiChat.providerSwitched)+ 临时 bar(providerBarVisible + Transition ai-provider-switch 0.15s 淡入/淡出 + 2000ms 自动淡出,展示 aiChat.providerSwitchHint model 名小字+accent 脉动圆点)。toast 调研:AiChat 已有自管 reactive showToast(分离窗口无根 toast 故自管),复用无降级。_providerBarTimer onBeforeUnmount 清(L1067)。stores/ai.ts 未改(cycleProvider 在 AiChat.vue 内)。— src/components/AiChat.vue + i18n(aiChat.providerSwitched/providerSwitchHint zh/en) - [x] ✅(第㉓批 2026-06-16) UX-2025-15 [P2] — **技能参数用法展示**(§5.2)。浮层 .ai-skill-item 改 column:.ai-skill-item-row(name/desc/src 单行)+ code.ai-skill-item-hint(v-if s.argument_hint,等宽 dim 11px,L390)。chip(L354-361):.ai-skill-chip-main(name+desc+code.ai-skill-chip-hint v-if pendingSkill.argument_hint L358)+ x 关闭(@click clearSkill,补 aria-label=aiChat.clearSkill 无障碍)。复用 var(--df-font-mono)+ui-monospace fallback。**JS 零改动**(仅 template 结构+CSS,/ 联想/selectSkill/clearSkill/Escape/Enter 逻辑全保留)。i18n 无新增(argument_hint 是 skill 数据自带,复用 aiChat.clearSkill)。— src/components/AiChat.vue - [x] ✅(第㉑批 2026-06-16) UX-2025-16 [P2] — **侧栏宽度可拖拽**(§6.1)。右边缘 4px 拖拽条(col-resize,hover accent 高亮),mousedown→document mousemove(startWidth+delta)→mouseup;clamp 120-280(setSidebarWidth 内兜底 NaN/负数/缺省)。**持久化**:move 高频只改 state 不落盘,up 一次性 persistUiState 写 df-ai-ui(复用现有机制,App.vue key 已注册)。**宽度绑定 CSS 变量** `--sidebar-width`(非内联 width,保 sidebar-slide 折叠过渡不被压过)。**卸载兜底** onBeforeUnmount 摘 listener 防泄漏。body.ai-sidebar-resizing 锁 cursor+user-select 防抖动/误选。— src/components/AiChat.vue + src/composables/ai/useAiPanel.ts + src/stores/ai.ts(sidebarWidth:200) + i18n(aiChat.sidebarResizeHint zh/en) - [x] ✅(batch49·2026-06-16·workflow wkan76fon) UX-2025-17 [P3] — **对话置顶**(§3.2)。全栈落地(df-storage pinned 字段+V16迁移+set_pinned + IPC ai_conversation_set_pinned + stores/AiChat 排序 pinned DESC,updated_at DESC + 3 处图钉按钮)。注:无独立 Sidebar.vue 侧栏在 AiChat.vue,conversation.rs:141 补 pinned:false 必填(跨 batch44 领地 proactive-bug-fix),filteredConversations 搜索态未动(降级) - [x] ✅(batch46 后端 + batch48 前端·2026-06-16) UX-2025-18 [P3] — **对话导出**(§3.3)。全闭环:后端(batch46 ai_conversation_export IPC commands.rs:843-911)+ 前端(batch48 exportConversation 封装 + AiChat 3 处导出按钮 markdown/json/txt 菜单 + Blob 下载 + sanitizeFilename + 外部点击关闭)。注:无独立 Sidebar.vue 侧栏在 AiChat.vue -- [ ] UX-2025-19 [P3] — **消息列表虚拟滚动**(§7.2)。⏸️ **暂缓**:同 UX-12,当前数据量无需虚拟滚动;与 UX-12 合并为一条观察项,等用户反馈长对话卡顿再启动。长对话几百条消息全量渲染卡顿;集成 vue-virtual-scroller 或自研 IntersectionObserver 懒渲染;流式最后一条始终保持挂载。— src/components/AiChat.vue + 新依赖 +- [ ] UX-2025-19 [P2] — **消息列表虚拟滚动**(§7.2)。**▶ 已打开推进(2026-06-17 用户决定不再等痛点,与 UX-12 一并纳入,作为首选方案)**。方案:集成 vue-virtual-scroller 或自研 IntersectionObserver 懒渲染;流式最后一条始终保持挂载(参照现有 onContentChange/isNearBottom 滚动逻辑)。**与 UX-12 分页二选一**(见 UX-12 决策点),**倾向本项优先**(纯前端零后端改动+体验优,P3→P2 升级因纳入推进)。— src/components/AiChat.vue + 新依赖(vue-virtual-scroller) - [x] ✅(第㉒批 2026-06-16) UX-2025-20 [P3] — **空状态引导 + 标题过渡**(§7.3+§7.4)。空态:messages 空+非流式时 .ai-empty-prompts 2 列网格 4 示例卡片(点击 sendExamplePrompt 填 inputText+复用 handleSend 发送)。无 provider 显「去设置」按钮(goToSettings:嵌入 router.push('/settings'),分离窗口降级 showToast 提示去主窗口)。标题淡入:titleFlash ref + watch(activeConv.title)空→非空(loadConversations 回填触发)置 flash 驱动 0.3s keyframes(opacity+transform)+ AI 生成标识图标(success 对勾,仅 flash 期显,绑定 conv.id===activeConversationId)。_titleFlashTimer 320ms 清 + onBeforeUnmount 兜底(L1065)。— src/components/AiChat.vue + i18n(aiChat.examplePrompt1~4/goToSettings/goToSettingsHint zh/en) ### 🟡 AI Chat 授权体验改进(2026-06-14 方案 → 待办化)