render: extract render_node() + add {{seed}} placeholder

Refactor cmd_render into render_node() that returns a String —
reusable by both the CLI and agent placeholders.

Add {{seed}} placeholder: renders each seed node using the same
output as poc-memory render (content + deduped footer links). Agents
see exactly what a human sees — no special formatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kent Overstreet 2026-03-20 13:47:14 -04:00
parent 5ce1d4ed24
commit d6c26e27fe
2 changed files with 40 additions and 20 deletions

View file

@ -164,6 +164,21 @@ fn resolve(
}) })
} }
// seed — render output for each seed node (content + deduped links)
"seed" => {
let mut text = String::new();
let mut result_keys = Vec::new();
for key in keys {
if let Some(rendered) = crate::cli::node::render_node(store, key) {
if !text.is_empty() { text.push_str("\n\n---\n\n"); }
text.push_str(&format!("## {}\n\n{}", key, rendered));
result_keys.push(key.clone());
}
}
if text.is_empty() { return None; }
Some(Resolved { text, keys: result_keys })
}
"organize" => { "organize" => {
// Show seed nodes with their neighbors for exploratory organizing // Show seed nodes with their neighbors for exploratory organizing
use crate::store::NodeType; use crate::store::NodeType;

View file

@ -187,42 +187,33 @@ pub fn cmd_node_rename(old_key: &str, new_key: &str) -> Result<(), String> {
Ok(()) Ok(())
} }
pub fn cmd_render(key: &[String]) -> Result<(), String> { /// Render a node to a string: content + deduped footer links.
if key.is_empty() { /// Used by both the CLI command and agent placeholders.
return Err("render requires a key".into()); pub fn render_node(store: &store::Store, key: &str) -> Option<String> {
} let node = store.nodes.get(key)?;
let key = key.join(" "); let mut out = node.content.clone();
let store = store::Store::load()?;
let bare = store::strip_md_suffix(&key);
let node = store.nodes.get(&bare)
.ok_or_else(|| format!("Node not found: {}", bare))?;
// Build neighbor lookup: key → strength // Build neighbor lookup: key → strength
let mut neighbor_strengths: std::collections::HashMap<&str, f32> = std::collections::HashMap::new(); let mut neighbor_strengths: std::collections::HashMap<&str, f32> = std::collections::HashMap::new();
for r in &store.relations { for r in &store.relations {
if r.deleted { continue; } if r.deleted { continue; }
if r.source_key == bare { if r.source_key == key {
let e = neighbor_strengths.entry(&r.target_key).or_insert(0.0); let e = neighbor_strengths.entry(&r.target_key).or_insert(0.0);
*e = e.max(r.strength); *e = e.max(r.strength);
} else if r.target_key == bare { } else if r.target_key == key {
let e = neighbor_strengths.entry(&r.source_key).or_insert(0.0); let e = neighbor_strengths.entry(&r.source_key).or_insert(0.0);
*e = e.max(r.strength); *e = e.max(r.strength);
} }
} }
// Detect which neighbors are already referenced inline in the content. // Detect which neighbors are already referenced inline in the content.
// These are omitted from the footer to avoid duplication.
let mut inline_keys: std::collections::HashSet<String> = std::collections::HashSet::new(); let mut inline_keys: std::collections::HashSet<String> = std::collections::HashSet::new();
for nbr_key in neighbor_strengths.keys() { for nbr_key in neighbor_strengths.keys() {
// Match `key` (backtick-quoted) or bare key after → arrow
if node.content.contains(nbr_key) { if node.content.contains(nbr_key) {
inline_keys.insert(nbr_key.to_string()); inline_keys.insert(nbr_key.to_string());
} }
} }
print!("{}", node.content);
// Footer: only show links NOT already referenced inline // Footer: only show links NOT already referenced inline
let mut footer_neighbors: Vec<(&str, f32)> = neighbor_strengths.iter() let mut footer_neighbors: Vec<(&str, f32)> = neighbor_strengths.iter()
.filter(|(k, _)| !inline_keys.contains(**k)) .filter(|(k, _)| !inline_keys.contains(**k))
@ -232,17 +223,31 @@ pub fn cmd_render(key: &[String]) -> Result<(), String> {
if !footer_neighbors.is_empty() { if !footer_neighbors.is_empty() {
footer_neighbors.sort_by(|a, b| b.1.total_cmp(&a.1)); footer_neighbors.sort_by(|a, b| b.1.total_cmp(&a.1));
let total = footer_neighbors.len(); let total = footer_neighbors.len();
let shown: Vec<_> = footer_neighbors.iter().take(15) let shown: Vec<String> = footer_neighbors.iter().take(15)
.map(|(k, s)| format!("({:.2}) `poc-memory render {}`", s, k)) .map(|(k, s)| format!("({:.2}) `poc-memory render {}`", s, k))
.collect(); .collect();
print!("\n\n---\nLinks:"); out.push_str("\n\n---\nLinks:");
for link in &shown { for link in &shown {
println!("\n {}", link); out.push_str(&format!("\n {}", link));
} }
if total > 15 { if total > 15 {
println!(" ... and {} more (`poc-memory graph link {}`)", total - 15, bare); out.push_str(&format!("\n ... and {} more (`poc-memory graph link {}`)", total - 15, key));
} }
} }
Some(out)
}
pub fn cmd_render(key: &[String]) -> Result<(), String> {
if key.is_empty() {
return Err("render requires a key".into());
}
let key = key.join(" ");
let store = store::Store::load()?;
let bare = store::strip_md_suffix(&key);
let rendered = render_node(&store, &bare)
.ok_or_else(|| format!("Node not found: {}", bare))?;
print!("{}", rendered);
Ok(()) Ok(())
} }