refactor(agent-plan): use session-scoped live float - #1909
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (4)
📝 WalkthroughWalkthroughThis PR changes agent plan handling from persisted assistant ChangesAgent plan float refactor
Estimated code review effort: 4 (Complex) | ~75 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/presenter/agentRuntimePresenter/accumulator.ts`:
- Around line 110-121: The plan branch in accumulator.ts updates trailing
narrative block status but does not mark the state as dirty. In the `plan` case,
after calling `finalizeTrailingPendingNarrativeBlocks(state.blocks)`, set
`state.dirty = true` before updating `state.latestAgentPlanSnapshot`, matching
the behavior in `tool_call_start`, `image_data`, and `error` so the renderer
flushes the finalized block state.
In `@src/shared/types/agent-plan.ts`:
- Around line 62-65: The step extraction in the agent plan parser currently
treats a blank value.step as final and returns null even when value.content has
usable text. Update the logic in the step normalization path to prefer
value.step when it contains non-whitespace text, but fall back to value.content
when step is empty or only spaces, so entries with valid plan text are
preserved. Keep the trimming and null check behavior in the same parsing flow so
the final step value reflects whichever field actually has content.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 74a8cfab-8707-4763-acfc-c70b2f5e0e1a
📒 Files selected for processing (29)
docs/ARCHITECTURE.mddocs/FLOWS.mddocs/architecture/agent-plan-task-refactor/spec.mddocs/features/acp-v1-reliability/spec.mddocs/features/message-delete-confirmation/spec.mdsrc/main/presenter/agentRuntimePresenter/accumulator.tssrc/main/presenter/agentRuntimePresenter/dispatch.tssrc/main/presenter/agentRuntimePresenter/process.tssrc/main/presenter/agentRuntimePresenter/types.tssrc/main/presenter/llmProviderPresenter/acp/acpContentMapper.tssrc/main/presenter/llmProviderPresenter/providers/acpProvider.tssrc/main/routes/debug/createMockChatSession.tssrc/renderer/src/components/chat/messageListItems.tssrc/renderer/src/components/message/MessageBlockPlan.vuesrc/renderer/src/components/message/MessageItemAssistant.vuesrc/renderer/src/composables/message/useMessageWindow.tssrc/renderer/src/composables/useAgentPlanStatus.tssrc/renderer/src/pages/ChatPage.vuesrc/shared/chat/agentPlanBlock.tssrc/shared/types/agent-plan-block.tssrc/shared/types/agent-plan.tstest/main/presenter/agentRuntimePresenter/accumulator.test.tstest/main/presenter/agentRuntimePresenter/dispatch.test.tstest/main/presenter/agentRuntimePresenter/process.test.tstest/main/presenter/llmProviderPresenter/acpContentMapper.test.tstest/main/shared/agentPlanBlock.test.tstest/renderer/components/ChatPage.test.tstest/renderer/components/message/MessageBlockBasics.test.tstest/renderer/components/message/MessageItemAssistant.test.ts
💤 Files with no reviewable changes (7)
- test/main/shared/agentPlanBlock.test.ts
- src/renderer/src/composables/message/useMessageWindow.ts
- src/shared/types/agent-plan-block.ts
- src/renderer/src/components/message/MessageBlockPlan.vue
- src/shared/chat/agentPlanBlock.ts
- test/renderer/components/message/MessageBlockBasics.test.ts
- src/main/routes/debug/createMockChatSession.ts
zerob13
left a comment
There was a problem hiding this comment.
Code Review Summary
这个 PR 将 agent plan 从「实时 float + 持久化正文 block」双轨模式简化为「仅实时 float」单轨模式,参考 Codex 的瞬态进度 UI 设计。整体架构决策合理,实现质量高,但有几个关键细节需要确认。
✅ 优点
- 架构简化彻底:删除了
MessageBlockPlan.vue、agentPlanBlock.ts、agent-plan-block.ts等冗余组件和类型 - 生命周期清晰:session-scoped live plan snapshot,切换会话时保留内存状态但不持久化
- 终态处理完善:
terminalReason覆盖 complete/stop/error/max_steps/aborted 五种终态 - 测试覆盖全面:主进程和渲染进程测试都更新了,覆盖新的 float-only 行为
⚠️ 主要问题
1. Legacy Plan Block 兼容性处理不完整 🔄
export function isRenderableAssistantBlock(block: DisplayAssistantMessageBlock): boolean {
if (block.type === 'plan') {
return false // ✅ 过滤掉旧 plan block
}
// ...
}问题:虽然正确过滤了 type:'plan' block,但 MessageItemAssistant.vue 可能渲染空消息(只有 plan block 的旧消息)。
const filteredBlocks = computed(() => filterRenderableAssistantBlocks(message.content))
// ⚠️ 如果 filteredBlocks 为空,会渲染空的 message card 还是完全隐藏?建议:在 MessageList.vue 或 display message 转换层添加 hasRenderableAssistantBlocks 检查,完全跳过渲染纯 plan 消息。
2. Plan Float Linger 清理时序依赖 ⏱️
const PLAN_FLOAT_CLEAR_DELAY_MS = 1200 // ⚠️ 硬编码延迟
function schedulePlanSnapshotClear(sessionId: string) {
const existingTimer = planSnapshotClearTimers.get(sessionId)
if (existingTimer !== undefined) {
window.clearTimeout(existingTimer)
}
const timer = window.setTimeout(() => {
agentPlanStore.clearPlanSnapshot(sessionId)
planSnapshotClearTimers.delete(sessionId)
}, PLAN_FLOAT_CLEAR_DELAY_MS)
planSnapshotClearTimers.set(sessionId, timer)
}风险:
- 如果用户在 1.2 秒内快速切换回原会话,可能看到 plan 闪现后消失
- 多次快速切换可能累积大量未清理的 timer
建议:
- 切换会话时立即取消所有旧会话的 linger timer
- 或使用
onUnmounted统一清理
3. ACP Content Mapper 的 Plan Event 处理 🎯
case 'plan':
if (streamEvent.plan) {
this.emitPlanUpdate(streamEvent.plan, event.index)
}
break // ✅ 只发 event,不产生 block问题:代码中只有 case 'plan' 分支,但没看到 emitPlanUpdate 实现。需要确认:
- 是否正确映射到
chat.plan.updatedIPC 事件? terminalReason是否正确传递到前端?
建议:补充 emitPlanUpdate 的实现细节到 PR 描述,或添加单测覆盖。
4. Session Switch 不清除 Live Plan 的副作用 💾
// docs/architecture/agent-plan-task-refactor/spec.md
5. session switch 只加载消息历史,不从旧 type:'plan' block 恢复;
但不清除当前 app 运行内已有的 session-scoped live snapshot。场景问题:
- Session A 正在运行,显示 plan float
- 切换到 Session B(无 plan)
- 再切回 Session A
- 预期:恢复 A 的 live plan(如果仍在内存中)
- 实际:可能因为
beginTurn已清除而丢失
watch(
() => props.sessionId,
async (newSessionId, oldSessionId) => {
// ⚠️ 切换时调用 beginTurn,会清除 snapshot
agentPlanStore.beginTurn(newSessionId)
}
)建议:明确 beginTurn 的语义:
- 如果是「清除旧 plan」,则不符合 spec 的「保留 live snapshot」承诺
- 如果是「准备新轮次」,则应该只在实际发送消息时调用,而不是切换会话时
5. Internal Tool Call 标记不一致 🏷️
export function isInternalAssistantToolCallBlock(block: DisplayAssistantMessageBlock): boolean {
return (
block.type === 'tool_call' &&
block.tool_call?.name === UPDATE_PLAN_TOOL_NAME &&
block.extra?.internalTool === true // ⚠️ 三重检查
)
}问题:extra.internalTool 标记依赖 runtime 正确设置,如果某个代码路径遗漏,update_plan 会泄露到正文。
建议:
- 在
dispatch.ts中集中标记所有UPDATE_PLAN_TOOL_NAME为 internal - 添加防御性检查:即使缺少
internalTool标记,也过滤掉UPDATE_PLAN_TOOL_NAME
🐛 潜在 Bug
6. Pending Interaction 与 Plan Float 同时显示的布局冲突
<div v-if="activePendingInteraction && latestPlanSnapshot" class="agent-question-panel">
<AgentProgressFloat :snapshot="latestPlanSnapshot" />
<div class="agent-question-divider" />
<ChatToolInteractionOverlay :interaction="activePendingInteraction" />
</div>风险:如果 plan entries 很长 + question 很复杂,面板可能超出视口高度,导致无法滚动查看完整内容。
建议:为 .agent-question-panel 添加 max-height 和 overflow-y: auto。
7. Message Delete 后的状态同步
// docs/features/message-delete-confirmation/spec.md 更新为:
- Message delete and related recovery flows now consistently reload the current session.但代码中没看到删除消息后清除对应 session 的 live plan 的逻辑。如果删除的是触发 plan 的那条消息,plan float 可能残留。
建议:在 onMessageDelete 成功回调中调用 agentPlanStore.clearPlanSnapshot(sessionId)。
📝 代码质量建议
- Magic Number 提取:
PLAN_FLOAT_CLEAR_DELAY_MS = 1200可以作为用户可配置项(如 settings) - 类型安全增强:
planFloatLingerBySession的Record<string, boolean>应该改为Map<string, boolean>,避免原型链污染 - 测试覆盖补充:
- Session A/B 快速切换时 live plan 的保留行为
- 只有 plan block 的旧消息是否正确隐藏
beginTurn调用时机的准确性
🎨 文档质量
docs/architecture/agent-plan-task-refactor/spec.md 非常清晰,但有两点建议:
- Migration Guide 缺失:没有说明旧数据中的
type:'plan'block 如何处理(保留但隐藏 vs 清理) - 终态流程图:建议补充 plan 终态转换的状态机图,明确 5 种
terminalReason的触发条件
✨ 总体评价
这是一个高质量的架构重构 PR,方向正确,执行彻底。主要风险点在于:
- Session 切换与
beginTurn的时序逻辑(可能违反 spec 承诺) - Legacy plan block 的空消息渲染(用户体验问题)
- Plan float linger timer 的生命周期管理(内存泄漏风险)
建议优先修复问题 1、2、4,其他可以作为后续迭代。代码实现稳健,测试覆盖充分,通过后可以安全合并。 🚀
重点确认项:
- 确认
beginTurn不会在 session switch 时清除 live plan - 验证纯 plan 消息是否正确隐藏(不渲染空 card)
- 测试快速切换 session 时 plan float 的行为
- 确认 message delete 后清除对应 plan snapshot
|
@zerob13 Thanks. Fixed the two actionable issues: the combined plan/interaction panel is now scroll-bounded, and deleting the associated assistant message clears only its matching live plan snapshot.I checked the remaining items as well. The current behavior is intentional and covered by existing tests, so I left those unchanged. |
Summary by CodeRabbit