From 0ec4b860767481b2f01ca6c5789fe4823d681439 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:24:58 -0500 Subject: [PATCH] feat(tui-v2): polish action strip copy and rebalance dense layout --- docs/tui_v2_ui_refresh_plan.md | 93 +++++++++++++++ memory-update.md | 65 ++++++++++ src/seedpass/tui_v2/app.py | 111 +++++++++++++----- src/tests/test_tui_v2_textual_interactions.py | 71 ++++++++++- 4 files changed, 306 insertions(+), 34 deletions(-) diff --git a/docs/tui_v2_ui_refresh_plan.md b/docs/tui_v2_ui_refresh_plan.md index ad478b5f..acf02d10 100644 --- a/docs/tui_v2_ui_refresh_plan.md +++ b/docs/tui_v2_ui_refresh_plan.md @@ -1073,3 +1073,96 @@ Still needed to match mockup behavior intent: - denser row/column presentation, - reduced decorative padding. 3. Next implementation slices should target this density path to effectively fit more content per viewport. + +## 29) Progress Update (2026-03-03, Slice: Password/Stored Password/Note Card Geometry) + +Completed: + +1. Password and Stored Password inspector boards now use stronger card sections: +- `Credentials` +- `Quick Actions` +2. Note/Document board now uses the same card section framing: +- `Content` +- `Quick Actions` +3. Increased note preview payload from 120 to 180 chars for better content-card parity in standard density. + +Code references: + +1. `src/seedpass/tui_v2/app.py` (`_board_card`, password/stored_password board, note/document board) + +Regression coverage: + +1. Updated note board metadata test to assert card framing. +2. Added password board card-section regression test. + +Validation: + +1. `poetry run pytest -q src/tests/test_tui_v2_textual_interactions.py src/tests/test_tui_v2_action_matrix.py` +2. Result: `57 passed`. + +Remaining strict-closeout slice order unchanged: + +1. Final UI-board density pass (table rows + lower inspector balance) +2. Final action-strip verb/microcopy polish + +## 30) Progress Update (2026-03-03, Slice: UI-Board Density Rebalance) + +Completed: + +1. Dense/high-res layout now rebalances vertical space by context: +- Idle (no selected entry, no editor, no 2FA board): `top-work=9fr`, `right=3fr` +- Active inspect/edit/2FA: `top-work=8fr`, `right=4fr` +2. Added explicit layout rebalance hook so selection and pane-mode transitions re-apply split without requiring window resize. +3. Rebalance path wired through: +- page render transitions (empty/non-empty) +- entry selection/open flow +- right-pane mode switches (view/edit/2FA) + +Code references: + +1. `src/seedpass/tui_v2/app.py` (`_update_responsive_layout`, `_refresh_layout_balance`, `_set_right_pane_mode`, `_render_current_page`, `_show_entry`) + +Regression coverage: + +1. Added dense-mode split behavior test: +- `test_tui2_textual_hires_density_rebalances_idle_vs_selected` + +Validation: + +1. `poetry run pytest -q src/tests/test_tui_v2_textual_interactions.py src/tests/test_tui_v2_action_matrix.py` +2. Result: `58 passed`. + +Remaining strict-closeout slice: + +1. Final action-strip verb/microcopy polish. + +## 31) Progress Update (2026-03-03, Slice: Action-Strip Verb/Microcopy Polish) + +Completed: + +1. Context row wording standardized to verb-first labels with key hints: +- `Reveal (v)` +- `QR (g)` +- `Edit (e)` / `Edit Doc (e)` +- `Archive (a)` +- `Save (Ctrl+S)` +- `2FA Board (6)` +2. Managed account row now retains explicit session controls while matching the same style: +- `managed-load` +- `managed-exit` +- plus reveal/qr/edit/archive with key hints. +3. Click routing remains compatible after copy update: +- row-2 parser now recognizes both symbol-led and verb-led tokens (`v`/`Reveal`, `g`/`QR`, etc.). + +Code references: + +1. `src/seedpass/tui_v2/app.py` (`_update_action_strip`, `_action_strip_context_action`) + +Regression coverage: + +1. Updated action-strip click + context-update tests to assert verb-led labels. + +Validation: + +1. `poetry run pytest -q src/tests/test_tui_v2_textual_interactions.py src/tests/test_tui_v2_action_matrix.py` +2. Result: `58 passed`. diff --git a/memory-update.md b/memory-update.md index 1a22f738..e8718afd 100644 --- a/memory-update.md +++ b/memory-update.md @@ -110,3 +110,68 @@ - Focused and gate suite green: - `poetry run pytest -q src/tests/test_tui_v2_textual_interactions.py src/tests/test_tui_v2_action_matrix.py` - Result: `56 passed`. + +## Session Update (2026-03-03, Slice 2) + +### UI-board density and hierarchy polish +- Dense high-resolution mode now keeps `#grid-heading` at 4 rows instead of 3 to avoid header/divider clipping in large viewports. +- Refined dense top-left header hierarchy to reduce line noise: + - retained explicit `Fingerprint` and `Path` + - consolidated session/profile/managed state into one line + - kept scope line focused on active filters only + +### Regression updates +- Updated `test_tui2_textual_hires_density_compacts_vertical_chrome` to assert the new dense grid-heading height (`4`). + +### Verification +- Focused TUI v2 gate remains green: + - `poetry run pytest -q src/tests/test_tui_v2_textual_interactions.py src/tests/test_tui_v2_action_matrix.py` + - Result: `56 passed`. + +## Session Update (2026-03-03, Slice 3) + +### Board geometry parity pass +- Added reusable ASCII card renderer for inspector sections (`_board_card`). +- Password and stored-password boards now render framed `Credentials` and `Quick Actions` blocks. +- Note/document boards now render framed `Content` and `Quick Actions` blocks. +- Increased note/document preview length (120 -> 180 chars) for better mockup-like content emphasis. + +### Regression coverage +- Extended note metadata board test to assert card framing. +- Added password board card-section test. + +### Verification +- Focused TUI v2 gate: + - `poetry run pytest -q src/tests/test_tui_v2_textual_interactions.py src/tests/test_tui_v2_action_matrix.py` + - Result: `57 passed`. + +## Session Update (2026-03-03, Slice 4) + +### UI-board density rebalancing +- Added dynamic dense/high-res vertical split: + - idle: `top-work=9fr`, `right=3fr` + - active inspect/edit/2FA: `top-work=8fr`, `right=4fr` +- Added `_refresh_layout_balance()` and invoked it during key state transitions so layout rebalance is state-driven, not resize-driven. + +### Regression coverage +- Added `test_tui2_textual_hires_density_rebalances_idle_vs_selected`. + +### Verification +- Focused TUI v2 gate: + - `poetry run pytest -q src/tests/test_tui_v2_textual_interactions.py src/tests/test_tui_v2_action_matrix.py` + - Result: `58 passed`. + +## Session Update (2026-03-03, Slice 5) + +### Action-strip microcopy polish +- Standardized context action wording to verb-first with key hints across entry kinds. +- Preserved managed account session actions while aligning adjacent wording style. +- Expanded row-2 click parser to recognize verb-led labels so copy changes do not break mouse interactions. + +### Regression coverage +- Updated action-strip context tests to match polished copy. + +### Verification +- Focused TUI v2 gate: + - `poetry run pytest -q src/tests/test_tui_v2_textual_interactions.py src/tests/test_tui_v2_action_matrix.py` + - Result: `58 passed`. diff --git a/src/seedpass/tui_v2/app.py b/src/seedpass/tui_v2/app.py index 489c717a..0df0b4a1 100644 --- a/src/seedpass/tui_v2/app.py +++ b/src/seedpass/tui_v2/app.py @@ -884,21 +884,23 @@ def launch_tui2( kind_l = self._entry_kind(self._selected_entry) kind = kind_l if kind_l in {"password", "stored_password"}: - context = "Entry ▣ v Reveal ▣ g QR ▣ e Edit ▣ a Archive" + context = "Entry ▣ Reveal (v) ▣ QR (g) ▣ Edit (e) ▣ Archive (a)" elif kind_l == "managed_account": - context = "Entry ▣ managed-load ▣ managed-exit ▣ v Reveal (confirm) ▣ g QR ▣ e Edit ▣ a Archive" + context = "Entry ▣ managed-load ▣ managed-exit ▣ Reveal (v confirm) ▣ QR (g) ▣ Edit (e) ▣ Archive (a)" elif kind_l == "seed": - context = "Entry ▣ v Reveal (confirm) ▣ g QR ▣ e Edit ▣ a Archive" + context = "Entry ▣ Reveal (v confirm) ▣ QR (g) ▣ Edit (e) ▣ Archive (a)" elif kind_l == "totp": - context = "Entry ▣ 6 2FA Board ▣ v Reveal ▣ g QR ▣ a Archive" + context = "Entry ▣ 2FA Board (6) ▣ Reveal (v) ▣ QR (g) ▣ Archive (a)" elif kind_l in {"ssh", "pgp"}: - context = "Entry ▣ v Reveal (confirm) ▣ a Archive ▣ copy/export-field" + context = "Entry ▣ Reveal (v confirm) ▣ Archive (a) ▣ copy/export-field" elif kind_l == "nostr": - context = "Entry ▣ v Reveal ▣ g QR (public/private) ▣ a Archive" + context = "Entry ▣ Reveal (v) ▣ QR (g public/private) ▣ Archive (a)" elif kind_l in {"document", "note"}: - context = "Entry ▣ e Edit Doc ▣ Ctrl+S Save ▣ a Archive ▣ doc-export" + context = ( + "Entry ▣ Edit Doc (e) ▣ Save (Ctrl+S) ▣ Archive (a) ▣ doc-export" + ) elif kind_l == "key_value": - context = "Entry ▣ set-field/clear-field ▣ a Archive ▣ notes/tags" + context = "Entry ▣ set-field/clear-field ▣ Archive (a) ▣ notes/tags" else: context = "Select an entry to view context actions." if kind: @@ -985,15 +987,15 @@ def launch_tui2( if not token: return None lead = token.split()[0].strip().lower() - if lead == "v": + if lead in {"v", "reveal", "rev"}: return "reveal" - if lead == "g": + if lead in {"g", "qr"}: return "qr" - if lead == "e": + if lead in {"e", "edit"}: return "edit" - if lead == "a": + if lead in {"a", "archive", "arch"}: return "archive" - if lead == "6": + if lead in {"6", "2fa"}: return "totp_board" if lead in {"managed-load", "managed_load", "ml"}: return "managed_load" @@ -1243,13 +1245,30 @@ def launch_tui2( right.styles.height = "5fr" activity.add_class("hidden") elif height >= 52: - top_work.styles.height = "8fr" - right.styles.height = "4fr" + if dense_hires and ( + self._selected_entry_id is None + and not self.editing_document + and not self.totp_board_open + ): + # In dense high-res idle state, bias vertical space to the grid. + top_work.styles.height = "9fr" + right.styles.height = "3fr" + else: + top_work.styles.height = "8fr" + right.styles.height = "4fr" activity.remove_class("hidden") activity.styles.height = 4 else: - top_work.styles.height = "7fr" - right.styles.height = "4fr" + if dense_hires and ( + self._selected_entry_id is None + and not self.editing_document + and not self.totp_board_open + ): + top_work.styles.height = "8fr" + right.styles.height = "3fr" + else: + top_work.styles.height = "7fr" + right.styles.height = "4fr" activity.remove_class("hidden") activity.styles.height = 3 @@ -1260,7 +1279,7 @@ def launch_tui2( ribbon.styles.height = 2 status.styles.height = 2 action_strip.styles.height = 2 - grid_heading.styles.height = 3 + grid_heading.styles.height = 4 inspector_heading.styles.height = 2 else: brand.styles.height = 3 @@ -1280,6 +1299,11 @@ def launch_tui2( except Exception: pass + def _refresh_layout_balance(self) -> None: + self._update_responsive_layout( + width=self._viewport_width, height=self._viewport_height + ) + def _refresh_doc_edit_help(self) -> None: marker = "*" if self._doc_dirty else "clean" self.query_one("#doc-edit-help", Static).update( @@ -1535,8 +1559,15 @@ def launch_tui2( "SeedPass ◈ TUI v2", f"Fingerprint: {current_fp}", f"Path: {self._session_breadcrumb_text()}", - f"FP: {current_fp} | Profiles: {profiles_loaded} | Session: {session_state}", - f"Scope: filter={self.filter_kind} arch={self.archive_scope} links={self.link_relation_filter} managed={managed_state}", + ( + "Session: " + f"{session_state} | Profiles: {profiles_loaded} | Managed: {managed_state}" + ), + ( + "Scope: " + f"filter={self.filter_kind} arch={self.archive_scope} " + f"links={self.link_relation_filter}" + ), self._selected_summary(), ( f"Results: {len(self._all_results)} | " @@ -1742,6 +1773,7 @@ def launch_tui2( else: self._set_status("No entries match current filter/search") self._update_filters_panel() + self._refresh_layout_balance() return self._result_page, start, end, _total_pages = pagination_window( @@ -1791,6 +1823,7 @@ def launch_tui2( pass self._set_status("Entries loaded. Select an entry to inspect.") self._update_filters_panel() + self._refresh_layout_balance() def _entry_detail_text(self, entry: dict[str, Any]) -> str: payload = truncate_entry_for_display( @@ -1865,6 +1898,14 @@ def launch_tui2( f"Notes: {notes_preview}", ] + @staticmethod + def _board_card(title: str, rows: list[str]) -> list[str]: + width = max([len(title), *[len(row) for row in rows], 12]) + top = f"+- {title} " + "-" * max(1, width - len(title) - 2) + "+" + body = [f"| {row.ljust(width)} |" for row in rows] + bottom = "+" + "-" * (width + 2) + "+" + return [top, *body, bottom] + def _entry_board_header(self, entry: dict[str, Any]) -> list[str]: entry_id = ( self._selected_entry_id @@ -1919,6 +1960,13 @@ def launch_tui2( archived = "Yes" if bool(entry.get("archived", False)) else "No" index_num = str(entry.get("index", "(auto)")) label = str(entry.get("label") or "(untitled)") + credential_rows = [ + "Password*: hidden (v reveal, g qr)", + f"Username*: {username}", + f"URL : {url}", + f"Length : {length} chars", + ] + operation_rows = [action_row] lines = [ f"Entry #{entry_id} {label}", "-" * max(24, len(label) + 10), @@ -1926,12 +1974,9 @@ def launch_tui2( f"Kind: {kind} | Modified: {modified} | Archived: {archived}", f"Index Num*: {index_num}", "Login Fields", - "Password*: hidden (v reveal, g qr)", - f"Username*: {username}", - f"URL : {url}", - f"Length : {length} chars", + *self._board_card("Credentials", credential_rows), "Operations", - action_row, + *self._board_card("Quick Actions", operation_rows), *self._notes_tags_panel_hint( tags_text=tags_text, notes_text=notes_text ), @@ -1948,7 +1993,7 @@ def launch_tui2( content = str(entry.get("content") or "") preview = content.strip() if preview: - preview = preview.replace("\n", " ")[:120] + preview = preview.replace("\n", " ")[:180] else: preview = "(empty)" entry_id = ( @@ -1964,6 +2009,10 @@ def launch_tui2( ) archived = "Yes" if bool(entry.get("archived", False)) else "No" index_num = str(entry.get("index", "(auto)")) + document_rows = [ + f"Content Length: {len(content)} chars", + f"Preview : {preview}", + ] lines = [ f"Entry #{entry_id} {str(entry.get('label') or '(untitled)')}", "-" * 28, @@ -1971,10 +2020,9 @@ def launch_tui2( f"Kind: {kind} | Modified: {modified} | Archived: {archived}", f"Index Num*: {index_num} | File Type: {file_type}", "Document Fields", - f"Content Length: {len(content)} chars", - f"Preview : {preview}", + *self._board_card("Content", document_rows), "Operations", - "▣ Edit ▣ Export", + *self._board_card("Quick Actions", ["▣ Edit ▣ Export"]), *self._notes_tags_panel_hint( tags_text=tags_text, notes_text=notes_text ), @@ -2397,6 +2445,7 @@ def launch_tui2( self._update_links_panel() self._update_filters_panel() self._set_status(f"Selected entry {entry_index}") + self._refresh_layout_balance() except Exception as exc: self.query_one("#entry-detail", Static).update( f"Failed to load entry {entry_index}: {exc}" @@ -2415,6 +2464,7 @@ def launch_tui2( retry=lambda: self._show_entry(entry_index), hint="Press 'x' to retry.", ) + self._refresh_layout_balance() def _update_links_panel(self) -> None: if self._service is None or self._selected_entry_id is None: @@ -2926,6 +2976,7 @@ def launch_tui2( self.totp_board_open = False self.editing_document = True self._update_inspector_heading() + self._refresh_layout_balance() return if mode_token == "totp": editor.add_class("hidden") @@ -2937,6 +2988,7 @@ def launch_tui2( self.editing_document = False self._refresh_totp_board(force_reload=True) self._update_inspector_heading() + self._refresh_layout_balance() return # Default "view": entry board + side panels, no editor/2FA board. if mode_token != "view": @@ -2950,6 +3002,7 @@ def launch_tui2( self.totp_board_open = False self.editing_document = False self._update_inspector_heading() + self._refresh_layout_balance() def _set_palette_visible(self, visible: bool) -> None: palette = self.query_one("#command-palette", Input) diff --git a/src/tests/test_tui_v2_textual_interactions.py b/src/tests/test_tui_v2_textual_interactions.py index b9879369..8c37dff8 100644 --- a/src/tests/test_tui_v2_textual_interactions.py +++ b/src/tests/test_tui_v2_textual_interactions.py @@ -1317,7 +1317,7 @@ async def test_tui2_textual_hires_density_compacts_vertical_chrome() -> None: assert str(ribbon.styles.height) == "2" assert str(status.styles.height) == "2" assert str(action_strip.styles.height) == "2" - assert str(grid_heading.styles.height) == "3" + assert str(grid_heading.styles.height) == "4" assert "Dense" in _widget_text(app, "#action-strip") assert "S Set" in _widget_text(app, "#action-strip") assert "Keys:" in _widget_text(app, "#filters") @@ -1338,6 +1338,37 @@ async def test_tui2_textual_hires_density_compacts_vertical_chrome() -> None: assert "Dense" not in _widget_text(app, "#action-strip") +@pytest.mark.anyio +async def test_tui2_textual_hires_density_rebalances_idle_vs_selected() -> None: + service = FakeEntryService( + [ + {"id": 1, "kind": "password", "label": "Login 1", "length": 16}, + {"id": 2, "kind": "document", "label": "Doc 2", "content": "hello"}, + ] + ) + app = _build_app(service) + + async with app.run_test() as pilot: + await pilot.pause() + top_work = app.query_one("#top-work") + right = app.query_one("#right") + + app._update_responsive_layout(width=220, height=55) + await pilot.pause() + assert str(top_work.styles.height) == "9fr" + assert str(right.styles.height) == "3fr" + + app._run_palette_command("open 1") + await pilot.pause() + assert str(top_work.styles.height) == "8fr" + assert str(right.styles.height) == "4fr" + + app._set_totp_board_visible(True) + await pilot.pause() + assert str(top_work.styles.height) == "8fr" + assert str(right.styles.height) == "4fr" + + @pytest.mark.anyio async def test_tui2_textual_dense_mode_reduces_inspector_helper_noise() -> None: service = FakeEntryService( @@ -1501,6 +1532,8 @@ async def test_tui2_textual_note_and_totp_boards_include_common_metadata() -> No assert "Kind: document | Modified:" in board assert "Index Num*:" in board assert "Document Fields" in board + assert "+- Content " in board + assert "+- Quick Actions " in board app._run_palette_command("open 2") await pilot.pause() @@ -1511,6 +1544,34 @@ async def test_tui2_textual_note_and_totp_boards_include_common_metadata() -> No assert "2FA Fields" in board +@pytest.mark.anyio +async def test_tui2_textual_password_board_uses_card_sections() -> None: + service = FakeEntryService( + [ + { + "id": 1, + "kind": "password", + "label": "Login 1", + "username": "alice", + "url": "https://example.com", + "length": 18, + } + ] + ) + app = _build_app(service) + + async with app.run_test() as pilot: + await pilot.pause() + app._run_palette_command("open 1") + await pilot.pause() + board = _widget_text(app, "#entry-detail") + assert "Password Board" in board + assert "Login Fields" in board + assert "+- Credentials " in board + assert "Password*: hidden (v reveal, g qr)" in board + assert "+- Quick Actions " in board + + @pytest.mark.anyio async def test_tui2_textual_filters_panel_tracks_selected_entry() -> None: service = FakeEntryService( @@ -1682,9 +1743,9 @@ async def test_tui2_textual_action_strip_click_routes_to_shortcuts() -> None: app._run_palette_command("open 1") await pilot.pause() action_line = _widget_text(app, "#action-strip").splitlines()[1] - reveal_col = action_line.find("v Reveal") + reveal_col = action_line.find("Reveal (v") if reveal_col < 0: - reveal_col = action_line.find("v Rev") + reveal_col = action_line.find("Rev (v") assert reveal_col >= 0 handled = app._handle_action_strip_click(reveal_col + 1, 1) await pilot.pause() @@ -1707,13 +1768,13 @@ async def test_tui2_textual_action_strip_context_updates_for_selected_kind() -> app._run_palette_command("open 1") await pilot.pause() action_line = _widget_text(app, "#action-strip").splitlines()[1] - assert ("v Reveal" in action_line) or ("v Rev" in action_line) + assert ("Reveal (v)" in action_line) or ("Rev (v)" in action_line) assert "(password)" in action_line app._run_palette_command("open 2") await pilot.pause() action_line = _widget_text(app, "#action-strip").splitlines()[1] - assert "e Edit Doc" in action_line + assert "Edit Doc (e)" in action_line assert "(document)" in action_line