chore: land tree view-state, vault, Pi module split, and repo hygiene

Persist PageTree expand state via control-plane view-state and align
chevron/DOM with restored expansion; keep Sidex-style shallow page-tree
scan and drop the unused recursive scanner that only added cargo noise.

Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi
into a module package, and retire Hermes/ACP/OpenHub recycle + root
harness evidence from the index while gitignoring recycle and local
diag dumps.

Archive superseded design/bugs docs under old/, point architecture at
ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor
regressions so the working tree can stay clean.
This commit is contained in:
Agent Board
2026-07-21 05:13:05 +08:00
parent 6f9c7d3b58
commit b798f628ee
264 changed files with 17480 additions and 17314 deletions
+116 -1
View File
@@ -46,6 +46,64 @@ pub struct WorkspaceShellEntry {
pub icon: String,
}
/// 使用页面树投影生成顶栏页面祖先链。
pub fn render_page_breadcrumb_html(
projection: &WorkspaceShellProjection,
active_page_id: Option<&str>,
) -> String {
let Some(active_page_id) = active_page_id
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return String::new();
};
let by_id = projection
.my_page_items
.iter()
.map(|item| (item.id.as_str(), item))
.collect::<std::collections::HashMap<_, _>>();
let mut chain = Vec::new();
let mut cursor = Some(active_page_id);
let mut seen = std::collections::HashSet::new();
while let Some(id) = cursor {
if !seen.insert(id) {
break;
}
let Some(item) = by_id.get(id) else {
break;
};
chain.push(*item);
cursor = item.parent_id.as_deref();
if chain.len() >= 64 {
break;
}
}
chain.reverse();
chain
.iter()
.enumerate()
.map(|(index, item)| {
let separator = if index == 0 {
String::new()
} else {
r#"<span class="wolai-breadcrumb-separator" aria-hidden="true"></span>"#.to_string()
};
let title = escape_html(&item.title);
let id = escape_html(&item.id);
if index + 1 == chain.len() {
format!(
r#"{separator}<span class="wolai-breadcrumb-current" data-breadcrumb-document-id="{id}"><span class="material-symbols-outlined wolai-home-icon" data-icon="home" aria-hidden="true"></span><span data-page-title-current="true">{title}</span></span>"#
)
} else {
format!(
r#"{separator}<a class="wolai-breadcrumb-link" data-breadcrumb-document-id="{id}" href="{}">{title}</a>"#,
escape_html(&item.href)
)
}
})
.collect()
}
pub fn build_workspace_shell_projection(
dataset: &Value,
workspace_id: &str,
@@ -553,6 +611,32 @@ mod tests {
.any(|entry| entry.label == "模板中心"));
}
#[test]
fn page_breadcrumb_renders_all_ancestors_as_links() {
let projection = build_workspace_shell_projection(
&json!({
"workspaces": [{ "id": "ws_demo", "name": "我的空间" }],
"documents": [
{ "id": "root", "workspace_id": "ws_demo", "title": "个人", "parent_id": null },
{ "id": "parent", "workspace_id": "ws_demo", "title": "密码", "parent_id": "root" },
{ "id": "current", "workspace_id": "ws_demo", "title": "deepseek ai", "parent_id": "parent" }
]
}),
"ws_demo",
Some("current"),
"我的空间",
);
let html = render_page_breadcrumb_html(&projection, Some("current"));
assert!(html.contains(r#"data-breadcrumb-document-id="root""#));
assert!(html.contains(r#"data-breadcrumb-document-id="parent""#));
assert!(html.contains(r#"data-breadcrumb-document-id="current""#));
assert!(html.contains(r#"href="/documents/root?workspaceId=ws_demo""#));
assert!(html.contains(r#"href="/documents/parent?workspaceId=ws_demo""#));
assert!(html.contains("deepseek ai"));
assert_eq!(html.matches("wolai-breadcrumb-separator").count(), 2);
}
#[test]
fn workspace_shell_sidebar_html_outputs_projection_rows_with_active_and_parent() {
let dataset = json!({
@@ -652,6 +736,7 @@ mod tests {
assert!(html.contains(r#"data-mnote-sidebar-tree-panel="filetree" hidden"#));
assert!(html.contains(r#"id="sidebar-tree-root""#));
assert!(html.contains(r#"id="sidebar-file-tree-root""#));
assert!(!html.contains(r#"data-filetree-ssr="pending""#));
assert!(!html.contains("wolai-file-tree-section"));
}
@@ -687,6 +772,27 @@ mod tests {
assert!(html.contains(r#"data-mnote-sidebar-tree-panel="filetree"><div"#));
}
#[test]
fn workspace_shell_sidebar_html_propagates_pending_filetree_shell_marker() {
let dataset = json!({
"workspaces": [{ "id": "ws_demo", "name": "开发用户 的工作区" }],
"documents": []
});
let projection =
build_workspace_shell_projection(&dataset, "ws_demo", None, "开发用户 的工作区");
let html = render_workspace_shell_sidebar_html(
&projection,
Some(r#"<ul data-rust-page-renderer="initial_v1"></ul>"#),
Some(r#"<ul class="tree-root" role="tree" data-rust-filetree-renderer="pending_shell_v1" data-filetree-ssr="pending" aria-busy="true"></ul>"#),
None,
None,
);
assert!(html.contains(r#"id="sidebar-file-tree-root""#));
assert!(html.contains(r#"data-filetree-ssr="pending""#));
assert!(html.contains(r#"data-rust-filetree-renderer="pending_shell_v1""#));
assert!(html.contains(r#"aria-busy="true""#));
}
#[test]
fn workspace_shell_sidebar_html_marks_filetree_scope() {
let dataset = json!({
@@ -805,8 +911,17 @@ pub fn render_workspace_shell_sidebar_html(
)
})
.unwrap_or_default();
// Propagate shell-first pending marker onto the FileTree root so the client
// can hydrate without scanning for nested placeholders.
let pending_attr = if html.contains("data-filetree-ssr=\"pending\"")
|| html.contains("data-rust-filetree-renderer=\"pending_shell_v1\"")
{
r#" data-filetree-ssr="pending" aria-busy="true""#
} else {
""
};
format!(
r#"<div class="sidebar-tree-section sidebar-file-tree-section"><div id="sidebar-file-tree-root" class="sidebar-tree" data-tree-shell-mode="filetree" data-workspace-id="{}"{file_tree_scope_attr}>{html}</div></div>"#,
r#"<div class="sidebar-tree-section sidebar-file-tree-section"><div id="sidebar-file-tree-root" class="sidebar-tree" data-tree-shell-mode="filetree" data-workspace-id="{}"{file_tree_scope_attr}{pending_attr}>{html}</div></div>"#,
escape_html(&projection.workspace_id),
)
})