Skip to content

feat(serve): 调用链路面板展开/折叠 + has_more 截断标记 (#152) - #155

Merged
c2j merged 3 commits into
mainfrom
feat/issue-152
Sep 7, 2026
Merged

c2j merged 3 commits into
mainfrom
feat/issue-152

Conversation

@c2j

@c2j c2j commented Sep 7, 2026

Copy link
Copy Markdown
Owner

概要

Closes #152

serve 模式详情面板的 Callers/Callees 调用链树此前一次性平铺渲染全部层级,且预算截断产生的"假叶子"(children: [])与真叶子在 JSON 中无法区分,复杂链路不可聚焦、截断状态误导。本 PR 实现 issue 的必做 A(折叠交互)与必做 B(has_more 标记),懒加载真分页(可选 D)按 issue 建议暂缓。

后端(507a5dd)

  • TreeNode 新增 has_more: bool + more_count: usize(未展示直接孩子数,精确值)
  • build_tree_dfs 返回签名改为 (Vec<TreeNode>, usize):邻居收集(含 ancestors/builtin 过滤)提前至 early-return 前,预算耗尽、深度钳制、循环 break 三种截断统一折算为 unexpanded,push 时 has_more = unexpanded > 0 — 闭环覆盖且不误报(真叶子/全祖先环均 effective=0
  • 顶层 truncated / caller_count / trace_chain 签名保持兼容
  • HTTP(tree_nodes_to_json)与 MCP(同名副本)同步输出新字段
  • TDD:6 个新测试(Red → Green),构造点唯一,TUI/CLI 消费方零影响

前端(c3d7319)

  • +/ 逐层折叠,默认展开 1 层(codeweb-tree-expand-depth 持久化)
  • expand all / collapse all(清空显式状态 + 调整默认深度,免枚举)
  • 折叠状态键 = currentTraceKey|pathKey 按目标作用域 — 同名节点跨分支不误伤,跨目标不泄漏(code review 修正项)
  • has_more 节点渲染「▾ N more」注解徽章(无连接符样式),点击复用现有 navigateTo 重新获取 500 节点预算深入查看
  • 折叠/展开操作零 fetch(缓存 trace 重渲染 + 保留滚动位置);导航重置顶部
  • localStorage JSON 读取加 try/catch 守卫;trace 请求 depth=50→10 消除被服务端钳制的误导

测试

  • cargo test traverse — 6 个新截断行为测试 + 既有 14 个全过
  • cargo test --features serve / cargo test --features full(跳过 CI 已知环境项)全绿
  • cargo build --features full + clippy --features full -- -D warnings + fmt --check 干净
  • 浏览器手工验收(serve + demo 图):默认折叠/逐节点 toggle 不误触导航/expand·collapse all 及刷新持久化/徽章渲染与深入跳转/跨目标状态隔离/损坏 localStorage 容错

说明

  • assets/app.js 为实际 UI 唯一真相(README 提及的 Cytoscape 画布未落地,与本需求正交)
  • out of scope:懒加载真分页(现有 nodes/:id/callers|callees 分页端点已具备,待实测超限频率再立项)、CLI/TUI 输出的 more 提示

c2j and others added 2 commits September 7, 2026 09:12
build_tree_dfs 返回 (nodes, unexpanded) 闭环:预算耗尽、深度钳制、
循环 break 三种截断统一折算为未展开的有效邻居数,使假叶子与真叶子
在 JSON 中可区分(修复截断误导)。顶层 truncated/caller_count 语义
保持兼容;HTTP 与 MCP 的 trace JSON 同步输出新字段。

测试:新增 6 个截断行为测试(预算精确计数/兄弟截断/深度边界/
真叶子不误报/祖先环防误报/builtin 过滤不计入)。

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
- 树节点 +/- 逐层折叠,默认展开 1 层(expand-depth 偏好持久化)
- expand all / collapse all(清空显式状态 + 调整默认深度)
- 折叠状态按目标节点作用域(currentTraceKey|pathKey),跨目标不泄漏
- has_more 节点渲染「▾ N more」注解徽章,点击复用 navigateTo 深入
- 折叠重渲染零 fetch(缓存 trace + 保留滚动位置),导航重置顶部
- localStorage JSON 读取加守卫,损坏数据不再阻断 UI 初始化
- trace 请求 depth 50→10,消除被服务端钳制的误导

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

@c2j c2j left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — Overall: Approve with minor suggestions

I traced the backend counting logic through all four truncation paths (budget exhausted at a child's recursion entry / budget breaking a sibling loop / depth clamp / genuine leaf) and the has_more/more_count attribution is correct at every level: no double counting, no false positives, and the two filters (ancestors + skip_builtins) are correctly applied before effective is computed so more_count is an exact value, not an upper bound. The six new tests are meaningful and map 1:1 to real behaviors (the budget-exhaustion-at-recursion-entry case is the hardest one and it is precisely locked). The TreeNode struct has a single construction site (traverse.rs:161 — verified by grep), so the additive fields are zero-breakage, and both serializers (HTTP handlers.rs, MCP tools.rs) are updated in sync.

Frontend: the collapse key design (currentTraceKey | section.path-idx) correctly isolates state per trace target AND per tree position, which natively avoids the "duplicate node across branches" trap from issue #152. Toggling is zero-fetch (cached currentDetailData + local re-render with scroll preservation), and the delegated click ordering ensures [+] never triggers navigation. The depth=50→10 change is honest (server was silently clamping anyway). localStorage guards are robust.

Nit 1 (recommend fixing before merge) — expand/collapse-all "default depth" leaks across targets and persists

expandAllTree sets treeExpandDepth = 99 and collapseAllTree sets it to 0, and neither is reset in navigateTo/hideDetail, and both are written to localStorage.

Consequence: after clicking "collapse all" once on node A, opening node B renders B's entire tree at default depth 0 — the top-level callers/callees are all tucked behind [+] and effectively invisible. It survives page reloads too. This contradicts the PR's own "cross-target isolation" goal: the explicit collapse map (treeCollapsed) is correctly scoped per target, but the default depth leaks globally.

Suggested fix: reset treeExpandDepth to 1 on navigateTo/hideDetail (or treat expand/collapse-all as session-only and stop persisting it), so expand/collapse-all acts only on the current target.

Nit 2 (cosmetic, cheap) — leaf placeholder width vs [+]/[-] mismatch

The leaf placeholder is &nbsp; (~1 monospace char + trailing space) while [+]/[-] is 3 chars. For siblings at the same depth where one is expandable and one is a leaf, the node-tag/key columns shift by ~2 chars and rows look ragged. A fixed min-width (e.g. 2ch) + centering on .tree-toggle, or an equal-width placeholder, would keep the columns aligned.

Nit 3 (design, acceptable) — has_more badge hidden while node is collapsed

The if (isCollapsed) continue; skips both children and the "N more" badge, so a truncated fake-leaf at the default collapse depth only shows [+]; the user must expand before seeing "N more — click to open". Acceptable because has_more already makes the node expandable (the [+] itself signals "more below"), but if you want stronger affordance you could always render the badge for nodes with children: [] and has_more: true, independent of collapse state.

Notes (no action needed, confirmed)

  • caller_count/callee_count still report the visible top-level count (smaller than reality under truncation) — pre-existing behavior, not claimed to be fixed here; the global truncated banner still covers it. Could be a follow-up issue.
  • Frontend has no JS test harness and CI skips test_serve_*; manual browser acceptance is consistent with current repo state — acceptable.
  • README Cytoscape mismatch correctly declared out of scope in the PR body.

Overall this is a solid, well-tested implementation of issue #152. Happy to approve once Nit 1 is addressed or confirmed as an intentional global preference.

Review 修正(PR #155):

- expand/collapse-all 不再修改全局默认深度(原实现写 localStorage
  后跨目标泄漏、跨刷新持久,与按目标隔离目标矛盾),改为枚举当前
  树的可展开节点写入作用域键(target|pathKey);默认深度回归常量 1
- .tree-toggle 固定 min-width:3ch + 居中,叶子占位与 [+]/[-] 列对齐
- 无 children 的截断假叶子折叠后仍渲染「N more」徽章,折叠态保留
  深入查看入口;有 children 的节点折叠仍隐藏徽章(行为不变)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@c2j

c2j commented Sep 7, 2026

Copy link
Copy Markdown
Owner Author

感谢细致的 review,四处截断路径的追踪确认很有帮助。三个 nit 已在 92e40bd 全部处理:

Nit 1 — 已修复(采纳方案变体)expandAllTree/collapseAllTree 不再修改全局 treeExpandDepth(该全局键已彻底移除,默认深度回归常量 TREE_DEFAULT_DEPTH = 1),改为 collectExpandableKeys() 枚举当前树的可展开节点、逐个写入按目标作用域的 treeCollapsed 键(target|pathKey)。这样 expand/collapse-all 严格作用于当前目标、经 localStorage 持久化、跨目标零泄漏,也与「导航后状态保持」验收项一致。浏览器回归确认:A 上 collapse all → B 按默认深度渲染 → 回 A 全折叠恢复;干净目标无显式键时严格遵循 depth 规则。

Nit 2 — 已修复.tree-togglemin-width:3ch; text-align:center,同层叶子与可展开节点列对齐。

Nit 3 — 已采纳增强children: [] && has_more: true 的假叶子折叠后仍渲染「▾ N more」徽章([+] 与徽章并存,折叠态保留深入入口);有 children 的节点折叠后徽章保持隐藏,原行为不回归。

Notes 部分无异议:caller_count 可见性语义留作后续 issue。

@c2j
c2j merged commit 00fd106 into main Sep 7, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(serve): 调用链路面板支持 + 展开/折叠,配合 has_more 截断标记解决复杂链路可读性

1 participant