From 43f735fe3ce63a0de7d3805d66a748cb5fa8c3a2 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:29:47 -0500 Subject: [PATCH] feat(tui-v2): implement true profile tree hierarchy with expand/collapse Added _profile_tree_expanded state to track node visibility. Implemented action_profile_tree_toggle and profile-tree-toggle command. Bound 'space' and 'right' keys to toggle tree nodes. Added [+] / [-] visual indicators to profile nodes. Updated interactive agent test to verify hierarchy logic across collapse/expand transitions. --- scripts/interactive_agent_tui_test.py | 25 +++++++++++++ src/seedpass/tui_v2/app.py | 53 ++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/scripts/interactive_agent_tui_test.py b/scripts/interactive_agent_tui_test.py index d4863ebb..18eab9f7 100644 --- a/scripts/interactive_agent_tui_test.py +++ b/scripts/interactive_agent_tui_test.py @@ -158,6 +158,10 @@ async def run_full_walkthrough(): app._run_palette_command('add-password "Gmail" 20') app._run_palette_command('add-totp "Bank"') app._run_palette_command('add-document "Readme" md "Hello World"') + # Add managed/agent for hierarchy + service.add_entry("SubUser", 10, kind="managed_account") + service.add_entry("MyAgent", 10, kind="nostr") + await pilot.pause() print(f" [OK] Created {len(service.entries)} entries.") @@ -197,6 +201,27 @@ async def run_full_walkthrough(): print(f" [DEBUG] QR Panel Content:\n{qr_text}") print("\n--- PHASE 3: System Operations ---") + # Tree Hierarchy + print(" Testing tree hierarchy...") + # Initial state: current profile expanded (auto-expand logic) + nodes_before = app._profile_tree_visible_nodes() + has_child = any(n.get("kind") in {"managed", "agent"} for n in nodes_before) + print(f" [OK] Child nodes visible initially: {has_child}") + + # Collapse via palette + app._run_palette_command("profile-tree-toggle") + await pilot.pause() + nodes_after = app._profile_tree_visible_nodes() + has_child_after = any(n.get("kind") in {"managed", "agent"} for n in nodes_after) + print(f" [OK] Child nodes hidden after collapse: {not has_child_after}") + + # Expand via palette + app._run_palette_command("profile-tree-toggle") + await pilot.pause() + nodes_final = app._profile_tree_visible_nodes() + has_child_final = any(n.get("kind") in {"managed", "agent"} for n in nodes_final) + print(f" [OK] Child nodes restored after expand: {has_child_final}") + # Managed Account service.add_entry("Sub", 10, kind="managed_account") app._load_entries("") diff --git a/src/seedpass/tui_v2/app.py b/src/seedpass/tui_v2/app.py index ed940b8d..af6a9c27 100644 --- a/src/seedpass/tui_v2/app.py +++ b/src/seedpass/tui_v2/app.py @@ -345,6 +345,8 @@ def launch_tui2( ("down", "profile_tree_next", "Profile Next"), ("ctrl+o", "profile_tree_open", "Profile Open"), ("u", "profile_tree_preview", "Profile Preview"), + ("space", "profile_tree_toggle", "Toggle Node"), + ("right", "profile_tree_toggle", "Expand"), ("p", "prev_page", "Prev Page"), ("n", "next_page", "Next Page"), ("l", "cycle_link_filter", "Link Filter"), @@ -505,6 +507,7 @@ def launch_tui2( self._density_mode = "compact" self._profile_tree_items: list[str] = [] self._profile_tree_cursor = 0 + self._profile_tree_expanded: dict[str, bool] = {} self._compact_layout = False self._dense_hires_layout = False self._viewport_width = 0 @@ -789,11 +792,21 @@ def launch_tui2( profiles = [current_fp or "(default)"] for fp in profiles: nodes.append({"kind": "profile", "fingerprint": fp}) + # Auto-expand the active profile if not explicitly tracked + if fp not in self._profile_tree_expanded: + self._profile_tree_expanded[fp] = (fp == current_fp) + + if not self._profile_tree_expanded.get(fp, False): + continue + + # Only show child nodes for the ACTIVE profile branch for now + # (since searching across all profiles is expensive) is_active_branch = fp == current_fp or ( not current_fp and fp == "(default)" ) if not is_active_branch: continue + for entry_id, label in managed_nodes[:3]: nodes.append( { @@ -1487,6 +1500,11 @@ def launch_tui2( label = item if item != "(default)" else "default" fp_short = item if len(item) <= 12 else f"{item[:9]}..." active_branch = item == current_fp + + # Add expand/collapse indicator + expanded = self._profile_tree_expanded.get(item, False) + exp_ind = "[-] " if expanded else "[+] " + branch_suffix = "" if active_branch: managed_count = len( @@ -1496,8 +1514,9 @@ def launch_tui2( [n for n in nodes if str(n.get("kind")) == "agent"] ) branch_suffix = f" M:{managed_count} A:{agent_count}" + tree_lines.append( - f"{cursor} {marker} {label[:16]:<16} | Seed: {fp_short}{branch_suffix}" + f"{cursor} {marker} {exp_ind}{label[:12]:<12} | Seed: {fp_short}{branch_suffix}" ) elif kind == "managed": if not rendered_managed_header: @@ -4463,6 +4482,13 @@ def launch_tui2( self.action_profile_tree_open() return + if cmd == "profile-tree-toggle": + if args: + self._set_status("Usage: profile-tree-toggle") + return + self.action_profile_tree_toggle() + return + if cmd == "setting-secret": if self._config_service is None: self._set_status("Config service unavailable") @@ -5751,6 +5777,31 @@ def launch_tui2( self._update_filters_panel() self._set_status(self._profile_tree_selection_text(selected)) + def action_profile_tree_toggle(self) -> None: + if self._sidebar_collapsed: + self._set_status("Sidebar is collapsed. Press Ctrl+B to expand.") + return + if self._focus_pane != "left": + self._set_status("Focus left pane first (press 1)") + return + self._refresh_profile_tree() + nodes = self._profile_tree_visible_nodes() + if not nodes: + return + self._profile_tree_cursor = min( + max(0, self._profile_tree_cursor), len(nodes) - 1 + ) + selected = nodes[self._profile_tree_cursor] + if selected.get("kind") == "profile": + fp = str(selected.get("fingerprint", "")) + is_expanded = self._profile_tree_expanded.get(fp, False) + self._profile_tree_expanded[fp] = not is_expanded + state = "Expanded" if not is_expanded else "Collapsed" + self._update_filters_panel() + self._set_status(f"{state} profile branch: {fp}") + else: + self._set_status("Only profile nodes can be expanded/collapsed") + def action_focus_center(self) -> None: self._focus_pane = "center" self._apply_focus_style()