feat(desktop): 追踪 dashboard 侧栏面板 + codex 风格浮层交互 + opencode/nga 路径回退#137
feat(desktop): 追踪 dashboard 侧栏面板 + codex 风格浮层交互 + opencode/nga 路径回退#137wanglongan587 wants to merge 3 commits into
Conversation
4b78a67 to
1309ce7
Compare
wanglongan587
left a comment
There was a problem hiding this comment.
总结:整体方案清晰、ADR 文档到位,locator 文件通道把 agent_session_id 留在后端是个很好的安全设计,纯函数单测也扎实。主要问题集中在「切换会话时的状态陈旧」和「Claude transcript 全量扫描的健壮性/性能」两处,建议优先处理。
值得学习的地方:
- ADR-0002/0003 的设计:磁盘 locator + agent session id 不出后端,避免了把敏感 id 放进 URL。
- write_locator 的原子写 + fsync、config.rs 对 legacy 配置的 serde 兼容测试,都很到位。
- 纯函数拆分干净,单测覆盖好;vite.config 忽略 src-tauri/target 解决 Windows EBUSY 是个实在的修复。
主要问题(按优先级):
- [建议修改] useDashboardEndpoint 切换会话时短暂展示旧 endpoint、无 loading 态(URL 里带着旧 session_id)——见 use-dashboard-endpoint.ts 行内。
- [建议修改] walkdir_files 全量收集 + 任一不可读子目录即整树失败,重度用户每次开面板都全量扫描 ~/.claude/projects——见 dashboard.rs 行内。
- [建议修改] validate_dashboard_endpoint 名实不符(说 loopback 只查非空)+ IPv6 会让 URL 失效——见 config.rs 行内。
- [建议修改] setter 手工逐字段重建 DesktopConfig,新增字段易漏改——见 config.rs 行内。
- [仅供参考] trace/locator 错误被吞成通用文案无 tracing、probe 与命令编排缺单测、file_for 缺路径清洗——见对应行内。
未单独行内列出的小点:
- i18n 有几个未使用的 key(dashboard.close / dashboard.tab.review / dashboard.tab.terminal / dashboard.loadingFrame),若是预留 tab 建议加注释说明。
- PanelResizeHandle 里 {...props} 写在显式 onPointerDown/Move/Up 之后,会把内置事件处理器覆盖掉,建议把 spread 挪到前面。
- iframe sandbox="allow-scripts allow-same-origin ...":当前 dashboard 跨域(127.0.0.1 vs tauri origin),是安全的;但若 #3 把 host 配成 tauri 同源就会变危险,所以 #3 的 loopback 校验也顺带堵住了这条。
- locator 文件按 session 累积、无清理;如不影响可后续加过期清理。
| if (!canResolve) return EMPTY_STATE; | ||
| return { | ||
| endpoint: resolved.endpoint, | ||
| isLoading: resolved.endpoint === null && resolved.error === null, |
There was a problem hiding this comment.
[建议修改] 切换会话时会短暂展示上一个会话的 iframe URL,且不经过 loading 态
第 75 行的 isLoading 只在「从未拿到过 endpoint」时为 true(resolved.endpoint === null)。但 resolved 是跨会话复用的 state,切换 sessionId 时并没有被重置——effect 的 cleanup 只是把旧 promise 标记为 superseded,并没有清空 resolved。
复现:选中会话 A 打开面板(A 解析完成)→ 关闭面板 → 在侧栏选中会话 B → 重新打开面板。此时 canResolve 由 false→true,effect 重新发起 resolve(B),但在 B 解析完成前,hook 返回的 endpoint 仍是 A 的(URL 里 session_id=A),isLoading 为 false,于是 iframe 会带着 A 的 URL 先加载,直到后端 resolve(B) 返回(locator 写入 + 500ms 探测)才切换。对慢机器/慢磁盘这不止是闪一下,是展示了错误会话的数据。
建议二选一:
- 在 resolved 里带上它对应的 sessionId,派生 isLoading 时若 resolved.sessionId !== currentSessionId 也算 loading;
- 或在 effect 开头(sessionId 变化时)setResolved({ endpoint: null, error: null }) 重置,让 loading 态自然出现。
There was a problem hiding this comment.
已修复(commit 795f55a)。
采用了第一个建议:在 resolved 中带上 sessionId,切换会话时若 resolved.sessionId !== currentSessionId 则视为 stale,派生 isLoading=true + endpoint=null,面板自然展示 loading 态,不再短暂加载旧会话的 iframe URL。
这样无需在 effect 里 setState 重置(保持了原有的"不在 effect 里同步 setState"设计),stale 判断完全在派生阶段完成。
| } | ||
|
|
||
| /// Recursively yields files under `root` without depending on an external crate. | ||
| fn walkdir_files(root: &Path) -> std::io::Result<Box<dyn Iterator<Item = PathBuf>>> { |
There was a problem hiding this comment.
[建议修改] walkdir_files 签名承诺懒迭代,实现却是全量收集;且任一不可读子目录会让整棵扫描失败
两个问题:
-
返回类型是 Box<dyn Iterator<Item = PathBuf>>,暗示调用方可以「边遍历边早返回」,但实现先把整棵 ~/.claude/projects 收集进 Vec 再返回迭代器。于是 resolve_claude_code_trace 里的 return Ok(Some(path)) 早返回只省掉了字符串比较,省不掉 I/O。对重度 Claude Code 用户(projects 下可能几千个 .jsonl),每次打开面板都要全量扫一次。
-
std::fs::read_dir(&dir)? 把子目录不可读(权限/被占用)直接传播成 Err,导致整次解析失败、面板报「无法解析 trace」。单个无权限子目录不该让整个面板不可用。
建议:
- 要么把签名改成 Vec(诚实表达 eager),同时改成「遇到不可读子目录就 continue 并 tracing::warn! 一条」;
- 要么真正流式化(递归 generator 或直接引入 walkdir crate),让早返回能省 I/O。
There was a problem hiding this comment.
已修复(commit 795f55a)。
改成了真正惰性的 WalkDirIter(自定义 Iterator 实现),签名仍为 Box<dyn Iterator<Item=PathBuf>> 但现在每次只 read_dir 一层、遇到文件即 yield,调用方的 return Ok(Some(path)) 早返回能真正省 I/O。
不可读子目录改为 tracing::warn!(?dir, ?e, "skipping unreadable directory during trace scan") 后 continue,不再让单个权限问题拖垮整棵扫描。
|
|
||
| /// Rejects ambiguous dashboard endpoints before they become active runtime configuration. | ||
| pub(crate) fn validate_dashboard_endpoint(host: &str, port: u16) -> Result<(), DesktopConfigError> { | ||
| if host.trim().is_empty() { |
There was a problem hiding this comment.
[建议修改] 校验说 loopback address 却只查非空,名实不符;IPv6 loopback 还会让 URL 构造失效
第 235 行只判断了 host.trim().is_empty() 和 port == 0,但错误信息和文档(dashboard_host 注释、ADR-0001)都说是 loopback。目前用户可以把 dashboard_host 配成 0.0.0.0、192.168.x.x、甚至 tauri.localhost——后者会让 iframe 与宿主同源,配合下文 allow-scripts allow-same-origin 会变成不安全的组合。
另外若用户配 ::1(这也是 loopback),dashboard_url / probe_dashboard_server 里的 format!("{host}:{port}") 会生成 ::1:8601 这种非法地址(IPv6 需要 [::1]:8601)。
建议二选一:
- 真正校验 loopback:host.parse::() 后判断 is_loopback(),或限定 127.0.0.1/localhost/::1 白名单;同时在 dashboard_url / probe_dashboard_server 里对 IPv6 加方括号;
- 或把文案改成 non-empty host,与实现一致。
There was a problem hiding this comment.
已修复(commit 795f55a),采用了第一个建议——真正校验 loopback。
is_loopback_host:host.parse::<IpAddr>().is_loopback()或localhost(不区分大小写),其余一律拒绝。- 新增
DesktopConfigError::DashboardHostNotLoopback,error.rs同步映射为dashboard_host_not_loopback。 - 新增
format_host_port辅助函数,IPv6 加方括号([::1]:8601),dashboard_url和probe_dashboard_server均已改用之。 - 补充了测试:
192.168.1.5→DashboardHostNotLoopback;format_host_port("::1", 8601)→"[::1]:8601"。
| let updated = DesktopConfig { | ||
| version: CONFIG_VERSION, | ||
| worktree_root, | ||
| dashboard_host: config.dashboard_host.clone(), |
There was a problem hiding this comment.
[建议修改] setter 里手工逐字段重建 DesktopConfig,新增字段时极易漏改
set_worktree_root(118 行)和 set_dashboard_endpoint 都是用「把所有字段列一遍」的方式构造 updated。这个 PR 正是因为新增了两个 dashboard 字段,才不得不回来给旧的 set_worktree_root 补 dashboard_host.clone() / dashboard_port——下一次再加字段(比如某个 proxy 配置)时,很容易忘掉某一处 setter,导致那个字段被静默重置回默认值,且不会有编译错误。
建议改成结构更新语法,只写要改的字段:
let updated = DesktopConfig {
version: CONFIG_VERSION,
worktree_root,
..config.clone()
};
set_dashboard_endpoint 同理。这样未来加字段只需保证 DesktopConfig: Clone(已 derive),各 setter 自动携带新字段。
There was a problem hiding this comment.
已修复(commit 795f55a)。
set_worktree_root 和 set_dashboard_endpoint 都改成了结构更新语法:
let updated = DesktopConfig {
version: CONFIG_VERSION,
worktree_root,
..config.clone()
};未来新增字段只需保证 DesktopConfig: Clone(已 derive),各 setter 自动携带,不再有漏改风险。
| ) { | ||
| Ok(Some(path)) => Some(path.to_string_lossy().into_owned()), | ||
| Ok(None) => None, | ||
| Err(_) => { |
There was a problem hiding this comment.
[仅供参考] trace 解析与 locator 写入的错误被吞成通用文案,没有 tracing 落盘,排障困难
229 行的 Err(_) => 把 io::Error(权限不足?符号链接环?编码错误?)直接映射成 "failed to scan the agent trace directory";252 行 locator 写入同理。用户看到的是同一个通用文案,无法区分「Claude projects 目录权限不足」和「trace 文件还没生成」。crate 里已经有 tracing,建议在映射前 tracing::warn!(?err, ...) 落一条,至少日志里能区分根因。
There was a problem hiding this comment.
已修复(commit 795f55a)。
trace 解析和 locator 写入的 Err(_) 分支都改成了先 tracing::warn!(?err, ...) 落盘根因,再映射为通用文案。日志里现在能区分"权限不足""符号链接环""trace 未生成"等不同场景。
| } | ||
|
|
||
| /// Returns true when something answers a TCP connect on host:port within a beat. | ||
| async fn probe_dashboard_server(host: &str, port: u16) -> bool { |
There was a problem hiding this comment.
[仅供参考] probe_dashboard_server 与 get_dashboard_url 命令本身没有单测
纯函数(dashboard_url / write_locator / resolve_*_trace)测得很扎实,但 500ms 超时 / 连接拒绝 → server_reachable=false 这条分支,以及命令「写 locator → 探测 → 返回」的编排没有被覆盖。probe_dashboard_server 比较好测:对一个空闲端口 connect 应返回 false,对一个本地 listener 应返回 true。建议补上,避免以后改超时阈值或探测策略时静默回归。
There was a problem hiding this comment.
已修复(commit 795f55a)。
补充了两个 #[tokio::test]:
probe_returns_false_for_closed_port:bind 后立即 drop 得到空闲端口,探测应返回 false。probe_returns_true_for_open_port:本地 listener + 后台 accept,探测应返回 true。
命令编排(get_dashboard_url 的写 locator → 探测 → 返回)依赖完整 DesktopState,mock 成本较高,暂留到后续集成测试迭代。
| } | ||
|
|
||
| /// Returns the locator file path for one Ora session id. | ||
| pub fn file_for(app_data_directory: &Path, ora_session_id: &str) -> PathBuf { |
There was a problem hiding this comment.
[仅供参考] file_for 没有对 ora_session_id 做路径清洗,靠 find_session 间接防穿越
第 61 行 format!("{ora_session_id}.json") 若 ora_session_id 含 .. 或路径分隔符,会写到 dashboard/ 之外。目前安全是因为 get_dashboard_url 在写 locator 前会先调 resolve_session_locator → find_session(mod.rs 465 行)做 DB 精确查询,不存在的 session 直接 NotFound,所以穿越串进不来。但这层防护是隐式的、依赖调用顺序。
建议在 file_for 里显式拒绝含 ..、/ 或反斜杠的 id(或断言为合法 UUID 形态),把约束放在真正使用它的地方,防止未来有人直接调 write_locator 绕过 DB 校验。
There was a problem hiding this comment.
已修复(commit 795f55a)。
file_for 中新增 debug_assert_path_safe(id),显式拒绝含 /、\ 或 .. 的 session id。约束放在真正使用它的地方,防止未来有人直接调 write_locator 绕过 DB 校验。
根据 PR ora-space#137 检视意见修复: 1. useDashboardEndpoint 切换会话时不再展示旧 endpoint: 在 resolved 中追踪 sessionId,切换时旧数据视为 stale, 派生 isLoading=true + endpoint=null,面板展示 loading 态。 2. walkdir_files 改为真正惰性迭代器(WalkDirIter), 早返回可省 I/O;不可读子目录 tracing::warn! 后跳过, 不再让单个权限问题拖垮整棵扫描。 3. validate_dashboard_endpoint 真正校验 loopback (IpAddr::is_loopback() 或 localhost),拒绝非回环地址, 堵住 tauri.localhost 同源 + allow-scripts 的沙箱风险; 新增 DashboardHostNotLoopback 错误变体。 4. config setters 改用结构更新语法(..config.clone()), 未来新增字段自动携带,不再逐字段手工重建。 5. dashboard.rs 错误映射前加 tracing::warn! 落盘根因, 区分权限不足 / trace 未生成等不同场景。 6. probe_dashboard_server 补充单测(空闲端口→false, 本地 listener→true);format_host_port 对 IPv6 加方括号, 修复 ::1:8601 非法地址。 7. file_for 加 debug_assert_path_safe 路径清洗, 显式拒绝含 / \ .. 的 session id(纵深防御)。 8. PanelResizeHandle 的 {...props} 移到显式事件处理器之前, 防止外部 onPointerDown 覆盖内部拖拽逻辑。 9. 清理未使用的 i18n key(dashboard.close / tab.review / tab.terminal / loadingFrame)。
根据 PR ora-space#137 检视意见修复: 1. useDashboardEndpoint 切换会话时不再展示旧 endpoint: 在 resolved 中追踪 sessionId,切换时旧数据视为 stale, 派生 isLoading=true + endpoint=null,面板展示 loading 态。 2. walkdir_files 改为真正惰性迭代器(WalkDirIter), 早返回可省 I/O;不可读子目录 tracing::warn! 后跳过, 不再让单个权限问题拖垮整棵扫描。 3. validate_dashboard_endpoint 真正校验 loopback (IpAddr::is_loopback() 或 localhost),拒绝非回环地址, 堵住 tauri.localhost 同源 + allow-scripts 的沙箱风险; 新增 DashboardHostNotLoopback 错误变体。 4. config setters 改用结构更新语法(..config.clone()), 未来新增字段自动携带,不再逐字段手工重建。 5. dashboard.rs 错误映射前加 tracing::warn! 落盘根因, 区分权限不足 / trace 未生成等不同场景。 6. probe_dashboard_server 补充单测(空闲端口→false, 本地 listener→true);format_host_port 对 IPv6 加方括号, 修复 ::1:8601 非法地址。 7. file_for 加 debug_assert_path_safe 路径清洗, 显式拒绝含 / \ .. 的 session id(纵深防御)。 8. PanelResizeHandle 的 {...props} 移到显式事件处理器之前, 防止外部 onPointerDown 覆盖内部拖拽逻辑。 9. 清理未使用的 i18n key(dashboard.close / tab.review / tab.terminal / loadingFrame)。
333f733 to
795f55a
Compare
|
感谢详细的检视意见,已全部处理(commit 行内 7 条已逐条回复,总结中的小点也一并修了:
测试全绿:Rust 22/22(新增 4 个:probe true/false、IPv6 URL、format_host_port),前端 tsc/eslint/vitest 4/4 全过。 |
根据 PR ora-space#137 检视意见修复: 1. useDashboardEndpoint 切换会话时不再展示旧 endpoint: 在 resolved 中追踪 sessionId,切换时旧数据视为 stale, 派生 isLoading=true + endpoint=null,面板展示 loading 态。 2. walkdir_files 改为真正惰性迭代器(WalkDirIter), 早返回可省 I/O;不可读子目录 tracing::warn! 后跳过, 不再让单个权限问题拖垮整棵扫描。 3. validate_dashboard_endpoint 真正校验 loopback (IpAddr::is_loopback() 或 localhost),拒绝非回环地址, 堵住 tauri.localhost 同源 + allow-scripts 的沙箱风险; 新增 DashboardHostNotLoopback 错误变体。 4. config setters 改用结构更新语法(..config.clone()), 未来新增字段自动携带,不再逐字段手工重建。 5. dashboard.rs 错误映射前加 tracing::warn! 落盘根因, 区分权限不足 / trace 未生成等不同场景。 6. probe_dashboard_server 补充单测(空闲端口→false, 本地 listener→true);format_host_port 对 IPv6 加方括号, 修复 ::1:8601 非法地址。 7. file_for 加 debug_assert_path_safe 路径清洗, 显式拒绝含 / \ .. 的 session id(纵深防御)。 8. PanelResizeHandle 的 {...props} 移到显式事件处理器之前, 防止外部 onPointerDown 覆盖内部拖拽逻辑。 9. 清理未使用的 i18n key(dashboard.close / tab.review / tab.terminal / loadingFrame)。
795f55a to
21aa5c8
Compare
为 Ora 桌面端集成代理追踪可视化 dashboard,以右侧侧栏面板形式 展示 opencode / Claude Code 的追踪时序图与 token / 工具分析。 后端(apps/desktop/src-tauri): - config.rs:新增 dashboardHost(127.0.0.1)/ dashboardPort(8601) 配置项,附 serde 默认值,保证旧版 config.json 仍可加载 - SessionLocator + resolve_session_locator(仅后端使用, agent_session_id 绝不暴露给前端) - dashboard.rs:get_dashboard_url 命令——按 agent 类型解析追踪文件、 写入 locator、探测 host:port 健康度、返回 iframe URL - 支持 Claude(~/.claude/projects/**/<id>.jsonl)与 opencode(~/.local/share/opencode/trace/<id>.ndjson)两种 trace 路径 - resolve_claude_code_trace 使用 home_directory 而非 cwd 前端(packages/app-shell): - ui-store:dashboardOpen / dashboardWidth 状态 - 可拖拽调宽的侧栏面板(pointer-capture 拖拽手柄) - TraceDashboardPanel 直接渲染 dashboard 内容 - App.tsx 注入 Tauri invoke 解析器 - 新增 i18n 键值(中英双语) - mock-client 补充 gitIdentity namespace 以适配 upstream 新接口 vite.config.ts:忽略 src-tauri/target 目录,避免 EBUSY 监听崩溃。
将侧栏从内联 dock 重构为 @ora/ui 的共享 Sheet 原语, 获得 codex/ChatGPT 风格的交互体验: - 打开时滑入 + 淡入、关闭时淡出(duration-200 ease-in-out, 与 SettingsDialog 等保持统一的对话框节奏) - 点击遮罩关闭 - 原生关闭按钮(top-3 right-3) - 关闭即卸载 iframe(重开时重新解析 locator;Streamlit 常驻, 重连成本极低) 左缘拖拽手柄保留在 Sheet 内部,面板仍可调整宽度; 持久化的像素宽度覆盖 Sheet 默认的比例宽度(inline width + maxWidth:none 覆盖 data-[side=right]:sm:max-w-sm 的 384px 上限)。
根据 PR ora-space#137 检视意见修复: 1. useDashboardEndpoint 切换会话时不再展示旧 endpoint: 在 resolved 中追踪 sessionId,切换时旧数据视为 stale, 派生 isLoading=true + endpoint=null,面板展示 loading 态。 2. walkdir_files 改为真正惰性迭代器(WalkDirIter), 早返回可省 I/O;不可读子目录 tracing::warn! 后跳过, 不再让单个权限问题拖垮整棵扫描。 3. validate_dashboard_endpoint 真正校验 loopback (IpAddr::is_loopback() 或 localhost),拒绝非回环地址, 堵住 tauri.localhost 同源 + allow-scripts 的沙箱风险; 新增 DashboardHostNotLoopback 错误变体。 4. config setters 改用结构更新语法(..config.clone()), 未来新增字段自动携带,不再逐字段手工重建。 5. dashboard.rs 错误映射前加 tracing::warn! 落盘根因, 区分权限不足 / trace 未生成等不同场景。 6. probe_dashboard_server 补充单测(空闲端口→false, 本地 listener→true);format_host_port 对 IPv6 加方括号, 修复 ::1:8601 非法地址。 7. file_for 加 debug_assert_path_safe 路径清洗, 显式拒绝含 / \ .. 的 session id(纵深防御)。 8. PanelResizeHandle 的 {...props} 移到显式事件处理器之前, 防止外部 onPointerDown 覆盖内部拖拽逻辑。 9. 清理未使用的 i18n key(dashboard.close / tab.review / tab.terminal / loadingFrame)。
21aa5c8 to
23fb071
Compare
概述
本 PR 为 Ora 桌面端引入代理追踪可视化 dashboard 功能,让用户在对话过程中以侧栏面板的形式实时查看 opencode / Claude Code 的追踪时序图与 token / 工具分析。同时附带若干工程化改进(路径解析回退、代码格式化)。
变更内容
1. 追踪 dashboard 侧栏面板(
684d313)前端(
packages/app-shell)TraceDashboardPanel组件:以右侧 Sheet 浮层形式嵌入主界面,支持拖拽调宽(420–1400px,宽度持久化到ui-store)。workspace-view.tsx中加入 dashboard 触发按钮,选中会话后可一键打开侧栏。App.tsx注入 Tauri 调用解析器,前端通过agent_type+ Ora 会话 id 解析 iframe URL,agent_session_id全程不暴露给前端。gitIdentitynamespace 以适配 upstream 新接口。后端(
apps/desktop/src-tauri)config.rs:新增dashboardHost/dashboardPort配置项(默认127.0.0.1:8601)。dashboard.rs:实现get_dashboard_url命令——解析追踪文件路径 → 写入 Locator 文件 → 健康探测 dashboard 服务 → 返回 iframe URL。SessionLocator+resolve_session_locator(仅后端使用,agent_session_id绝不暴露给前端)。~/.claude/projects/**/<id>.jsonl)与 opencode(~/.local/share/opencode/trace/<id>.ndjson)两种 trace 路径;Claude 路径使用home_directory而非cwd。2. codex 风格浮层面板交互(
9d9d6ff)@ora/ui的共享Sheet原语,获得原生的:duration-200 ease-in-out,与 SettingsDialog 等保持一致)。absolute top-3 right-3)。sm:max-w-sm(384px)上限。3. opencode 路径解析支持 nga 回退(
1309ce7)resolve_opencode_path(Windows 分支):依次在 PATH 上搜索opencode与nga,命中第一个即返回。部分用户的 OpenCode 发布产物以nga命名(与opencode共享相同数据模型与 trace 格式),此前会导致启动失败。cargo fmt --all全量格式化(dashboard.rs / error.rs)及 Cargo.lock 传递依赖同步。设计决策
host:port,不负责启停<appDataDir>/dashboard/<oraSessionId>.json = {traceFilePath, agentType}写入磁盘;dashboard 通过操作系统约定(非 URL 参数)计算路径agent_session_id保持后端私有测试
🤖 Generated with excellent code assistant