feat: add evidence search and stabilize pdf previews

- add document evidence parsing/search/open routes, Hermes tool wiring, local index settings/status, and the document-evidence skill plus design notes
- fix PDF resource tabs by rendering PDFs inline with pdf.js canvases instead of iframe preview pages, release PDF documents on close, and document the fourth-PDF stall bug
- keep PDF preview at 2x rendering while removing the previous lazy-load/placeholder direction, and make dev:hot bind loopback defaults externally reachable

Verification:
- node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.js
- node scripts/task-dev-hot-plan-test.js
- cargo test -p mnote-web --manifest-path rust/Cargo.toml pdf_preview_page_does_not_render_visible_toolbar
- cargo test -p mnote-web --manifest-path rust/Cargo.toml document_shell_returns_page_aggregate_snapshot
- cargo build -p mnote-web --manifest-path rust/Cargo.toml
- browser smoke: sequentially opened the four tea_seed_oil_cosmetic PDFs; fourth PDF rendered 15/15 canvases, iframeCount=0, browser errors=0
This commit is contained in:
lix-2026
2026-06-04 18:51:16 +08:00
parent 9f4b5c4d48
commit 627213dc01
51 changed files with 15960 additions and 1844 deletions
@@ -1,4 +1,4 @@
use crate::app::AppState;
use crate::app::{open_control_plane_store, AppState};
use crate::context::RequestContext;
use crate::error::WebError;
use crate::page_aggregate::{
@@ -2680,6 +2680,21 @@ pub fn load_local_folder_file_tree_children_snapshot(
load_local_folder_file_tree_scope_snapshot(root_uri, Some(parent_relative_path), None)
}
pub fn load_local_folder_file_tree_children_snapshot_with_reveal(
root_uri: &str,
parent_relative_path: &str,
reveal_document_id: Option<&str>,
) -> Result<ProjectionSnapshot, WebError> {
let reveal_relative_path = reveal_document_id
.and_then(local_markdown_relative_path_from_document_id)
.map(|path| path.replace('\\', "/"));
load_local_folder_file_tree_scope_snapshot(
root_uri,
Some(parent_relative_path),
reveal_relative_path.as_deref(),
)
}
fn load_local_folder_file_tree_scope_snapshot(
root_uri: &str,
parent_relative_path: Option<&str>,
@@ -2732,16 +2747,15 @@ fn load_local_folder_file_tree_scope_snapshot(
&workspace_id,
&metadata,
)?;
if parent_relative_path.is_empty() {
append_file_tree_reveal_rows(
&canonical_root,
reveal_relative_path,
&root_source_uri,
&workspace_id,
&metadata,
&mut scan_result.rows,
)?;
}
append_file_tree_reveal_rows(
&canonical_root,
parent_relative_path,
reveal_relative_path,
&root_source_uri,
&workspace_id,
&metadata,
&mut scan_result.rows,
)?;
let items = scan_result
.rows
@@ -3207,7 +3221,13 @@ fn save_local_markdown_page_inner(
}
fn refresh_local_search_index_best_effort(root: &Path, root_uri: &str, workspace_id: &str) {
let _ = local_search_index::refresh_local_search_index(root, root_uri, workspace_id);
let control_plane = open_control_plane_store();
let _ = local_search_index::refresh_local_search_index_for_change_with_store(
&control_plane,
root,
root_uri,
workspace_id,
);
}
fn local_markdown_conflict_error(
@@ -6842,6 +6862,7 @@ fn ancestor_directories_for_relative_path(relative_path: &str) -> Vec<String> {
fn append_file_tree_reveal_rows(
root: &Path,
parent_relative_path: &str,
reveal_relative_path: Option<&str>,
root_source_uri: &str,
workspace_id: &str,
@@ -6854,10 +6875,30 @@ fn append_file_tree_reveal_rows(
else {
return Ok(());
};
let mut ancestors = ancestor_directories_for_relative_path(reveal_relative_path);
if let Some(bundle_parent) = same_name_markdown_bundle_parent(reveal_relative_path) {
let parent_relative_path = parent_relative_path
.trim()
.trim_matches('/')
.replace('\\', "/");
let reveal_relative_path = reveal_relative_path
.trim()
.trim_matches('/')
.replace('\\', "/");
if !parent_relative_path.is_empty()
&& reveal_relative_path != parent_relative_path
&& !reveal_relative_path.starts_with(&format!("{parent_relative_path}/"))
{
return Ok(());
}
let mut ancestors = ancestor_directories_for_relative_path(&reveal_relative_path);
if let Some(bundle_parent) = same_name_markdown_bundle_parent(&reveal_relative_path) {
ancestors.retain(|ancestor| ancestor != &bundle_parent);
}
if !parent_relative_path.is_empty() {
ancestors.retain(|ancestor| {
ancestor != &parent_relative_path
&& ancestor.starts_with(&format!("{parent_relative_path}/"))
});
}
if ancestors.is_empty() {
return Ok(());
}
@@ -7047,7 +7088,38 @@ fn resolve_metadata_relative_path(root: &Path, relative_path: &str) -> Result<Pa
}
fn should_ignore_entry(relative_path: &str, file_name: &str) -> bool {
if matches!(file_name, ".git" | "node_modules" | ".mnote") {
let lower_name = file_name.to_ascii_lowercase();
if matches!(
lower_name.as_str(),
".git"
| ".mnote"
| ".codegraph"
| ".codex"
| ".claw"
| ".gemini"
| ".reasonix"
| ".venv"
| "__pycache__"
| "node_modules"
| ".next"
| ".turbo"
| ".pnpm-store"
| ".convex-tmp"
| "target"
| "dist"
| "build"
| "tmp"
| "temp"
| "artifacts"
| "test-results"
| "pw-tests"
| "recycle"
| "reference-code"
| "services"
| "cankao"
| "ai-sessions"
) || lower_name.starts_with("onlyoffice-")
{
return true;
}
relative_path == ".mnote/trash"
@@ -9732,6 +9804,18 @@ mod tests {
std::fs::write(root.join(".mnote").join("ignored.txt"), "ignored").expect("write ignored");
let ignored = local_folder_watch_revision(&root_uri).expect("ignored revision");
assert_eq!(initial.revision, ignored.revision);
std::fs::create_dir_all(root.join("target")).expect("create target");
std::fs::write(root.join("target").join("ignored.md"), "# Ignored\n")
.expect("write ignored target md");
std::fs::create_dir_all(root.join("reference-code")).expect("create reference-code");
std::fs::write(
root.join("reference-code").join("ignored.md"),
"# Ignored\n",
)
.expect("write ignored reference md");
let ignored_generated =
local_folder_watch_revision(&root_uri).expect("ignored generated revision");
assert_eq!(initial.revision, ignored_generated.revision);
std::fs::write(root.join("docs").join("page.md"), "# Watch Again\n").expect("update md");
let updated = local_folder_watch_revision(&root_uri).expect("updated revision");
assert_ne!(initial.revision, updated.revision);