Use node identity in local helper UI

This commit is contained in:
HumbleTurtle
2026-05-22 23:39:59 -03:00
parent 1cb8075324
commit 031b888fdd
2 changed files with 60 additions and 25 deletions
+48 -25
View File
@@ -10041,7 +10041,7 @@ def ui_tree_insert(root, node):
cursor.setdefault("nodes", []).append(node)
def ui_render_tree_branch(branch, selected_path, mode, search, prefix=""):
def ui_render_tree_branch(branch, selected_path, selected_node_id, mode, search, prefix=""):
folders = branch.get("folders", {})
nodes = branch.get("nodes", [])
output = []
@@ -10052,14 +10052,15 @@ def ui_render_tree_branch(branch, selected_path, mode, search, prefix=""):
output.append(
f"<details class='folder' {'open' if should_open else ''}>"
f"<summary><span class='folder-icon'>dir</span>{html.escape(name)}</summary>"
f"{ui_render_tree_branch(child, selected_path, mode, search, folder_path)}"
f"{ui_render_tree_branch(child, selected_path, selected_node_id, mode, search, folder_path)}"
"</details>"
)
for node in sort_nodes_by_recency(nodes):
full_path = node.get("full_path") or ""
node_id = node.get("id") or ""
name = full_path.rstrip("/").split("/")[-1] or full_path
params = urllib.parse.urlencode({"mode": mode, "q": search, "path": full_path})
active = " active" if full_path == selected_path else ""
params = urllib.parse.urlencode({"mode": mode, "q": search, "node_id": node_id})
active = " active" if node_id and node_id == selected_node_id else ""
output.append(
f"<a class='file{active}' href='/?{params}'><span class='file-icon'>node</span>"
f"<span>{html.escape(name)}</span><small>{html.escape(node.get('type') or 'note')}</small></a>"
@@ -10241,6 +10242,7 @@ def start_local_ui(args):
"feedback": [],
}
search = query.get("q", [""])[0]
selected_node_id = query.get("node_id", [""])[0]
selected_path = query.get("path", [""])[0]
nodes_query = {"limit": 200}
if search:
@@ -10257,14 +10259,21 @@ def start_local_ui(args):
agent_nodes = [node for node in all_nodes if is_agent_namespace_path(node.get("full_path") or "")]
if is_agent_namespace_path(selected_path):
selected_path = ""
if not selected_path and nodes:
selected_path = nodes[0].get("full_path") or ""
selected_node = None
if selected_path:
if not selected_node_id and selected_path:
try:
selected_node = read_node_by_path({"path": selected_path})
selected_node_id = (node_by_path({"path": selected_path}).get("identity") or {}).get("node_id") or ""
except Exception:
selected_node_id = ""
if not selected_node_id and nodes:
selected_node_id = nodes[0].get("id") or ""
selected_node = None
if selected_node_id:
try:
selected_node = get_node_by_id({"node_id": selected_node_id})
selected_path = selected_node.get("full_path") or ""
except Exception:
selected_node = None
selected_path = ""
schedules = request_json("GET", f"/v1/projects/{project_id}/schedules").get("items", [])
try:
feedback = request_json("GET", "/v1/feedback?limit=50").get("items", [])
@@ -10320,6 +10329,7 @@ def start_local_ui(args):
"archived_nodes": archived_nodes,
"agent_nodes": agent_nodes,
"connected_agents": connected_agents,
"selected_node_id": selected_node_id,
"selected_path": selected_path,
"selected_node": selected_node,
"schedules": schedules,
@@ -10453,6 +10463,7 @@ def start_local_ui(args):
mode = query.get("mode", ["filesystem"])[0]
if mode not in {"filesystem", "path-list"}:
mode = "filesystem"
selected_node_id = query.get("node_id", [""])[0]
selected_path = query.get("path", [""])[0]
nodes_query = {"limit": 200}
if search:
@@ -10460,32 +10471,40 @@ def start_local_ui(args):
nodes_suffix = rest_query_string(nodes_query)
nodes = request_json("GET", f"/v1/projects/{project_id}/nodes?{nodes_suffix}").get("items", [])
nodes = sort_nodes_by_recency(nodes)
if not selected_path and nodes:
selected_path = nodes[0].get("full_path") or ""
selected_node = None
if selected_path:
if not selected_node_id and selected_path:
try:
selected_node = read_node_by_path({"path": selected_path})
selected_node_id = (node_by_path({"path": selected_path}).get("identity") or {}).get("node_id") or ""
except Exception:
selected_node_id = ""
if not selected_node_id and nodes:
selected_node_id = nodes[0].get("id") or ""
selected_node = None
if selected_node_id:
try:
selected_node = get_node_by_id({"node_id": selected_node_id})
selected_path = selected_node.get("full_path") or ""
except Exception:
selected_node = None
selected_path = ""
schedules = request_json("GET", f"/v1/projects/{project_id}/schedules").get("items", [])
feedback = request_json("GET", "/v1/feedback?limit=50").get("items", [])
status = update_status()
current_url = "/" + (f"?{parsed_path.query}" if parsed_path.query else "")
filesystem_url = "/?" + urllib.parse.urlencode({"mode": "filesystem", "q": search, "path": selected_path})
path_list_url = "/?" + urllib.parse.urlencode({"mode": "path-list", "q": search, "path": selected_path})
filesystem_url = "/?" + urllib.parse.urlencode({"mode": "filesystem", "q": search, "node_id": selected_node_id})
path_list_url = "/?" + urllib.parse.urlencode({"mode": "path-list", "q": search, "node_id": selected_node_id})
tree = ui_build_tree(nodes)
tree_html = ui_render_tree_branch(tree, selected_path, mode, search)
tree_html = ui_render_tree_branch(tree, selected_path, selected_node_id, mode, search)
selected_links = ui_json_list((selected_node or {}).get("links") or [])
selected_metadata = ui_json((selected_node or {}).get("metadata") or {})
selected_tags = ", ".join((selected_node or {}).get("tags") or [])
selected_content = html.escape((selected_node or {}).get("content_text") or "")
selected_type = html.escape((selected_node or {}).get("type") or "note")
selected_path_value = html.escape(selected_path or "")
selected_node_id_value = html.escape(selected_node_id or "")
selected_title = html.escape(selected_path or "New node")
selected_badges = ui_tags((selected_node or {}).get("tags") or [])
node_rows = "".join(
f"<tr><td><a class='path-link' href='/?{rest_query_string({'mode': 'filesystem', 'path': item['full_path'], 'q': search})}'><code>{html.escape(item['full_path'])}</code></a></td><td>{ui_tags(item.get('tags') or [])}</td><td>{html.escape(item.get('type') or 'note')}</td><td><form method='post' action='/nodes/archive'><input type='hidden' name='return_to' value='/?{rest_query_string({'mode': mode, 'q': search})}'><input type='hidden' name='path' value='{html.escape(item['full_path'])}'><button class='ghost'>Archive</button></form></td></tr>"
f"<tr><td><a class='path-link' href='/?{rest_query_string({'mode': 'filesystem', 'node_id': item.get('id') or '', 'q': search})}'><code>{html.escape(item['full_path'])}</code></a></td><td>{ui_tags(item.get('tags') or [])}</td><td>{html.escape(item.get('type') or 'note')}</td><td><form method='post' action='/nodes/archive'><input type='hidden' name='return_to' value='/?{rest_query_string({'mode': mode, 'q': search})}'><input type='hidden' name='node_id' value='{html.escape(item.get('id') or '')}'><button class='ghost'>Archive</button></form></td></tr>"
for item in nodes
)
schedule_rows = "".join(
@@ -10516,13 +10535,13 @@ def start_local_ui(args):
{f'''
<div class="workspace"><aside class="side"><div class="side-head"><strong>Files</strong><span class="muted">{len(nodes)} nodes</span></div><div class="tree">{tree_html or '<div class="empty">No nodes found.</div>'}</div></aside>
<div class="detail"><div class="detail-head"><div class="selected-title"><strong>{selected_title}</strong></div><div>{selected_badges}</div></div><div class="detail-body">
<form method="post" action="/nodes/upsert"><input type="hidden" name="return_to" value="{html.escape(current_url)}"><h3>{'Edit node' if selected_node else 'Create node'}</h3>
<form method="post" action="/nodes/upsert"><input type="hidden" name="return_to" value="{html.escape(current_url)}"><input type="hidden" name="node_id" value="{selected_node_id_value}"><h3>{'Edit node' if selected_node else 'Create node'}</h3>
<label>Full path<input name="path" required value="{selected_path_value}" placeholder="/decisions/auth-model"></label>
<div class="grid"><label>Type<input name="type" value="{selected_type}"></label><label>Tags CSV<input name="tags" value="{html.escape(selected_tags)}" placeholder="decision, auth"></label></div>
<label>Content<textarea name="content_text">{selected_content}</textarea></label>
<div class="grid"><label>Links JSON<textarea class="mono" name="links">{selected_links}</textarea></label><label>Metadata JSON<textarea class="mono" name="metadata">{selected_metadata}</textarea></label></div>
<div class="actions"><button>Save node</button><a class="button ghost" href="/?mode=filesystem&q={urllib.parse.quote(search)}">New node</a></div></form>
<form method="post" action="/nodes/link"><input type="hidden" name="return_to" value="{html.escape(current_url)}"><h3>Link nodes</h3><div class="grid"><label>Source path<input name="source_path" value="{selected_path_value}" required></label><label>Target path<input name="target_path" required></label><label>Relation<input name="relation" value="references"></label></div><button class="ghost">Create link</button></form>
<form method="post" action="/nodes/link"><input type="hidden" name="return_to" value="{html.escape(current_url)}"><h3>Link nodes</h3><div class="grid"><label>Source node id<input name="source_node_id" value="{selected_node_id_value}" required></label><label>Target node id<input name="target_node_id" required></label><label>Relation<input name="relation" value="references"></label></div><button class="ghost">Create link</button></form>
</div></div></div>''' if mode == 'filesystem' else f'''
<div class="detail"><table><thead><tr><th>Path</th><th>Tags</th><th>Type</th><th>Action</th></tr></thead><tbody>{node_rows or '<tr><td colspan="4" class="muted">No nodes.</td></tr>'}</tbody></table></div>'''}
</section>
@@ -10600,20 +10619,24 @@ def start_local_ui(args):
"payload": json.loads((form.get("payload", ["{}"])[0] or "{}")),
})
elif self.path == "/nodes/upsert":
upsert_node({
node_id = form.get("node_id", [""])[0].strip()
payload = {
"path": form.get("path", [""])[0],
"type": form.get("type", ["note"])[0],
"tags": [tag.strip() for tag in form.get("tags", [""])[0].split(",") if tag.strip()],
"links": json.loads(form.get("links", ["[]"])[0] or "[]"),
"metadata": json.loads(form.get("metadata", ["{}"])[0] or "{}"),
"content_text": form.get("content_text", [""])[0],
})
}
if node_id:
payload["node_id"] = node_id
upsert_node(payload)
elif self.path == "/nodes/archive":
archive_node_by_path({"path": form.get("path", [""])[0]})
archive_node_by_path({"node_id": form.get("node_id", [""])[0]})
elif self.path == "/nodes/link":
link_nodes({
"source_path": form.get("source_path", [""])[0],
"target_path": form.get("target_path", [""])[0],
"source_node_id": form.get("source_node_id", [""])[0],
"target_node_id": form.get("target_node_id", [""])[0],
"relation": form.get("relation", ["references"])[0],
})
elif self.path == "/auth/renew":
@@ -632,6 +632,18 @@ def test_context_and_related_use_identity_inputs(mod):
mod.find_related_nodes({"path": "/notes/root"})
def test_local_helper_ui_forms_submit_identity_for_existing_node_actions():
source = (pathlib.Path(__file__).resolve().parents[1] / "resources" / "scripts" / "napseer_mcp_server.py").read_text()
assert "name='node_id' value='{html.escape(item.get('id') or '')}'" in source
assert 'name="source_node_id"' in source
assert 'name="target_node_id"' in source
assert 'archive_node_by_path({"node_id": form.get("node_id", [""])[0]})' in source
assert '"source_node_id": form.get("source_node_id", [""])[0]' in source
assert '"target_node_id": form.get("target_node_id", [""])[0]' in source
assert 'name="source_path"' not in source
assert 'name="target_path"' not in source
def test_discovery_rejects_full_content_view(mod):
with pytest.raises(RuntimeError, match="full node bodies require nap_node_get"):
mod.list_project_nodes({"view": "full"})