From d00b30f0ba29e36be06410aaf242f8926a3187af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Tue, 16 Jun 2026 19:56:29 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84:=20AI=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E5=8F=AF=E9=9D=A0=E6=80=A7=E6=89=B9=E6=AC=A1(=E5=AE=A1?= =?UTF-8?q?=E6=89=B9=E7=B4=AF=E8=AE=A1=E8=AE=A1=E6=95=B0/write=5Ffile=20di?= =?UTF-8?q?ff=E9=A2=84=E8=A7=88/=E4=BC=9A=E8=AF=9D=E9=9A=94=E7=A6=BB?= =?UTF-8?q?=E8=A1=A5=E6=B8=85/token=E7=BC=93=E5=AD=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/todo.md | 32 ++++++------- docs/待决策.md | 17 +++++-- docs/待审查.md | 26 ++++++++++- src-tauri/src/commands/ai/agentic.rs | 40 ++++++++++------- src-tauri/src/commands/ai/audit.rs | 39 ++++++++++++++++ src-tauri/src/commands/ai/commands.rs | 37 ++++++++++++--- src-tauri/src/commands/ai/mod.rs | 21 ++++++++- src-tauri/src/commands/ai/tool_registry.rs | 5 ++- src/api/types.ts | 5 ++- src/components/ToolCard.vue | 52 ++++++++++++++++++++++ src/composables/ai/useAiConversations.ts | 7 +++ src/composables/ai/useAiEvents.ts | 2 + 12 files changed, 238 insertions(+), 45 deletions(-) diff --git a/docs/todo.md b/docs/todo.md index dbc9979..5787edb 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -76,15 +76,15 @@ - [x] ✅(batch56·2026-06-16·workflow wsxxurgbr+主代核查) F-260616-08 [P2] — **AI 数据获取工具 offset 分页 + 探总量**。**全闭环落地**:①6 工具(list_projects/tasks/ideas/trash/read_file搜索模式/search_files)全部补 offset+limit 参数,返回 items/total/has_more 替代 truncated ②新增 `get_project_count`+`get_task_count` 探总量工具 ③knowledge_search 解钳 `min(3)`→`min(20)`。— src-tauri/src/commands/ai/tool_registry.rs + src-tauri/src/commands/knowledge.rs -- [ ] F-260616-09 [P1/架构] — **AiSession 单例 → 多会话并发架构(多窗口来回切 + 并行执行)**。用户需求(3 条消息强化):「应用级支持同时具备多会话」+「同时具备多会话的能力」+「多个会话窗口来回切,**并行执行**」。现状(Explore 走查确认):`AiSession` **应用级单例**(`state.rs:164` `ai_session: Arc>`,init:205),messages/generating/stop_flag/pending_approvals/agent_language 全一份。设计「单例 + 软隔离」——generating 互斥(同一时刻**只一个对话能跑**) + 切换旧 loop 退出(`agentic.rs:177-190` B-260615-11) + 切换时 generating→readonly(`commands.rs:873-881`)。`llm_concurrency` per_conv 因此退化为单对话内并发(`state.rs:93-96` 注释已标注预留改 HashMap)。**memory 已标记「AiSession 单例未动」架构债**(aichat-arch-extensibility)。**残留 bug**(表象=新建会话上下文残留):前端 `newConversation`(`useAiConversations.ts:31-41`) 漏清 queue/generatingConvId/agentRound/searchQuery;后端 `ai_conversation_create`(`commands.rs:784-821`) 漏清 agent_language/stop_flag(设 true 停旧 loop 没复位 false)。**用户「并行执行」明确=B 方向**(A 只解切换残留不满足并行)。**分档**:**A 短期隔离修复**(补漏清字段,维持单例软隔离,解残留,低风险立即做,**B 前置**) + **B 多会话并发架构**(单例 → `HashMap`,每会话独立 messages/generating/stop_flag/pending_approvals + llm_concurrency per_conv 改真 per-conv HashMap + loop 内校验改 conv_id 索引各自 session + 切换不退出旧 loop 各自跑完真并发 + 事件路由核对 conversation_id 全覆盖)。**决策点(待定)**:(a) A 先做解燃眉 vs 直接 B——倾向 **A 先**(残留真 bug 立即解,B 架构另立项) (b) B 形态——AiSession 整体 HashMap vs 拆字段(messages/pending 已可按 conv reload,只拆 generating/stop_flag per-conv)——倾向**后者侵入小** (c) B 并发上限——复用 llm_concurrency.global 限并发会话数防 token 暴增——倾向是 (d) **多窗口 UI 形态**——(d1) 单面板侧栏会话列表切换+后台并行(改动小) vs (d2) 每会话独立 Tauri 窗口(`useAiWindow.ts` detached 已有基础可扩,改动大但「窗口来回切」语义最贴)——**待用户定**,倾向先 d1 后 d2 (e) 旧 loop 切换处理——不退出各自跑完(真并发,倾向) vs 仍退出。— state.rs(AiSession 去单例化) + commands.rs(create/switch 重构 + 漏清补) + agentic.rs(:177-190 校验改 conv_id 索引) + context.rs(ContextManager per-conv) + useAiConversations.ts(漏清补) + stores/ai.ts(per-conv state) + llm_concurrency per_conv + useAiWindow.ts(若 d2 多窗口)。**B 是大改架构升级,需单独设计文档 + 立项**。**待用户定 A 先做 / B 立项 / 多窗口 d1·d2 方向**。 +- [ ] F-260616-09 [P1/架构·**A路线✅(batch60·2026-06-16·workflow whae812z5+主代核查),B多会话并发待立项**] — **AiSession 单例 → 多会话并发架构(多窗口来回切 + 并行执行)**。用户需求(3 条消息强化):「应用级支持同时具备多会话」+「同时具备多会话的能力」+「多个会话窗口来回切,**并行执行**」。现状(Explore 走查确认):`AiSession` **应用级单例**(`state.rs:164` `ai_session: Arc>`,init:205),messages/generating/stop_flag/pending_approvals/agent_language 全一份。设计「单例 + 软隔离」——generating 互斥(同一时刻**只一个对话能跑**) + 切换旧 loop 退出(`agentic.rs:177-190` B-260615-11) + 切换时 generating→readonly(`commands.rs:873-881`)。`llm_concurrency` per_conv 因此退化为单对话内并发(`state.rs:93-96` 注释已标注预留改 HashMap)。**memory 已标记「AiSession 单例未动」架构债**(aichat-arch-extensibility)。**残留 bug**(表象=新建会话上下文残留):前端 `newConversation`(`useAiConversations.ts:31-41`) 漏清 queue/generatingConvId/agentRound/searchQuery;后端 `ai_conversation_create`(`commands.rs:784-821`) 漏清 agent_language/stop_flag(设 true 停旧 loop 没复位 false)。**用户「并行执行」明确=B 方向**(A 只解切换残留不满足并行)。**分档**:**A 短期隔离修复**(补漏清字段,维持单例软隔离,解残留,低风险立即做,**B 前置**) + **B 多会话并发架构**(单例 → `HashMap`,每会话独立 messages/generating/stop_flag/pending_approvals + llm_concurrency per_conv 改真 per-conv HashMap + loop 内校验改 conv_id 索引各自 session + 切换不退出旧 loop 各自跑完真并发 + 事件路由核对 conversation_id 全覆盖)。**决策点(待定)**:(a) A 先做解燃眉 vs 直接 B——倾向 **A 先**(残留真 bug 立即解,B 架构另立项) (b) B 形态——AiSession 整体 HashMap vs 拆字段(messages/pending 已可按 conv reload,只拆 generating/stop_flag per-conv)——倾向**后者侵入小** (c) B 并发上限——复用 llm_concurrency.global 限并发会话数防 token 暴增——倾向是 (d) **多窗口 UI 形态**——(d1) 单面板侧栏会话列表切换+后台并行(改动小) vs (d2) 每会话独立 Tauri 窗口(`useAiWindow.ts` detached 已有基础可扩,改动大但「窗口来回切」语义最贴)——**待用户定**,倾向先 d1 后 d2 (e) 旧 loop 切换处理——不退出各自跑完(真并发,倾向) vs 仍退出。— state.rs(AiSession 去单例化) + commands.rs(create/switch 重构 + 漏清补) + agentic.rs(:177-190 校验改 conv_id 索引) + context.rs(ContextManager per-conv) + useAiConversations.ts(漏清补) + stores/ai.ts(per-conv state) + llm_concurrency per_conv + useAiWindow.ts(若 d2 多窗口)。**B 是大改架构升级,需单独设计文档 + 立项**。**待用户定 A 先做 / B 立项 / 多窗口 d1·d2 方向**。 - [x] ✅(batch58·2026-06-16·workflow w2dw65maz+主代核查,vue-tsc 0err) F-260616-10 [P1·bug] — **provider 切换/默认值重启·刷新后失效(前端 activeProvider 不同步 DB 默认)**。用户实测:Settings 设 A 为默认 → 重启 → 默认变 B(列表第一个)。**根因全在前端,后端链路正确**(独立核验不信 PROGRESS.md:「已修复」声明不可信):`ai_set_provider`(`commands.rs:747-774`) 互斥落库✅ + `update_full`(`crud.rs:1127`) SQL UPDATE 含 `is_default`✅ + `get_active_provider`(`prompt.rs:9-32`) 走 `DB.is_default`✅。**根因链**:①重启后前端 `store.activeProvider = null`(`stores/ai.ts:53`,无 localStorage 持久化 + 无启动恢复) ②`loadProviders`(`useAiPanel.ts:91-93`) 只拉列表**不同步 DB 默认** → activeProvider 仍 null ③**UI 错觉**:`activeProviderName`(`AiChat.vue:1169-1172`) null 时 fallback `providers[0]`(列表第一个) ≠ 用户设的 A → 「默认变另一个」 ④**真覆盖**:用户点 provider bar 切换 `cycleProvider`(`AiChat.vue:1180-1185`),`findIndex(activeProvider=null)` 返回 **-1** → `next = ps[(−1+1)%len] = ps[0]` = 列表第一个 B → `setProvider(B)` → 后端互斥落库把 `DB.is_default` 从 A **真改成 B**。即使用户不点切换,UI 也显示 providers[0] 而非 A。**修复方向**:①`loadProviders` 后同步 `activeProvider` = DB.is_default 的 provider——后端加 `ai_get_active_provider` IPC 返回当前默认 id,或 `listProviders` 返回值带 `active_provider_id` ②`cycleProvider` findIndex=-1 陷阱修正(null 时应从 DB 默认起算,非 ps[0]) ③前端 activeProvider 持久化(localStorage `df-ai-active-provider`,对齐 `df-ai-active-conv` 模式)。— useAiPanel.ts(loadProviders 同步默认) + AiChat.vue(:1169 activeProviderName fallback + :1180 cycleProvider findIndex 陷阱) + commands.rs(新 ai_get_active_provider IPC) + api/ai.ts + stores/ai.ts(activeProvider 持久化)。**确定性 bug,低风险,可立即修**。 -- [ ] F-260616-11 [P1/设计风险] — **审批后 iteration 重置致 max_iterations/token 超配**。性能分析批次发现:`try_continue_agent_loop` 重新 spawn `run_agentic_loop`,**iteration 从 0 重计**(`agentic.rs:418` 注释,F-260616-03 续跑决策 a 的延续,审批续跑复用此入口)。每次审批(Med/High 工具)后配额重置,多次审批=反复重跑 max_iterations 轮,实际可跑远超配额,**token 消耗不受 max_iterations 严格控制**。影响:长对话多次审批致 token 失控 + 对话变慢(每审批一次重跑 LLM 轮次)。**方向(待评估)**:①审批不重置 iteration(累计计数)②审批是用户决策点重置合理但需独立「审批次数」配额 ③至少 token 总量兜底。— agentic.rs(try_continue iteration 计数策略)。**需决策**:iteration 累计 vs 重置。 +- [x] ✅(batch60合批·2026-06-16·主代核查,cargo 0err) F-260616-11 [P1·决策a累计计数] — **审批后 iteration 重置致 max_iterations/token 超配**。决策(2026-06-16):**a 累计计数**(try_continue 不重置 iteration,跨审批续跑延续累计)。**已实施**:`iteration_used:usize` 累计字段(`mod.rs:215/230` AiSession+new init 0)+`start_iteration:usize` 参数(`agentic.rs:114` run_agentic_loop signature +`:176` loop 边界 `start_iteration..max_iterations` +`:210` 一致性块 `iteration_used=iteration+1` +`:578` try_continue 加参 +`:675` spawn 透传)+三处 reset(`commands.rs:55/152/412` ai_chat_send/ai_regenerate/ai_edit_last 新生命周期)+ai_approve 两处累计读 iteration_used(`:260-262` 拒绝续跑 +`:313-315` 通过续跑)+`ai_continue_loop:601` reset+传0(F-03 决策a 达max重计区分)。两路径严格:审批累计(ai_approve) vs 达max重计(ai_continue_loop,F-03 a 用户授权重来) vs 新消息 reset。边界case:start≥max loop 空区间→converged=false→AiMaxRoundsReached(防无限审批烧 token,正 F-11 语义)。原:`try_continue_agent_loop` 重 spawn `run_agentic_loop` iteration 从 0 重计致多次审批反复重跑 max_iterations 轮 token 失控。— agentic.rs + commands.rs + mod.rs。 - [ ] F-260616-12 [P2/依赖F-09] — **retry 持 permit 不释放(多会话隐患)**。F-260616-07 落地的流式重试循环(`agentic.rs:238-239`)重试期间持有 global+per_conv permit 不释放(注释「防新请求挤占」)。当前 AiSession 单例 + 主 loop 串行无影响,但 **F-260616-09 多会话并发后**,重试期间阻塞其他对话 LLM 调用。**方向**:多会话落地时核对——主 loop 串行下重试持 permit 防自己挤占无意义,倾向重试不持 permit 或仅持 per_conv。— agentic.rs:238 + F-260616-09 多会话架构。**依赖 F-260616-09 立项后一并处理**。 -- [ ] F-260616-13 [P2/小优化] — **build_for_request 持锁重活 + system_prompt token 每轮重估**。性能分析批次发现:每轮 `stream_llm` 前 `session_arc.lock()`(`agentic.rs:251`)持锁期间做 `TokenEstimator::estimate_text(system_prompt)`(L252)+ `build_for_request`(L253 history clone + 裁剪)。system_prompt loop 外固定传入,**每轮重估其 token 是浪费**(可缓存);build_for_request 持锁做 history clone 是重活,消息多时(200 cap)锁持有期长。**方向**:①system_prompt token loop 外算一次缓存 ②build_for_request 先 clone messages 释放锁再裁剪。低收益优化。— agentic.rs:251-253。 +- [x] ✅(batch60·2026-06-16·workflow whae812z5+主代核查,cargo 0err) F-260616-13 [P2] — **build_for_request 持锁重活 + system_prompt token 每轮重估**。性能分析批次发现:每轮 `stream_llm` 前 `session_arc.lock()`(`agentic.rs:251`)持锁期间做 `TokenEstimator::estimate_text(system_prompt)`(L252)+ `build_for_request`(L253 history clone + 裁剪)。system_prompt loop 外固定传入,**每轮重估其 token 是浪费**(可缓存);build_for_request 持锁做 history clone 是重活,消息多时(200 cap)锁持有期长。**方向**:①system_prompt token loop 外算一次缓存 ②build_for_request 先 clone messages 释放锁再裁剪。低收益优化。— agentic.rs:251-253。 - [x] ✅(主代核验·2026-06-16) F-260616-14 [P2/核验] — **max_tokens 8192 截断 + 前端批量审批核验**。核验结论:①`max_tokens=8192`(实际 `agentic.rs:270`,行号漂移)≈6-8k 字日常够用,长回复截断由 batch59 MidStream 保文兜底不丢文,**可配低优先非阻塞**(登记可选优化)②前端批量审批 **AE-2025-01 已完整闭环**(ToolCardList.vue:4-11 `ai-batch-approve` 栏 + AiChat.vue:444 `@batch-approve`→`store.batchApprove` + useAiSend.ts:338 遍历 pendingApprovals 逐个 approveToolCall + i18n approveAll/rejectAll),满足「一轮多 Med/High 全 insert pending→一次批量批全部」③audit.rs:496 `process_tool_calls` 一轮多工具 Med/High 进审批门控 insert pending + Low 并行执行链路通。无阻塞 bug,销账。— agentic.rs:270 + ToolCardList.vue + audit.rs:496。 @@ -126,7 +126,7 @@ - [x] ✅(batch35·2026-06-16·workflow) **①-3** i18n 文案 — `src/i18n/{zh-CN,en}/taskDetail.ts`(taskDetail.* 命名空间,8 key 对称:workflowAdvanceTitle/Advancing/AdvanceFailed{msg}/StepRunning{node}/StepsProgress{done}{total}/CompletedHint/FailedHint)。注:todo 原路径 `locales/tasks.ts` 不准,实为 `i18n/taskDetail.ts` - **实施顺序**:④-1→②-1→②-5→②-2→②-3→②-4→②-6→①-1/①-3。**最小里程碑**(④-1+②-1+②-2+②-3):run_workflow 单 task_advance 节点 DAG 端到端推进 todo→in_progress - [ ] F-260616-07 阶段3 AI 执行闭环(advance_task/run_workflow 工具+AiNode+自审)+ **F-03收口三件**(advance_task注册AI工具+update_task描述/schema移status+df-storage tasks白名单移status,同步落地防AI工具行为不一致) -- [ ] F-260616-08 阶段4 Git 集成(kind+git闸门+worktree) +- [x] ⏸️(待决策.md已决b暂缓·2026-06-16) F-260616-08 阶段4 Git 集成(kind+git闸门+worktree) - [x] ✅ **CR-260616-01 代码审查完成** → 审查登记已迁 [待审查.md](./待审查.md)(职责分离:审查队列独立,不进 todo)。结论 🔴0 🟡6 ⚪4 质量优,8 维度全过。**待修项 CR-01-A~I 见下方推进区**。 > **审查发现待修项(2026-06-16 · CR-260616-01)**: @@ -150,9 +150,9 @@ - [x] ✅(batch50·2026-06-16·workflow w6nqio4o5) **CR-15-1** [med] `crates/df-execute/src/shell.rs:60-79` 缺 `.kill_on_drop(true)` → 已修复(cmd.kill_on_drop(true) 三类型覆盖 + 注释配合 tokio::time::timeout 防孤儿,文案「进程已终止」名副其实) - [x] ✅(主代理验证:不成立·workspace cargo check exit 0) 🔴 **CR-23-1** [high·紧急·阻断构建] `crates/df-ai/src/context.rs:491` E0594 → **不成立**:审查代理跑 batch51 中间态 filter 版(EXIT 101),batch51 最终改 for 循环(EXIT 0,df-ai + workspace 双验证,代理自报属实)。context.rs:488 当前是 `for t in iter_mut()` 单层引用可写 status,无 filter 闭包。审查锚定滞后(跑 batch51 中间态)— CR-260616-23 归档 - [x] ✅(主代理修·CSS :has item 左边线) **CR-22-1** [med·UX] `AiChat.vue` 已置顶非 hover 不可见 → 已修复(`.ai-conv-item:has(.ai-conv-item-act--pinned){ box-shadow: inset 2px 0 0 accent }` 左边线强调,不破坏 hover 浮出 toggle,原方案图钉常驻与 actions display:none 矛盾,改 :has item 左线最简)— CR-260616-22 归档 - - [ ] **CR-34-1** [med] useAiPanel.ts:101 loadProviders 守卫 `if(!state.activeProvider)` 只查 null 不查有效性 — localStorage savedProvider 指向已删除 provider 时 activeProvider 残留无效 id → cycleProvider:1185 findIndex=-1 跳过 ps[0] + 发消息消费方用无效 id。修法:守卫加 `|| !list.some(p=>p.id===state.activeProvider)` 残留无效时回落 DB is_default — CR-260616-34 归档 - - [ ] **CR-30-1** [med] agentic.rs:298 退避 `1<p.id===state.activeProvider)` 残留无效时回落 DB is_default — CR-260616-34 归档 + - [x] ✅(CR-260616-35 复审 PASS·agent cargo 6+20 passed) **CR-30-1** [med] agentic.rs:298 退避 `1< 用户需求:AiChat 消息气泡支持一键拷贝——**文本拷贝**(content 原文→剪贴板) + **截图拷贝**(气泡 DOM 渲染→图片→剪贴板,粘贴出图片)。**现状**:全项目零 clipboard 实现(grep `navigator.clipboard`/`copy`/`clipboard`/`拷贝`/`复制` 零命中)。气泡结构:用户消息 `.ai-msg-bubble--user`(纯文本 `{{msg.content}}`, :185);AI 消息 `.ai-msg-bubble--ai.ai-md`(v-html 渲染 MD, :197)。**Tauri WebView2(Edge)**:Clipboard API 完整支持(`writeText`/`write([ClipboardItem])`),无需额外 plugin(做减法,不用 `@tauri-apps/plugin-clipboard-manager`)。**安全约束**:必须在用户点击事件(手势)内调用剪贴板 API。 - [x] F-260615-13 [P2] — **AiChat 气泡文本拷贝**。每条消息气泡右上角加拷贝按钮(📋 icon,hover 显示)。点击 → `navigator.clipboard.writeText(msg.content)` → 短暂 toast「已复制」。AI 气泡拷原始 content(非 v-html 渲染后 HTML,防 HTML 标签污染);用户气泡同。按钮位置:`.ai-msg-bubble` 内右下角或 hover 时右上角浮现(不占空间,信息密度优先)。i18n 加 `aiChat.copied` / `aiChat.copyMsg`。— src/components/AiChat.vue(:185 用户/:197 AI) + src/i18n (第⑫批销账 06-15) -- [ ] F-260615-14 [P2] — **AiChat 气泡截图拷贝(图片)**。同位置第二按钮(🖼️ 或与文本拷贝合并为下拉)。技术链:`html2canvas(bubbleElement)` → canvas → `canvas.toBlob('image/png')` → `new ClipboardItem({ 'image/png': blob })` → `navigator.clipboard.write([item])`。需加依赖 `html2canvas`(~90KB,或轻量替代 `dom-to-image-more` ~3KB,待选型)。**注意**:html2canvas 对 CSS 支持有限(阴影/渐变/CSS 变量可能不全),但气泡样式简单(ai-md 基本色+圆框),风险低。粘贴目标:Word/微信/钉钉等富文本编辑器可接图片;纯文本框不行(正常行为)。— src/components/AiChat.vue + 新依赖(html2canvas/dom-to-image-more 待选) +- [x] ✅(待决策.md已决c不做·2026-06-16) F-260615-14 [P2] — **AiChat 气泡截图拷贝(图片)**。同位置第二按钮(🖼️ 或与文本拷贝合并为下拉)。技术链:`html2canvas(bubbleElement)` → canvas → `canvas.toBlob('image/png')` → `new ClipboardItem({ 'image/png': blob })` → `navigator.clipboard.write([item])`。需加依赖 `html2canvas`(~90KB,或轻量替代 `dom-to-image-more` ~3KB,待选型)。**注意**:html2canvas 对 CSS 支持有限(阴影/渐变/CSS 变量可能不全),但气泡样式简单(ai-md 基本色+圆框),风险低。粘贴目标:Word/微信/钉钉等富文本编辑器可接图片;纯文本框不行(正常行为)。— src/components/AiChat.vue + 新依赖(html2canvas/dom-to-image-more 待选) ### 🟡 AI Chat 交互体验改进(2026-06-14 方案 → 待办化) @@ -548,7 +548,7 @@ - [x] AE-2025-01 [P0] — **批量审批**。同一轮多 pending 时,ToolCardList 顶部显示「全部批准(N) / 全部拒绝」按钮。改动:①`ToolCardList.vue` 加批量操作栏(监听 pending_approval 数量)②`useAiSend.ts` 新增 `approveAll(rejectAll)` 循环调 `ai_approve`。预估 0.5 天 — src/components/ToolCardList.vue + src/composables/ai/useAiSend.ts (第⑫批销账 06-15) - [x] AE-2025-02 [P0] — **审批计数器 + 跳转**。输入框上方/header 显示 `⏳ N 项待审批` 徽标,点击跳转到第一个 pending 卡片。改动:①`AiChat.vue` header 区域加审批徽标 ②`ToolCardList.vue` 暴露 `scrollToFirstPending` 方法。预估 0.5 天 — src/components/AiChat.vue + src/components/ToolCardList.vue (第⑬批销账 06-15) -- [ ] AE-2025-03 [P0] — **write_file diff 预览** ⚠️**降级阻塞(2026-06-16·batch54 workflow 探索+主代核验)**:后端 write_file **未返 diff**(`tool_registry.rs:694` 结果 payload `{path,bytes_written,old_size,encoding}` 无 diff 字段,`generate_diff` 仅 patch_file 调用),前端 ToolCard 无 diff 可渲染,审批 payload `AiApprovalRequired`/`PendingApproval` 亦无 diff(只有 `args={path,content,encoding}` 纯新内容,拿不到旧文件)。需**后端先补**再追加前端渲染。**两条路径**(均 src-tauri 改动,跨 mod.rs/audit.rs/types.ts/AiChat.vue 领地):路径 B(推荐,满足审批预览诉求)审批 payload 加 `diff` 字段 + audit.rs:555 挂起审批前预读旧文件 + `generate_diff` 注入事件;路径 A 仅 write_file handler 补 diff 但结果阶段才有(审批时看不到,不满足)。前端部分待后端 diff 返回后追加 `
` 红删绿增渲染。原诉求:write_file 审批时若文件已存在,展示前后对比 diff 而非裸 content(content 可能几百行代码截断到 300 字符)。改动:①后端 `tool_registry.rs` write_file handler 执行前读旧文件返回 diff ②`ToolCard.vue` pending_approval + name=write_file 时渲染 diff 视图(红删绿增)。关联 F-260615-10(diff 工具)可复用。预估 1 天 — src-tauri/src/commands/ai/tool_registry.rs + src/components/ToolCard.vue
+- [x] ✅(batch60·2026-06-16·workflow whae812z5+主代核查·路径B,cargo+vue-tsc 0err) AE-2025-03 [P0] — **write_file diff 预览** ✅**已落地(batch60 路径B)**:后端 write_file **未返 diff**(`tool_registry.rs:694` 结果 payload `{path,bytes_written,old_size,encoding}` 无 diff 字段,`generate_diff` 仅 patch_file 调用),前端 ToolCard 无 diff 可渲染,审批 payload `AiApprovalRequired`/`PendingApproval` 亦无 diff(只有 `args={path,content,encoding}` 纯新内容,拿不到旧文件)。需**后端先补**再追加前端渲染。**两条路径**(均 src-tauri 改动,跨 mod.rs/audit.rs/types.ts/AiChat.vue 领地):路径 B(推荐,满足审批预览诉求)审批 payload 加 `diff` 字段 + audit.rs:555 挂起审批前预读旧文件 + `generate_diff` 注入事件;路径 A 仅 write_file handler 补 diff 但结果阶段才有(审批时看不到,不满足)。前端部分待后端 diff 返回后追加 `
` 红删绿增渲染。原诉求:write_file 审批时若文件已存在,展示前后对比 diff 而非裸 content(content 可能几百行代码截断到 300 字符)。改动:①后端 `tool_registry.rs` write_file handler 执行前读旧文件返回 diff ②`ToolCard.vue` pending_approval + name=write_file 时渲染 diff 视图(红删绿增)。关联 F-260615-10(diff 工具)可复用。预估 1 天 — src-tauri/src/commands/ai/tool_registry.rs + src/components/ToolCard.vue
 
 **P1 — 增强控制力**
 
@@ -570,7 +570,7 @@
 
 - [ ] B-260614-05 — **[P2→降级]** 分离窗口(detached)跨窗口状态 — **核对修正**:reattachPanel 已接线(不再死代码)+ `tauri://destroyed` 监听已复位状态,"detached 永真卡死"已修复;剩余 localStorage `df-ai-gen`/`df-ai-text` 是 Sprint 19 **有意保留**(流式临时快照高频写),非 bug。仅在出现新场景失效时再评估改全局 emit/listen — **去重**:与 AR-M5(跨窗口 state 完全隔离)同类,aichat 审查描述更深 — source:代码审查 + Sprint 19 (06-14)
 - [x] T-260614-03 — ~~工具层白名单与 crud 白名单双份去重~~ ✅ WF-B ⑤ 已解决(tool_registry update_project 复用 is_allowed_column,与 CRUD 白名单同源,删硬编码 match)(06-14)
-- [ ] F-260614-02 — 技能联想「使用」 — 首批 3 类联想已做,联想后实际触发/执行技能未实现 (06-14)
+- [ ] F-260614-02 — 技能联想「使用」·**决策ai调用已定待设计**(2026-06-16) — 首批 3 类联想已做,「使用」=**AI 调用技能**(联想选中→技能内容/指令注入 AI context→AI 按指令执行,非执行本机 claude 技能)。实施机制待设计(技能→AI context 注入 vs 技能注册 AI 工具,依赖 F-07 trait 下沉)
 - [ ] F-260614-03 — **[需设计]** 灵感对抗评估接 LLM — 核对 adversarial.rs:`evaluate()` 纯启发式,接 LLM 需改签名注入 provider。3 种注入方式待定:A.trait 解耦(df-ideas 定义 IdeaAnalyzer trait,推荐)/ B.df-ideas 直接依赖 df-ai / C.closure。属架构决策非直接做 (06-14)
 - [x] T-260614-04 — ~~路径校验根治~~ ✅ 已完成(resolve_workspace_path 加 canonicalize 防 symlink 逃逸 + 词法 starts_with 兜底;仅校验、返回词法路径保持前端友好;cargo check 0 err / 22 test pass)(06-14)
 - [ ] F-260614-04 — 多 Provider 负载均衡池 — 备用模型/多账号聚合,全局容量=min(各 provider 上限之和, global_cap) (06-14)
@@ -579,10 +579,10 @@
 - [x] T-260614-09 — ~~idea.rs 物理删不级联~~ ✅ WF-E 完成(idea.rs:149 delete→purge_with_descendants,1 行,签名兼容)(06-14, commit 89da9fa)
 - [x] T-260614-10 — ~~findBinding canonicalize~~ ✅ WF-E 判定已解决(normalize_path 已含 canonicalize 优先 + 词法回退,find_binding_conflict 两端对称已用,无需重复加;零改动)(06-14, commit 89da9fa)
 - [ ] F-260614-07 — **[架构前置]** df-ai-core trait 下沉拆 crate — F-03 注入方式前置(决策记录已收敛:原 F-03 的 A/B/C 选型作废,统一为全局 AI trait 下沉独立 crate,df-ideas/df-nodes 依赖 trait 而非 df-ai 具体 impl)。解锁 F-03 — source:功能决策记录 (06-14)
-- [ ] F-260614-08 — 决策治理产品化 — 活契约/规格契约自检机制产品化为可操作功能(当前散落文档机制) — source:功能决策记录缺口
-- [ ] F-260614-09 — 项目 status 字段治理 — status 状态机规范化(planning/active/archived 等枚举约束) — source:功能决策记录缺口
-- [ ] F-260614-10 — 知识库 MCP Server + Tier 2/3 — 对外 MCP 暴露 + 分层存储(当前仅 Tier 1 全栈) — source:PROGRESS Sprint15
-- [ ] T-260614-11 — 全局#6 条件表达式引擎升级 — df-workflow conditions 仅支持 true/false 字面量,升级为真表达式求值 — source:PROGRESS 全局问题
+- [x] ⏸️(待决策.md已决c暂缓·2026-06-16) F-260614-08 — 决策治理产品化 — 活契约/规格契约自检机制产品化为可操作功能(当前散落文档机制) — source:功能决策记录缺口
+- [x] ⏸️(待决策.md已决c暂缓·2026-06-16) F-260614-09 — 项目 status 字段治理 — status 状态机规范化(planning/active/archived 等枚举约束) — source:功能决策记录缺口
+- [x] ⏸️(待决策.md已决c暂缓·2026-06-16) F-260614-10 — 知识库 MCP Server + Tier 2/3 — 对外 MCP 暴露 + 分层存储(当前仅 Tier 1 全栈) — source:PROGRESS Sprint15
+- [x] ⏸️(待决策.md已决暂缓·2026-06-16) T-260614-11 — 全局#6 条件表达式引擎升级 — df-workflow conditions 仅支持 true/false 字面量,升级为真表达式求值 — source:PROGRESS 全局问题
 - [x] T-260614-12 — ~~df-ideas 死代码~~ ✅ WF-E 部分完成(capture.rs 删 CaptureInput/IdeaCapture 死码,保留 Idea/IdeaScores 共享实体;promotion/scoring/adversarial 内"两套 Recommendation/PromotionPolicy 死枚举"嫌疑 agent 未确认存在/保留为对外契约,本次未动,待复查)(06-14, commit 89da9fa)
 - [x] F-260615-01 ✅(2026-06-16,待commit a3cccb070fe9c8821) — **[P1 功能增强]** HumanNode 审批节点支持自定义选项 + 单选/多选类型。现状:config `options: Vec` 已支持任意数量(2/3/…数量扩展已通),但 `decision` 是单 `String` 仅单选语义,`options` 空=自由文本。增强目标:不止「同意/拒绝」二选一,可配置 N 个候选项 + 单选(single)/多选(multiple)两种类型。**改动面**:①`df-core/events.rs` `WorkflowEvent::HumanApprovalRequest` 加 `select_type`、`HumanApprovalResponse` decision 单值→多值(`decisions: Vec` 或保留 decision 兼容 + 加 decisions)②`human_node.rs` config 解析 `select_type` + 校验(多选时每项 ∈ options,可加 min/max 选中数约束)③IPC `approve_human_approval` 签名 ④前端 `stores/project.ts` approve + `api/types.ts` 事件类型 + 审批弹窗 UI(单选 radio / 多选 checkbox)⑤单测改断言 + 新增多选/超限测。**注意**:向后兼容现 single 调用方,`select_type` 缺省 = single — source:用户需求(06-15),crates/df-nodes/src/human_node.rs + src-tauri/src/commands/workflow.rs + src/stores/project.ts — **✅已实施(a3cccb070fe9c8821,2026-06-16,待commit)**:6 文件契约向后兼容(events.rs SelectType 枚举 Single/Multiple 缺省 Single+HumanApprovalResponse decision+decisions 双字段/human_node.rs 校验 single len==1·multiple len≥1·∈options/workflow.rs IPC 加 decisions+select_type Option 缺省兼容/types.ts/project.ts approve/ProjectDetail.vue checkbox 多选 UI)。cargo check 0 err/df-nodes 21 test(含 4 新增多选)/vue-tsc 0 err。主代理核查契约向后兼容 + 6 文件边界
 - [x] F-260615-02 ✅(2026-06-16,待commit) — **[P1 功能]** task 详情查看。✅已实施:get_task_by_id IPC(复用 TaskRepo::get_by_id ok_or_else 转 Result)+lib.rs 注册+taskApi.get+/tasks/:id 路由+TaskDetail.vue(11 字段:标题/描述/状态/优先级/关联项目 router-link 解析名/分支标签/负责人/基础分支/工作流定义/创建更新时间;复用 constants/project 标签+formatDate+watch route.params.id 重载)+Tasks.vue 列表项 @click router.push;TaskRecord TS 类型已存在无需新增;主代理核查 cargo check 0 err/vue-tsc 0 err/git diff 6 文件边界干净。现状:`src/views/Tasks.vue` 仅列表,无独立 TaskDetail 视图/路由(grep 仅 `ProjectDetail.vue` 嵌套任务命中,无独立详情页)。需求:点击 task 查看详情。**数据模型已就绪**(`df-storage/src/models.rs:53` `TaskRecord` 12 字段:id / project_id / title / description / status / priority / branch_name / assignee / workflow_def_id / base_branch / created_at / updated_at)。**改动面**:①新建 `src/views/TaskDetail.vue` 视图 + 路由(`router/index.ts` `/tasks/:id`)②task 详情 IPC(`get_task_by_id`,核对 `commands/task.rs` 现有 IPC 是否已有,无则补)③`Tasks.vue` 列表项点击 → 跳详情 ④详情页字段展示(title/description 渲染、status/priority 标签、关联项目名解析 project_id→name、branch/assignee 信息、时间戳)⑤可选:详情页内编辑(`update_task` IPC 已存在,FR-D6)。— source:用户需求(06-15),src/views/Tasks.vue + src/router/index.ts + src-tauri/src/commands/task.rs
@@ -693,7 +693,7 @@
 - [x] ✅(batch34·2026-06-16·workflow wvr7mdo0p) B-260615-42 [P1] — ~~错误信息不一致~~ 同类错误在不同场景下返回的信息格式不统一。根因:错误处理分散在不同模块,缺乏统一规范。影响:用户和调试者难以理解错误含义。修法:制定错误码规范 + 统一错误格式 + 前端错误码映射 — src-tauri/src/commands/ai/mod.rs + src/api/types.ts
 
 **P2 — 中等问题**
-- [ ] B-260615-43 [P2] — ~~无撤销重做功能~~ AI 操作(如删除项目、修改任务)无撤销功能。根因:缺乏操作历史记录和回滚机制。影响:误操作无法恢复。修法:实现操作历史栈 + 撤销/重做 UI + 关键操作二次确认 — src/stores/project.ts + src/views/ProjectDetail.vue
+- [x] ⏸️(决策c暂缓·2026-06-16) B-260615-43 [P2] — ~~无撤销重做功能~~ AI 操作(如删除项目、修改任务)无撤销功能。根因:缺乏操作历史记录和回滚机制。影响:误操作无法恢复。修法:实现操作历史栈 + 撤销/重做 UI + 关键操作二次确认 — src/stores/project.ts + src/views/ProjectDetail.vue
 - [x] B-260615-44 [P2] — ~~工具响应时间无反馈~~ 工具执行(如 read_file、write_file)长时间无响应时用户不知道卡在哪里。根因:缺乏工具执行进度反馈。修法:添加工具执行进度提示 + 超时警告 + 取消按钮 — src/composables/ai/useAiStream.ts
 - [x] ✅(batch41·2026-06-16·workflow w9vtei15z) B-260615-45 [P2] — ~~内存泄漏隐患~~ 已核查覆盖(useAiEvents stopListener 三 listener+计时器全清 + AiChat onBeforeUnmount + MESSAGE_CAP=200 滚动淘汰),无泄漏不强改 长时间使用后内存占用持续增长。根因:消息缓存、事件监听器未及时清理。修法:优化缓存策略 + 定期清理机制 + 内存使用监控 — src/stores/ai.ts + src/composables/ai/useAiEvents.ts
 
diff --git a/docs/待决策.md b/docs/待决策.md
index fec8768..2af7f5e 100644
--- a/docs/待决策.md
+++ b/docs/待决策.md
@@ -118,6 +118,17 @@
 - **关联**:todo F-260615-14
 - **状态**:✅ 已决(2026-06-16)— **决策:c 不做**。文本拷贝+代码块复制已覆盖核心需求,等用户真实反馈再定。
 
+#### F-260616-11 审批续跑 iteration 计数策略
+- **背景**:`try_continue_agent_loop` 审批后续跑 iteration 从 0 重计(agentic.rs:418 注释,F-260616-03 续跑决策 a 延伸),多次审批(Med/High 工具)=反复重跑 max_iterations 轮,token 消耗不受 max_iterations 严格控制。
+- **决策点**:iteration 累计 vs 重置?
+- **选项**:
+  - a: 累计计数(try_continue 不重置 iteration,跨审批续跑延续累计)
+  - b: 重置 + 独立「审批次数」配额
+  - c: token 总量兜底(不碰 iteration,加 token 硬上限)
+- **推荐**:**a + c**(累计防超配 + token 兜底)
+- **关联**:todo F-260616-11 / F-260616-03
+- **状态**:✅ 已决(2026-06-16)— **决策:a**。累计计数,try_continue 不重置 iteration 跨审批累计,防多次审批 token 失控。token 兜底 c 暂不加(避免过度设计,iteration 累计已控)。实施待 batch60 完成(agentic.rs 锁释放)。
+
 ---
 
 ### ④类 — 设计方向(方案未定,需人定 A/B/C 选型)
@@ -233,7 +244,7 @@
 - **决策点**:「使用」语义?(执行本机 claude 技能?AI 调用?需明确)
 - **推荐**:需先澄清语义(关联 memory aichat-skill-slash-autocomplete)
 - **关联**:todo F-260614-02
-- **状态**:🟡 待澄清语义
+- **状态**:✅ 已决(2026-06-16)— **决策:ai调用**(联想选中技能→AI 调用执行,非执行本机 claude 技能。实施机制待设计:技能内容注入 AI context 或技能注册为 AI 工具,依赖 F-07 trait 下沉)。
 
 #### F-260614-04 多 Provider 负载均衡池
 - **背景**:备用模型/多账号聚合,全局容量=min(各 provider 上限之和, global_cap)。
@@ -282,7 +293,7 @@
 - **选项**: a:维持独立(职责清晰:节点定义vs执行运行时) / b:合并df-nodes(减少crate数)
 - **推荐**:**a**(维持独立,转架构维护记录非清债)
 - **关联**:todo ARC-260615-03
-- **状态**:🟡 待决
+- **状态**:✅ 已决(2026-06-16)— **决策:a**(维持独立,df-execute 非空壳 shell.rs execute() 已完整实现 + 被 run_command 复用,转架构维护记录非清债)。
 
 #### ARC-260615-07 中期技术债7件
 - **背景**:df-core改名df-types / IPC层抽取df-app / AI loop下沉df-ai / 3 view绕store调api / 类型契约ts-rs代码生成 / AiSession多会话 / AppState分组 / IPC命名统一。
@@ -306,7 +317,7 @@
 - **选项**: a:仅AI工具操作(delete_task/delete_project/restore/run_command等) / b:全局(含项目/任务CRUD) / c:暂缓(二次确认已部分覆盖)
 - **推荐**:**c 暂缓**(High-risk工具已有二次确认AE-05,撤销复杂度高)
 - **关联**:todo B-260615-43
-- **状态**:🟡 待决
+- **状态**:✅ 已决(2026-06-16)— **决策:c 暂缓**(High-risk 已有二次确认 AE-05,撤销复杂度高)。
 
 ### C类 — 用户实测/澄清(需用户操作给反馈,主会话无法代办)
 
diff --git a/docs/待审查.md b/docs/待审查.md
index bc716b5..533d7a4 100644
--- a/docs/待审查.md
+++ b/docs/待审查.md
@@ -23,11 +23,35 @@
 
 ## 当前队列
 
-### CR-260616-35 batch59 CR-34-1 守卫 + CR-30 流式重试对齐决策a1 + UX-2025-04 断线保文 — 待审
+### CR-260616-36 batch60 F-09A 隔离补清 + F-13 性能优化 + AE-03 write_file diff 预览(路径B) — ⏳ 待审
+
+- **范围**: 3 agent 合并批(workflow whae812z5,主代独立核查全过 + cargo check --workspace EXIT 0 + vue-tsc EXIT 0)。①**F-09 A 路线**(单例软隔离补漏清字段,不动 B 多会话架构):前端 `useAiConversations.ts:42-45` newConversation 补清 `queue=[]/generatingConvId=null/agentRound=0/searchQuery=''`(防旧会话排队消息带进新会话 + 后台事件错路由 + 侧栏旧搜索过滤);后端 `commands.rs:842-843` ai_conversation_create 补清 `stop_flag.store(false,SeqCst)`(关键:上方生成中分支置 true 停旧 loop 不复位则新会话 loop 启动即见 stop 退出) + `agent_language=None`(防沿用旧语言设置)。②**F-13 性能**:`agentic.rs:173` system_prompt token loop 外算一次缓存为 sys_tokens(整个 loop 不变参数,原每轮+每次重试重算);`:219-225` 外层 messages 构建用 sys_tokens;`:262-270` 重试循环改 `messages: messages.clone()` 复用外层 Vec(删每次重试的 session_arc.lock + build_for_request 全量 clone + estimate_text)。安全前提核验:stream_llm 不接 session_arc + 重试块到 process_tool_calls 间无 push,重试内 session.messages 与外层一致,重建等价复用。③**AE-03 路径 B**(审批 payload 加 diff,非路径 A):`tool_registry.rs:32` generate_diff fn→pub(crate)(复用 F-260615-10 LCS);`audit.rs:508-520` 新增 build_write_file_diff(从 args 取 path+content → 预读旧文件 tokio::fs::read_to_string → 无变化/旧文件缺失/读失败均 None → generate_diff);`audit.rs:588-609` 仅 write_file draft 挂起审批前预计算 approval_diff 注入 PendingApproval.diff(:600 clone)+AiApprovalRequired.diff(:609 move);恢复路径 diff=None(文件可能已变);`mod.rs:101/269` AiApprovalRequired+PendingApproval 加 `diff: Option`;`types.ts:198` AiApprovalRequired 加 `diff?: string`;`useAiEvents.ts` event.diff 传 toolCall 信息;`ToolCard.vue:40-41` 模板 `v-if="tc.diff"` diff 块 + :394 diffLines computed(按 +/-/space 前缀拆分)+ :677-705 样式(add 绿 del 红 ctx 灰 + token 复用)。
+- **维度**: ①F-09 单例字段补清完整性(stop_flag 复位是否覆盖所有生成中→新建路径/agent_language 影响面) + B 路线边界(确认未误动 AiSession 单例) ②F-13 行为不变性核验(sys_tokens 缓存是否真不变/messages.clone() 复用是否等价重建/stream_llm 不持 session_arc 前提是否成立) + 锁持有期缩短收益确认 ③AE-03 路径 B 安全性(预读旧文件只读不改/审批拒绝不执行 handler/路径不校验风险——恶意路径读失败仅 None) + diff 注入完整性(实时挂起+恢复路径双覆盖/前端消费链 event→toolCall→模板) + generate_diff pub(crate) 暴露面。
+- **commit**: 待提交(累积 ~12 文件攒批 + 4 决策回填)。
+
+### CR-260616-37 F-11 审批续跑 iteration 累计计数(跨审批不重置) — ⏳ 待审
+
+- **范围**: F-260616-11 决策a 落地(与 batch60 三项合批 commit)。`mod.rs:215/230` AiSession 加 `iteration_used:usize` 字段+new() init 0;`agentic.rs:114` run_agentic_loop 加 `start_iteration:usize` 参数 +`:176` loop 边界改 `start_iteration..max_iterations` +`:210` 一致性校验块更新 `session.iteration_used=iteration+1` +`:578` try_continue_agent_loop 加 start_iteration 参数 +`:675` spawn 透传 +`:499-501` 注释区分两路径;`commands.rs:55/152/412` ai_chat_send/ai_regenerate/ai_edit_last reset iteration_used=0(新生命周期)+`:260-262` ai_approve 拒绝续跑读 session.iteration_used 累计传 +`:313-315` ai_approve 通过续跑累计传 +`:601` ai_continue_loop reset iteration_used=0 传 0(F-03 决策a 达max重计区分)。两路径严格区分:审批续跑(ai_approve 累计透传 iteration_used) vs 达max续跑(ai_continue_loop reset+传0,F-03 决策a 用户授权重来) vs 新消息(reset 0)。
+- **维度**: ①iteration_used 字段生命周期完备性(所有会话生命周期入口 reset 覆盖——create/send/regenerate/edit_last/continue_loop 是否齐全无遗漏路径) ②start_iteration 透传链完整(agentic run_agentic_loop←try_continue_agent_loop←ai_approve 两个调用点,无断链) ③两路径区分正确性(ai_approve 累计 vs ai_continue_loop 重计,F-11/F-03 决策 a 分别归属无混淆) ④边界 case(start≥max loop 空区间→converged=false→AiMaxRoundsReached,防无限审批烧 token,符合 F-11 语义) ⑤并发安全(iteration_used 在一致性 lock 块更新,与现有 lock 模式一致)。
+- **commit**: 待提交(同 CR-36 合批)。
+- **主代独立核查**: ✅ 全过(useAiConversations:42-45 补清 / commands:842-843 stop_flag.store(false)+agent_language=None / agentic:173 sys_tokens loop 外缓存 + :219-225 外层用 + :262-270 重试 messages.clone() 复用 + 保文退避逻辑未动(Partial 不重试/retry_deadline 30s) / tool_registry:32 generate_diff pub(crate) / audit:508-520 build_write_file_diff 预读+无变化/缺失 None+复用 generate_diff / audit:588-609 仅 write_file 生成 diff 注入 PendingApproval(clone)+AiApprovalRequired(move) / mod:101+269 diff 字段 / types:198 diff? / ToolCard:40-41 模板 + :394 diffLines + :677-705 样式 / cargo+vue-tsc 双 EXIT 0)。
+- **审查 agent 待复审重点**: ①F-13 重试 messages.clone() 复用等价性——核验 stream_llm 签名(stream_recv.rs:137 不接 session_arc) + 重试块到 process_tool_calls 间确实无 session.messages.push(若遗漏则重试复用旧 messages 致 tool_calls 丢失) ②F-13 sys_tokens 缓存——核验 system_prompt 确为 run_agentic_loop 不变参数(整个 loop 期间无 mutate) ③AE-03 build_write_file_diff 安全——预读旧文件 `tokio::fs::read_to_string(path)` 路径未校验(恶意 path 读失败仅 None 不产生危害,但核验无 symlink 逃逸读敏感文件风险——审批只读,write_file handler 自身 validate_path 执行时兜底) ④AE-03 diff 注入双路径——实时挂起(audit:588-609 有 diff)+恢复路径(PendingApproval diff 字段是否持久化,若仅内存则重启恢复审批无 diff) ⑤文件锁独立性——3 agent 改动文件无重叠(F-09: useAiConversations/commands / F-13: agentic / AE-03: mod/tool_registry/audit/types/useAiEvents/ToolCard),useAiEvents.ts Agent C 独占(A/B 未碰)确认无冲突。
+
+---
+
+### CR-260616-35 batch59 CR-34-1 守卫 + CR-30 流式重试对齐决策a1 + UX-2025-04 断线保文 — ✅ 已审(PASS)
 
 - **范围**: 3 agent 合并批(workflow wdlt0xfl6,主代独立核查全过)。①**CR-34-1** useAiPanel.ts:102 守卫加有效性检查 `|| !list.some(p=>p.id===activeProvider)`(deleted provider 残留回落 DB is_default) ②**CR-30-1/CR-30-2 + UX-2025-04 合并**:stream_recv.rs `StreamResult` enum(Complete/Partial/InitFailed 三分支) + `classify_status_or_class`(4xx Fatal/5xx+429+timeout Retryable 镜像 retry::is_status_retryable) + agentic.rs 重试对齐决策 a1(Init 失败重试复用 retry::backoff_delay 1s→2s→4s±20%jitter + 30s 总预算 retry_deadline + Fatal 分类 4xx 立即放弃; MidStream Partial 不重试保文→入库 + AiCompleted(incomplete=true) + 系统提示「响应因网络中断不完整」) + 删 3 处误导注释(agentic.rs:12/commands.rs/state.rs) + retry.rs `pub mod` + `backoff_delay`/`is_status_retryable` 改 `pub` 暴露供 src-tauri 复用 + useAiEvents.ts:177 补 `case 'AiStreamRetry'`(消除死事件穿透) + AiCompleted case 处理 incomplete + types.ts:204/mod.rs `incomplete?:boolean` 字段(所有 emit 点补 None 向后兼容) + i18n aiStreamRetry/responseIncomplete 双语。
 - **维度**: 流式核心中风险(重试循环正确性/保文语义/跨 crate pub 暴露影响面) + 前端事件消费完整性 + 决策 a1 对齐度(Init 可重试 ≤max_retries+jitter+30s+Fatal, MidStream 不重试) + B-260615-05 保留(空文本无 finished 仍 InitFailed 交重试)。
 - **commit**: 待提交(累积 ~30 文件攒批)。
+- **复审结论(2026-06-16·CR-30 agent 亲跑 cargo + 主线程 CR-34-1 预核验)**: ✅ **PASS** — 🔴0 🟡0 ⚪0
+  - **验证**: `cargo check --workspace` EXIT 0(5 warning 全 pre-existing dead_code)/ `cargo test -p df-ai retry` **6 passed**(backoff_is_monotonic_within_jitter/status_classification/fatal_aborts_immediately/first_attempt_ok/retry_once_then_ok/all_retryable_exhausts)/ `cargo test stream_recv` **20 passed**(classify_429_retryable/classify_4xx_fatal 等)。
+  - **CR-30-1(7 项全 PASS)**:agentic.rs:320 真用 `retry::backoff_delay(retry_attempt+1)`(非自写,旧 `1<p.id===activeProvider)` 残留无效回落 DB is_default,cycleProvider 不受影响(findIndex 不再 -1 跳过 ps[0])。
+  - **UX-2025-04 断线保文**:核心 ✅(MidStream Partial 保文入库 + incomplete 系统提示);「继续生成」按钮未做(可复用 UX-2025-03 重试能力按需补,非阻断)。
+  - **待修项回流 todo**: **无**(CR-34-1/30-1/30-2 三条确认修复,todo 勾完成)。
 - **主代独立核查**: ✅ 全过(useAiPanel:102 守卫 / stream_recv:97 enum 三分支 + 注释 / agentic:242 retry_deadline + :274 Partial 不重试 + :286 InitFailed 重试 + :320 backoff_delay 复用 + :360 if incomplete 保文入库 + :400 emit incomplete:Some(true) / useAiEvents:177 AiStreamRetry case / types:204 incomplete / retry.rs:62+74 pub + agentic:14 `use df_ai::retry` 跨 crate 通)。
 - **审查 agent 待复审重点**: ①stream_recv StreamResult 三分支边界(Partial vs InitFailed 空文本路径,B-260615-05 保留点) ②agentic 重试循环 permit RAII 释放/边界(max_retries=0/guard.reset) ③跨 crate retry pub 暴露是否过度(retry_with_backoff 仍 pub(crate)) ④AiCompleted incomplete 前端消费完整性(系统提示气泡镜像后端) ⑤UX-2025-04「继续生成」按钮**未做**(保文核心 ✅,按钮可复用 UX-2025-03 重试能力或按需补)。
 
diff --git a/src-tauri/src/commands/ai/agentic.rs b/src-tauri/src/commands/ai/agentic.rs
index 33190b9..adb315f 100644
--- a/src-tauri/src/commands/ai/agentic.rs
+++ b/src-tauri/src/commands/ai/agentic.rs
@@ -111,6 +111,7 @@ pub(crate) async fn run_agentic_loop(
     llm_concurrency: LlmConcurrency,
     max_iterations: usize,
     max_retries: usize,
+    start_iteration: usize,
 ) {
     // B-260615-09: generating 状态由 RAII guard 收敛复位(正常 exit 显式 reset;panic/异常 Drop 兜底)
     let mut guard = GeneratingGuard::new(session_arc.clone());
@@ -168,7 +169,11 @@ pub(crate) async fn run_agentic_loop(
     // 区分"正常收敛退出"与"达 MAX 被截断退出"——后者末轮 tool_calls 仍非空(tool_result 不再回传 LLM),属异常
     let mut converged = false;
 
-    for iteration in 0..max_iterations {
+    // F-260616-13: system_prompt 是 run_agentic_loop 的不变参数(整个 loop 期间文本不变),
+    // 其 token 估算在 loop 外算一次缓存复用,避免每轮/每次重试重复 estimate_text(低收益优化,行为不变)。
+    let sys_tokens = TokenEstimator::default().estimate_text(&system_prompt);
+
+    for iteration in start_iteration..max_iterations {
         // 用户请求停止 → 收尾退出(已生成文本已在上一轮入库)
         if stop_flag.load(Ordering::SeqCst) {
             let usage = df_ai::provider::TokenUsage {
@@ -190,7 +195,7 @@ pub(crate) async fn run_agentic_loop(
         // 用户新建/切换对话后 active_conversation_id 变更,本 loop(conv_id 快照)成陈旧,
         // 继续跑会往新对话 push 消息/pending 造成污染。检测到即退出(guard Drop 复位 generating)。
         {
-            let session = session_arc.lock().await;
+            let mut session = session_arc.lock().await;
             if session.active_conversation_id.as_deref() != Some(conv_id.as_str()) {
                 tracing::warn!(
                     stale_conv = %conv_id,
@@ -199,6 +204,10 @@ pub(crate) async fn run_agentic_loop(
                 );
                 return;
             }
+            // F-260616-11 决策 a: 累计 iteration 计数(「下一轮起算值」= 当前轮+1)。
+            // 审批等待/达 max 暂停退出时,本字段即「当前轮+1」;审批续跑 ai_approve 读此值作
+            // start_iteration 透传,实现 iteration 累计不重置(防多次审批反复跑满 max 致 token 失控)。
+            session.iteration_used = iteration + 1;
         }
 
         // 新一轮通知前端(第二轮起),前端需新建 assistant 消息
@@ -210,9 +219,10 @@ pub(crate) async fn run_agentic_loop(
         }
 
         // 构建请求消息(超预算时自动裁剪旧消息,保护工具调用三元组 + 最近 6 条)
+        // F-260616-13: sys_tokens 已在 loop 外缓存;本块构建的 messages 在本轮重试循环中复用
+        // (本轮 stream_llm 不持 session_arc、不改 messages,重试无 push 发生,重建等价于复用)。
         let messages = {
             let session = session_arc.lock().await;
-            let sys_tokens = TokenEstimator::default().estimate_text(&system_prompt);
             let (history_msgs, _trimmed) = session.messages.build_for_request(sys_tokens);
             let mut msgs = vec![ChatMessage::system(&system_prompt)];
             msgs.extend(history_msgs);
@@ -248,16 +258,15 @@ pub(crate) async fn run_agentic_loop(
 
             for retry_attempt in 0..=max_retries {
                 // 每次重试重建 request(CompletionRequest 无状态,但 provider.stream() 内部可能消费 body)
+                //
+                // F-260616-13: messages 复用本轮外层构建的快照(不再持 session_arc.lock() 重建)。
+                // 安全前提:本轮 stream_llm 不接收 session_arc、不 push 消息;重试期间 tool 执行
+                // (process_tool_calls)在重试块之后,故重试内 session.messages 必然与外层构建时
+                // 一致,重建与复用等价。收益:省去每次重试的 lock() + build_for_request(含
+                // all_messages_clone 全量 clone)。
                 let retry_request = CompletionRequest {
                     model: provider_config.default_model.clone(),
-                    messages: {
-                        let session = session_arc.lock().await;
-                        let sys_tokens = TokenEstimator::default().estimate_text(&system_prompt);
-                        let (history_msgs, _trimmed) = session.messages.build_for_request(sys_tokens);
-                        let mut msgs = vec![ChatMessage::system(&system_prompt)];
-                        msgs.extend(history_msgs);
-                        msgs
-                    },
+                    messages: messages.clone(),
                     temperature: Some(0.7),
                     max_tokens: Some(8192),
                     stream: true,
@@ -487,8 +496,9 @@ pub(crate) async fn run_agentic_loop(
     // 达 MAX 未收敛(LLM 末轮仍想调工具被截断,末轮 tool_result 不再回传 LLM):转入暂停态询问用户
     // F-260616-03:不再 emit AiError + 走完成流程,改为 emit AiMaxRoundsReached + 保持 generating=true
     // (仿审批等待 L313-316),等用户点继续(ai_continue_loop → try_continue_agent_loop 再跑 max_iterations 轮)
-    // 或点停止(ai_stop_loop → 走完成流程)。try_continue 重新 spawn run_agentic_loop,iteration 从 0 重计,
-    // 故续跑天然再跑 max_iterations 轮(决策 a),无需 reset 任何计数器。
+    // 或点停止(ai_stop_loop → 走完成流程)。try_continue 续跑 iteration 由调用方传 start_iteration 决定:
+    // 审批续跑累计(F-260616-11 决策 a,防多次审批反复跑满 max 致 token 失控,传 session.iteration_used),
+    // 达 max 续跑重计(F-260616-03 决策 a,用户点继续=授权重来,传 0 + 重置 iteration_used)。
     if !converged {
         tracing::warn!(
             conv_id = %conv_id,
@@ -565,7 +575,7 @@ pub(crate) async fn run_agentic_loop(
 /// ai_chat_stop/clear/switch 并发改写,属竞态耦合。has_pending=false(全部审批已处理,续生成)
 /// 分支:审批已被 remove,改为以剩余 pending_approvals 任一 conversation_id 做一致性校验
 /// (此处空,校验通过即沿用全局值,该期 generating=true 且 switch 为 readonly 不并发)。
-pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState) {
+pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState, start_iteration: usize) {
     let (is_generating, has_pending, pending_conv_id) = {
         let session = state.ai_session.lock().await;
         // pending_approvals 中任一审批的 conversation_id:审批等待态(has_pending)下作为 conv_id 来源,
@@ -662,6 +672,6 @@ pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState) {
     });
 
     tauri::async_runtime::spawn(async move {
-        run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries).await;
+        run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, start_iteration).await;
     });
 }
diff --git a/src-tauri/src/commands/ai/audit.rs b/src-tauri/src/commands/ai/audit.rs
index 6821f1c..561ba34 100644
--- a/src-tauri/src/commands/ai/audit.rs
+++ b/src-tauri/src/commands/ai/audit.rs
@@ -19,6 +19,7 @@ use crate::state::AppState;
 use crate::commands::{err_str, now_millis};
 
 use super::{AiChatEvent, AiSession, PendingApproval, ToolCallDraft};
+use super::tool_registry::generate_diff;
 
 /// RiskLevel → 审计记录字符串(low/medium/high)
 pub(crate) fn risk_str(r: RiskLevel) -> &'static str {
@@ -274,6 +275,11 @@ pub async fn restore_pending_approvals(state: &AppState) {
                 risk_level: risk,
                 conversation_id: rec.conversation_id,
                 recovered: true,
+                // AE-2025-03: 重启恢复的审批不重读旧文件——审批可能跨重启,
+                // 期间文件可能已被外部改动,重读生成 diff 反映的不是当初决策时的状态,
+                // 且恢复路径在 session.lock 内做 async IO 复杂度高,预览价值低。
+                // 前端见 diff=None 时回退显新 content。
+                diff: None,
             },
         );
     }
@@ -491,6 +497,28 @@ fn sort_object_keys(v: &mut serde_json::Value) {
     }
 }
 
+/// AE-2025-03(路径 B):write_file 审批预览 diff 生成。
+///
+/// 从 write_file args 取 path(旧文件路径)+ content(新内容),
+/// 预读旧文件 → 复用 `generate_diff`(F-260615-10 LCS 行级 diff)注入审批事件。
+/// 旧文件不存在(新建)/ 读失败 / args 缺字段 → None(前端回退显新 content)。
+///
+/// **仅读不改**:审批未通过前不动文件;读路径不校验(write_file handler 自身会校验
+/// validate_path,审批拒绝则 handler 不执行,恶意路径读失败仅得到 None 不产生危害)。
+async fn build_write_file_diff(args: &serde_json::Value) -> Option {
+    let path = args.get("path")?.as_str()?;
+    let new_content = args.get("content")?.as_str()?;
+    match tokio::fs::read_to_string(path).await {
+        Ok(old_content) => {
+            if old_content == new_content {
+                return None; // 无变化不生成 diff(理论上 write_file 不会如此,容错)
+            }
+            Some(generate_diff(&old_content, new_content))
+        }
+        Err(_) => None, // 文件不存在(新建)/ 无读权限 → 无 diff,前端显新 content
+    }
+}
+
 /// 处理流式接收的工具调用:Low 风险并行执行(join_all),Med/High 进审批门控
 /// 返回待审批的工具数量(0 = 全部自动执行完成)
 pub(crate) async fn process_tool_calls(
@@ -553,6 +581,15 @@ pub(crate) async fn process_tool_calls(
                     }
                 }
                 pending_count += 1;
+                // AE-2025-03(路径 B):write_file 挂起审批前预读旧文件生成 diff。
+                // 仅 write_file(覆盖整文件,有完整新旧内容可对比);其他工具 diff=None。
+                // 旧文件不存在(新建)→ diff=None,前端回退显新 content。
+                // 读失败不阻断审批(容错:文件无读权限等极端情况降级为无 diff 预览)。
+                let approval_diff: Option = if draft.name == "write_file" {
+                    build_write_file_diff(&args).await
+                } else {
+                    None
+                };
                 session.pending_approvals.insert(draft.id.clone(), PendingApproval {
                     tool_call_id: draft.id.clone(),
                     tool_name: draft.name.clone(),
@@ -560,6 +597,7 @@ pub(crate) async fn process_tool_calls(
                     risk_level,
                     conversation_id: Some(conv_id.to_string()),
                     recovered: false,
+                    diff: approval_diff.clone(),
                 });
                 session.messages.push(ChatMessage::tool_result(&draft.id, "需要用户审批,等待确认"));
                 let reason = build_approval_reason(&draft.name, &args, risk_level, db).await;
@@ -568,6 +606,7 @@ pub(crate) async fn process_tool_calls(
                     name: draft.name.clone(),
                     args: args.clone(),
                     reason,
+                    diff: approval_diff,
                     conversation_id: Some(conv_id.to_string()),
                 });
                 audit_tool_call(&audit_repo, conv_id, &draft.id, &draft.name, &draft.args, "pending", risk_level, None, None).await;
diff --git a/src-tauri/src/commands/ai/commands.rs b/src-tauri/src/commands/ai/commands.rs
index be8692f..96cd298 100644
--- a/src-tauri/src/commands/ai/commands.rs
+++ b/src-tauri/src/commands/ai/commands.rs
@@ -51,6 +51,8 @@ pub async fn ai_regenerate(
         session.generating = true;
         session.stop_flag.store(false, Ordering::SeqCst);
         session.agent_language = language.clone();
+        // F-260616-11: 重生成 = 新生命周期起点,iteration 从头计数。
+        session.iteration_used = 0;
         let popped = session.messages.pop_last_assistant_round();
         if !popped {
             // 历史末尾无 AI 回复可弹(空对话/末尾是 user 错误态等),复位 generating 报错
@@ -105,7 +107,7 @@ pub async fn ai_regenerate(
     let max_retries = state.agent_max_retries.load(Ordering::SeqCst);
 
     tauri::async_runtime::spawn(async move {
-        run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries).await;
+        run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, 0).await;
     });
 
     Ok("ok".to_string())
@@ -146,6 +148,8 @@ pub async fn ai_chat_send(
         session.generating = true;
         session.stop_flag.store(false, Ordering::SeqCst);
         session.agent_language = language.clone();
+        // F-260616-11: 新对话生命周期 iteration 从头计数(累计计数器复位)。
+        session.iteration_used = 0;
         session.messages.push(ChatMessage::user(&message));
 
         // 首次发送时生成对话 id(懒创建:不立即落库,避免空对话残留;
@@ -201,7 +205,7 @@ pub async fn ai_chat_send(
     let max_retries = state.agent_max_retries.load(Ordering::SeqCst);
 
     tauri::async_runtime::spawn(async move {
-        run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries).await;
+        run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, 0).await;
     });
 
     Ok("ok".to_string())
@@ -251,8 +255,11 @@ pub async fn ai_approve(
         }
         // 审计:拒绝(决策者=human)
         audit_finalize(&state, &tool_call_id, "rejected", None).await;
+        // F-260616-11 决策 a: 审批续跑 iteration 累计(不重置)——读 session.iteration_used 透传
+        // 作 start_iteration,防多次审批反复跑满 max_iterations 致 token 失控。
+        let start_iter = state.ai_session.lock().await.iteration_used;
         // 所有待审批处理完毕后恢复 agentic 循环
-        try_continue_agent_loop(&app, &state).await;
+        try_continue_agent_loop(&app, &state, start_iter).await;
         return Ok("rejected".to_string());
     }
 
@@ -301,8 +308,11 @@ pub async fn ai_approve(
         save_conversation(&state.ai_session, &state.db, cid, None, None).await;
     }
 
+    // F-260616-11 决策 a: 审批续跑 iteration 累计(不重置)——读 session.iteration_used 透传
+    // 作 start_iteration,防多次审批反复跑满 max_iterations 致 token 失控。
+    let start_iter = state.ai_session.lock().await.iteration_used;
     // 所有待审批处理完毕后恢复 agentic 循环(recovered 无 live loop,try_continue 因 generating=false 自然不续)
-    try_continue_agent_loop(&app, &state).await;
+    try_continue_agent_loop(&app, &state, start_iter).await;
 
     Ok("executed".to_string())
 }
@@ -398,6 +408,8 @@ pub async fn ai_chat_edit(
         session.generating = true;
         session.stop_flag.store(false, Ordering::SeqCst);
         session.agent_language = language.clone();
+        // F-260616-11: 编辑重生成 = 新生命周期起点,iteration 从头计数。
+        session.iteration_used = 0;
     }
 
     let _tool_defs = state.ai_tools.tool_definitions();
@@ -453,6 +465,7 @@ pub async fn ai_chat_edit(
             llm_concurrency,
             max_iterations,
             max_retries,
+            0,
         )
         .await;
     });
@@ -560,8 +573,10 @@ pub async fn ai_chat_stop(state: State<'_, AppState>, app: AppHandle) -> Result<
 ///
 /// 场景:run_agentic_loop 达 max_iterations 未收敛 → emit AiMaxRoundsReached + 保持
 /// generating=true 暂停。用户点继续调本命令 → 复位 stop_flag(防上一轮残留致续跑入口即退出)
-/// → 调 try_continue_agent_loop 重新 spawn run_agentic_loop(iteration 从 0 重计,天然再跑
-/// max_iterations 轮,决策 a)。
+/// → 调 try_continue_agent_loop 重新 spawn run_agentic_loop。iteration 从 0 重计(天然再跑
+/// max_iterations 轮,决策 a),F-260616-11 落地后:重置 session.iteration_used=0 + 传
+/// start_iteration=0(达 max 续跑重计);审批续跑(ai_approve)则累计传 session.iteration_used,
+/// 两路径区分见 run_agentic_loop 达 max 分支注释。
 ///
 /// 校验复用 ai_approve 模式:generating 必须为 true(暂停态)+ active_conversation_id 一致性
 /// (防陈旧 loop 续跑污染新对话)。无硬上限防无限续(决策 b:用户主动授权 = 同意烧 token)。
@@ -581,9 +596,12 @@ pub async fn ai_continue_loop(
         }
         // 复位停止信号:暂停态可能因上一轮 stop_flag 残留为 true,续跑 loop 入口会立即退出走完成流程
         session.stop_flag.store(false, Ordering::SeqCst);
+        // F-260616-11 决策 a: 达 max 续跑重计 iteration(F-260616-03 决策 a,用户点继续=授权重来)。
+        // 审批续跑(ai_approve)累计不重置见另路径;本路径 start_iteration 传 0,loop 从头计数。
+        session.iteration_used = 0;
     }
     // 复用审批恢复续 loop 入口(不重写 loop),其内部 spawn run_agentic_loop
-    try_continue_agent_loop(&app, &state).await;
+    try_continue_agent_loop(&app, &state, 0).await;
     Ok("ok".to_string())
 }
 
@@ -836,6 +854,11 @@ pub async fn ai_conversation_create(
     session.active_conv_created_at = Some(now);
     session.messages.clear();
     session.pending_approvals.clear();
+    // F-260616-09(A 路线):补漏清字段维持单例软隔离,解「新建会话上下文残留」。
+    // stop_flag 复位 false:上方生成中分支曾置 true 停旧 loop,不复位则新会话 loop
+    // 启动即见 stop_flag=true 异常退出;agent_language 清空防新会话沿用旧会话语言设置。
+    session.stop_flag.store(false, Ordering::SeqCst);
+    session.agent_language = None;
 
     Ok(serde_json::json!({ "id": id }))
 }
diff --git a/src-tauri/src/commands/ai/mod.rs b/src-tauri/src/commands/ai/mod.rs
index eb30620..85279d4 100644
--- a/src-tauri/src/commands/ai/mod.rs
+++ b/src-tauri/src/commands/ai/mod.rs
@@ -96,7 +96,9 @@ pub enum AiChatEvent {
     /// 工具调用完成
     AiToolCallCompleted { id: String, result: serde_json::Value, conversation_id: Option },
     /// 需要人工审批
-    AiApprovalRequired { id: String, name: String, args: serde_json::Value, reason: String, conversation_id: Option },
+    /// diff(AE-2025-03,路径 B):仅 write_file 注入(旧文件 vs 新内容行级 unified diff),
+    /// 供审批卡即时预览。旧文件不存在(新建)或非 write_file 工具为 None。
+    AiApprovalRequired { id: String, name: String, args: serde_json::Value, reason: String, diff: Option, conversation_id: Option },
     /// 审批结果
     AiApprovalResult { id: String, approved: bool, conversation_id: Option },
     /// AI 响应完成
@@ -199,6 +201,18 @@ pub struct AiSession {
     /// `Arc` 可 Clone(Notify 内部用 Atomic 计数,Clone 等价于 Arc 引用 +1),
     /// 不影响 AiSession 其他字段语义。
     pub notify: Arc,
+    /// agentic loop 累计已跑 iteration 计数(F-260616-11 决策 a)。
+    ///
+    /// **语义**:「下一轮起算值」。run_agentic_loop 每轮写 `iteration + 1`,审批等待/
+    /// 达 max 暂停退出时即「当前轮+1」。续跑时由调用方决定如何接:
+    /// - **审批续跑**(ai_approve):传本字段作 start_iteration(**累计**,防多次审批反复
+    ///   跑满 max_iterations 致 token 失控);
+    /// - **达 max 续跑**(ai_continue_loop):重置本字段=0 + 传 0(**重计**,F-260616-03
+    ///   决策 a,用户点继续=授权重来);
+    /// - **新消息首次发送**(ai_chat_send):重置本字段=0(新对话生命周期从头计数)。
+    ///
+    /// 单例一份:当前 AiSession 是全局单例(F-09 B 多会话架构落地时改 per-conv)。
+    pub iteration_used: usize,
 }
 
 impl AiSession {
@@ -213,6 +227,7 @@ impl AiSession {
             agent_language: None,
             stop_flag: Arc::new(AtomicBool::new(false)),
             notify: Arc::new(tokio::sync::Notify::new()),
+            iteration_used: 0,
         }
     }
 
@@ -261,6 +276,10 @@ pub struct PendingApproval {
     pub conversation_id: Option,
     /// 重启恢复的积压审批:无 live loop 持有 session.messages,审批后不 save(防空 messages 污染老对话)、不续跑
     pub recovered: bool,
+    /// AE-2025-03(路径 B):write_file 的行级 unified diff(旧文件 vs 新内容)。
+    /// 仅挂起审批前预读注入,供前端审批卡预览;旧文件不存在(新建)或非 write_file 为 None。
+    /// recovered 审批(启动重建)无 diff(文件可能已变,重读无意义)。
+    pub diff: Option,
 }
 
 /// 工具调用草稿(流式收集时的临时结构)
diff --git a/src-tauri/src/commands/ai/tool_registry.rs b/src-tauri/src/commands/ai/tool_registry.rs
index 7b669f4..c92eba8 100644
--- a/src-tauri/src/commands/ai/tool_registry.rs
+++ b/src-tauri/src/commands/ai/tool_registry.rs
@@ -26,7 +26,10 @@ const DEFAULT_RUN_COMMAND_TIMEOUT_SECS: u64 = 60;
 /// 生成行级 unified diff(无外部依赖,基于 LCS)。
 /// 仅标 +/- 前缀,不做 hunk header(足够审批卡/审计留痕可读)。
 /// 文件改动通常集中在 old_text/new_text 局部,整体行对比可直观呈现。
-fn generate_diff(old: &str, new: &str) -> String {
+///
+/// AE-2025-03(路径 B):audit.rs 挂起审批前预读旧文件复用此函数生成 diff,
+/// 供前端审批卡即时预览(write_file 审批不再只看裸 content)。故 pub(crate)。
+pub(crate) fn generate_diff(old: &str, new: &str) -> String {
     let a: Vec<&str> = old.lines().collect();
     let b: Vec<&str> = new.lines().collect();
     let (n, m) = (a.len(), b.len());
diff --git a/src/api/types.ts b/src/api/types.ts
index ffb6249..c5e1b8d 100644
--- a/src/api/types.ts
+++ b/src/api/types.ts
@@ -195,7 +195,7 @@ export type AiChatEvent = ({
 } | {
   type: 'AiToolCallCompleted'; id: string; result: unknown
 } | {
-  type: 'AiApprovalRequired'; id: string; name: string; args: unknown; reason: string
+  type: 'AiApprovalRequired'; id: string; name: string; args: unknown; reason: string; diff?: string
 } | {
   type: 'AiApprovalResult'; id: string; approved: boolean
 } | {
@@ -239,6 +239,9 @@ export interface AiToolCallInfo {
   result?: unknown
   /** 审批风险说明(AiApprovalRequired 时填充) */
   reason?: string
+  /** AE-2025-03:write_file 行级 unified diff(旧文件 vs 新内容),审批卡即时预览。
+   *  旧文件不存在(新建)或非 write_file 为 undefined,前端回退显新 content。 */
+  diff?: string
 }
 
 /** 对话列表摘要 */
diff --git a/src/components/ToolCard.vue b/src/components/ToolCard.vue
index 9110d91..b2e17b6 100644
--- a/src/components/ToolCard.vue
+++ b/src/components/ToolCard.vue
@@ -36,6 +36,10 @@
           
         
         
⚠ {{ tc.reason }}
+ +
+
{{ ln.text }}{{ '\n' }}
+