docs(04): create phase plan
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
---
|
||||
phase: 04-bbs-polish-and-live-content
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- 04-01
|
||||
files_modified:
|
||||
- src/vault.rs
|
||||
- src/app.rs
|
||||
- src/main.rs
|
||||
autonomous: true
|
||||
requirements:
|
||||
- LIVE-02
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "User can follow a [[Directory]] wiki-link to see a tree view of all vault documents"
|
||||
- "Directory listing shows folder hierarchy with indentation, directories in yellow bold, files as cyan navigable links"
|
||||
- "User can Tab-cycle between directory entries and press Enter to open a document"
|
||||
- "Directory page participates in navigation history (back/forward works)"
|
||||
- "Navigating to [[Directory]] and back preserves scroll position and link selection"
|
||||
artifacts:
|
||||
- path: "src/vault.rs"
|
||||
provides: "[[Directory]] magic interception in resolve_wiki_link + list_vault_files() function"
|
||||
contains: "__directory__"
|
||||
- path: "src/app.rs"
|
||||
provides: "navigate_to_directory() method, DocumentState handling for virtual directory page"
|
||||
contains: "navigate_to_directory"
|
||||
key_links:
|
||||
- from: "src/vault.rs"
|
||||
to: "walkdir crate"
|
||||
via: "list_vault_files() using WalkDir recursive traversal"
|
||||
pattern: "WalkDir::new"
|
||||
- from: "src/app.rs"
|
||||
to: "src/vault.rs"
|
||||
via: "follow_selected_link detects __directory__ sentinel and calls navigate_to_directory"
|
||||
pattern: "__directory__"
|
||||
- from: "src/app.rs"
|
||||
to: "src/vault.rs"
|
||||
via: "navigate_to_directory calls list_vault_files to build the listing"
|
||||
pattern: "list_vault_files"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add a virtual directory listing page accessible via `[[Directory]]` wiki-link, displaying all vault documents in a navigable tree view.
|
||||
|
||||
Purpose: Users can discover all available documents in the vault without knowing their paths. The directory listing acts as a table of contents for the entire vault.
|
||||
|
||||
Output: Updated `src/vault.rs` with directory interception and file listing, updated `src/app.rs` with directory page navigation and rendering.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/ruohki/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/Users/ruohki/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/04-bbs-polish-and-live-content/04-RESEARCH.md
|
||||
@.planning/phases/04-bbs-polish-and-live-content/04-01-SUMMARY.md
|
||||
@src/vault.rs
|
||||
@src/app.rs
|
||||
@src/renderer.rs
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add [[Directory]] interception and vault file listing</name>
|
||||
<files>src/vault.rs</files>
|
||||
<action>
|
||||
1. In `resolve_wiki_link()`, add a magic sentinel check BEFORE the filesystem scan:
|
||||
```rust
|
||||
// Magic sentinel — [[Directory]] navigates to the virtual directory page
|
||||
if raw_target.eq_ignore_ascii_case("directory") {
|
||||
return Some(PathBuf::from("__directory__"));
|
||||
}
|
||||
```
|
||||
Place this at the very top of `resolve_wiki_link()`, before the `rfind('/')` split.
|
||||
|
||||
2. Add `list_vault_files()` function to `vault.rs`:
|
||||
```rust
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Entry in the vault directory listing.
|
||||
pub struct DirEntry {
|
||||
/// Indentation depth (1-based, depth 1 = vault root children)
|
||||
pub depth: usize,
|
||||
/// Display name (without .md extension for files)
|
||||
pub name: String,
|
||||
/// True if this entry is a directory
|
||||
pub is_dir: bool,
|
||||
/// Vault-relative path (only for .md files; None for directories)
|
||||
pub vault_path: Option<String>,
|
||||
}
|
||||
|
||||
/// List all markdown files and directories in the vault, sorted alphabetically.
|
||||
/// Skips hidden files/directories (starting with '.') and non-.md files.
|
||||
/// Returns entries with depth for tree indentation.
|
||||
pub fn list_vault_files(vault_path: &Path) -> Vec<DirEntry> {
|
||||
let mut entries = Vec::new();
|
||||
for entry in WalkDir::new(vault_path)
|
||||
.sort_by_file_name()
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.skip(1) // skip vault root itself
|
||||
{
|
||||
let depth = entry.depth();
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
// Skip hidden files/dirs
|
||||
if name.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_dir = entry.file_type().is_dir();
|
||||
|
||||
if is_dir {
|
||||
entries.push(DirEntry {
|
||||
depth,
|
||||
name,
|
||||
is_dir: true,
|
||||
vault_path: None,
|
||||
});
|
||||
} else if name.ends_with(".md") {
|
||||
let display_name = name.strip_suffix(".md").unwrap_or(&name).to_string();
|
||||
let rel = entry.path()
|
||||
.strip_prefix(vault_path)
|
||||
.unwrap_or(entry.path())
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
entries.push(DirEntry {
|
||||
depth,
|
||||
name: display_name,
|
||||
is_dir: false,
|
||||
vault_path: Some(rel),
|
||||
});
|
||||
}
|
||||
// Skip non-.md files silently
|
||||
}
|
||||
entries
|
||||
}
|
||||
```
|
||||
|
||||
3. Run `cargo build` to verify vault.rs compiles.
|
||||
</action>
|
||||
<verify>`cargo build` succeeds. `resolve_wiki_link()` returns `Some(PathBuf::from("__directory__"))` when called with "Directory" (case-insensitive). `list_vault_files()` is defined and exported.</verify>
|
||||
<done>[[Directory]] wiki-link resolves to the `__directory__` sentinel. list_vault_files() returns structured directory entries with depth, name, is_dir, and vault_path.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Wire directory as virtual page with Tab-cycling navigation</name>
|
||||
<files>src/app.rs, src/main.rs</files>
|
||||
<action>
|
||||
1. Add a `navigate_to_directory()` method to `App`:
|
||||
```rust
|
||||
/// Navigate to the virtual directory listing page.
|
||||
/// Generates a tree view of all vault files as styled lines with link records.
|
||||
fn navigate_to_directory(&mut self) {
|
||||
let vault_path = self.config.vault_path.clone();
|
||||
|
||||
// Save current state to history
|
||||
if let Some(entry) = self.history.get_mut(self.history_index) {
|
||||
entry.scroll_offset = self.scroll_offset;
|
||||
entry.selected_link = self.selected_link;
|
||||
}
|
||||
// Truncate forward history
|
||||
self.history.truncate(self.history_index + 1);
|
||||
|
||||
// Build directory listing
|
||||
let dir_entries = crate::vault::list_vault_files(&vault_path);
|
||||
let (lines, link_records) = build_directory_lines(&dir_entries);
|
||||
|
||||
self.document = DocumentState::Loaded {
|
||||
filename: "[Directory]".to_string(),
|
||||
lines,
|
||||
};
|
||||
self.raw_content = None; // No raw markdown for virtual pages
|
||||
self.link_records = link_records;
|
||||
self.selected_link = None;
|
||||
self.scroll_offset = 0;
|
||||
self.current_path = "__directory__".to_string();
|
||||
self.filename = "[Directory]".to_string();
|
||||
self.page_meta = None; // No file metadata for virtual pages
|
||||
|
||||
// Push history entry
|
||||
self.history.push(HistoryEntry {
|
||||
path: "__directory__".to_string(),
|
||||
scroll_offset: 0,
|
||||
selected_link: None,
|
||||
});
|
||||
self.history_index = self.history.len() - 1;
|
||||
}
|
||||
```
|
||||
|
||||
2. Create the `build_directory_lines()` helper function (module-level in app.rs):
|
||||
```rust
|
||||
/// Build styled lines and link records from vault directory entries.
|
||||
fn build_directory_lines(
|
||||
entries: &[crate::vault::DirEntry],
|
||||
) -> (Vec<Line<'static>>, Vec<crate::renderer::LinkRecord>) {
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
let mut link_records: Vec<crate::renderer::LinkRecord> = Vec::new();
|
||||
|
||||
// Title header
|
||||
lines.push(Line::from(Span::styled(
|
||||
" VAULT DIRECTORY".to_string(),
|
||||
Style::default().fg(Color::LightCyan).add_modifier(Modifier::BOLD),
|
||||
)));
|
||||
lines.push(Line::from(Span::styled(
|
||||
"=".repeat(40),
|
||||
Style::default().fg(Color::LightCyan),
|
||||
)));
|
||||
lines.push(Line::default()); // blank separator
|
||||
|
||||
for entry in entries {
|
||||
let indent = " ".repeat(entry.depth.saturating_sub(1));
|
||||
let indent_chars = indent.chars().count();
|
||||
|
||||
if entry.is_dir {
|
||||
// Directory: yellow bold, not a link
|
||||
lines.push(Line::from(vec![
|
||||
Span::raw(indent),
|
||||
Span::styled(
|
||||
format!("{}/", entry.name),
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
]));
|
||||
} else if let Some(ref vault_rel) = entry.vault_path {
|
||||
// File: cyan link — navigable via Tab-cycling
|
||||
let display = format!("[{}]", entry.name);
|
||||
let col_offset = indent_chars;
|
||||
let span_len = display.chars().count();
|
||||
let line_index = lines.len(); // Will be this line's index
|
||||
|
||||
lines.push(Line::from(vec![
|
||||
Span::raw(indent),
|
||||
Span::styled(
|
||||
display,
|
||||
Style::default().fg(Color::LightCyan),
|
||||
),
|
||||
]));
|
||||
|
||||
link_records.push(crate::renderer::LinkRecord {
|
||||
line_index,
|
||||
col_offset,
|
||||
span_len,
|
||||
dest: vault_rel.clone(),
|
||||
is_wiki: false, // Direct vault-relative path, not a wiki-link
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
(lines, link_records)
|
||||
}
|
||||
```
|
||||
|
||||
3. Update `follow_selected_link()` in app.rs:
|
||||
- After resolving a wiki-link with `resolve_wiki_link()`, check if the resolved path is the directory sentinel:
|
||||
```rust
|
||||
// Inside the is_wiki branch, after resolve_wiki_link returns Some:
|
||||
if resolved == Path::new("__directory__") {
|
||||
self.navigate_to_directory();
|
||||
} else {
|
||||
let rel = resolved.to_string_lossy().to_string();
|
||||
self.navigate_to(&rel);
|
||||
}
|
||||
```
|
||||
- For the non-wiki branch (standard links), if `dest == "__directory__"`, call `navigate_to_directory()` instead.
|
||||
|
||||
4. Update `navigate_back()` and `navigate_forward()`:
|
||||
- When the `target_path` is `"__directory__"`, call `navigate_to_directory()`-style logic instead of `load_document()`. Specifically:
|
||||
```rust
|
||||
if target_path == "__directory__" {
|
||||
// Regenerate directory listing (always fresh, never cached)
|
||||
let dir_entries = crate::vault::list_vault_files(&vault_path);
|
||||
let (lines, link_records) = build_directory_lines(&dir_entries);
|
||||
self.document = DocumentState::Loaded {
|
||||
filename: "[Directory]".to_string(),
|
||||
lines,
|
||||
};
|
||||
self.raw_content = None;
|
||||
self.link_records = link_records;
|
||||
self.selected_link = target_link;
|
||||
self.scroll_offset = target_scroll;
|
||||
self.current_path = "__directory__".to_string();
|
||||
self.filename = "[Directory]".to_string();
|
||||
self.page_meta = None;
|
||||
} else {
|
||||
// existing load_document() logic
|
||||
}
|
||||
```
|
||||
- IMPORTANT: Do NOT save/push new history entries in navigate_back/forward — they restore existing history positions. Only update current state fields. The existing history entry management (saving scroll/link to current index, then moving index) remains unchanged.
|
||||
|
||||
5. Update `handle_resize()`:
|
||||
- If `current_path == "__directory__"`, skip the `render_markdown` re-render. The directory listing is not markdown and does not need width-based re-render. The tree view spans are fixed-width text. Do nothing for directory pages on resize.
|
||||
|
||||
6. Run `cargo build` to verify everything compiles.
|
||||
</action>
|
||||
<verify>`cargo build` succeeds. The follow_selected_link() method correctly routes `__directory__` to navigate_to_directory(). navigate_back/forward handle the `__directory__` case. handle_resize skips re-render for directory pages.</verify>
|
||||
<done>[[Directory]] wiki-link navigates to a tree view page. Tab-cycling selects directory entries. Enter opens the selected document. Back/Forward navigates through directory page in history. Directory listing is always regenerated fresh (never stale).</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `cargo build` — compiles without errors
|
||||
2. Add `[[Directory]]` link to index.md in a test vault — following it shows tree view
|
||||
3. Tree view shows folders (yellow bold with `/`) and files (cyan `[name]` bracketed)
|
||||
4. Tab-cycle between file entries — REVERSED highlight moves correctly
|
||||
5. Press Enter on a file entry — navigates to that document
|
||||
6. Press Backspace — returns to directory listing with scroll/link selection preserved
|
||||
7. Navigate forward — returns to the document
|
||||
8. Case-insensitive: `[[directory]]` and `[[DIRECTORY]]` both work
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [[Directory]] wiki-link navigates to virtual tree view page
|
||||
- Tree shows hierarchy with indentation, dirs yellow bold, files cyan bracketed links
|
||||
- Tab-cycling works on directory entries using existing link model
|
||||
- Directory participates in back/forward history
|
||||
- Directory listing is regenerated each visit (not stale)
|
||||
- handle_resize does not crash on directory page
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-bbs-polish-and-live-content/04-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user