TUI v3: tighten action strip + restore/import guidance (SeedPass-57f6)

action_bar.py:
- Fix global row broken handlers: Remove Seed → open_profile_management,
  Backup Data → open_backup_parent_seed, Export/Import Data → open_palette
  (export/import need a path argument, palette is the right entry point)

add.py:
- seedplus-hint: mention BIP-85 explicitly, reference Seed+ (C) action
- AddEntryScreen success notify: include reveal/copy shortcut hint
- SeedPlusScreen: add #seedplus-warning label explaining deterministic
  derivation and index uniqueness requirement
- SeedPlusScreen success notify: mention index and parent seed

app.py:
- RecoverProfileScreen: set success status before bootstrap fires
- CreateProfileScreen: add intent messaging for nostr/backup restore modes
  with progress description and post-success status

grid.py:
- Add stored_password to secrets filter kind_map (was missing)

tests:
- test_tui_v3_parity: add test_v3_action_bar_global_row_uses_correct_handlers
- test_tui_v3_smoke: add seedplus-hint BIP-85 check, seedplus-warning label
  check, recover success-status check

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Sonnet 4.6 committed 2026-03-06 13:36:34 -05:00
1 parent ca31abf33e
commit 446d1ef2b8
6 files changed
+117 -7

No files matched your search

+15 -2
View File
@@ -508,13 +508,17 @@ class CreateProfileScreen(Screen):
self._set_status("Enter the seed phrase for Nostr restore.")
return
self._set_status(
"Attempting Nostr restore for the supplied seed profile..."
"Restoring from Nostr — this replays remote events onto the seed. "
"Your entries will be recovered from the network. This may take a moment..."
)
fingerprint = self.app._restore_from_nostr_profile(
seed=seed,
password=password,
continue_without_backup=continue_without_backup,
)
self._set_status(
f"Nostr restore complete ({fingerprint[:12]}) — launching session."
)
elif mode == "backup":
if not seed:
self._set_status("Enter the seed phrase for backup restore.")
@@ -523,13 +527,16 @@ class CreateProfileScreen(Screen):
self._set_status("Enter the encrypted backup path for backup mode.")
return
self._set_status(
"Attempting local backup restore from the supplied path..."
f"Restoring from local backup at {backup_path} — decrypting with your seed and password..."
)
fingerprint = self.app._restore_from_backup_profile(
seed=seed,
password=password,
backup_path=backup_path,
)
self._set_status(
f"Backup restore complete ({fingerprint[:12]}) — launching session."
)
else:
if not seed:
self._set_status("Enter the seed phrase to import.")
@@ -538,6 +545,9 @@ class CreateProfileScreen(Screen):
seed=seed,
password=password,
)
self._set_status(
f"Profile imported ({fingerprint[:12]}) — launching session."
)
self.app._bootstrap_profile_session(fingerprint, password)
except Exception as e:
self._set_status(f"Profile creation failed: {e}")
@@ -750,6 +760,9 @@ class RecoverProfileScreen(Screen):
seed=seed,
password=password,
)
self._set_status(
f"Recovery successful for {fingerprint[:12]} — launching profile session."
)
self.app._bootstrap_profile_session(fingerprint, password)
except Exception as e:
self._set_status(f"Recovery failed: {e}")
+8 -3
View File
@@ -94,7 +94,7 @@ class AddEntryScreen(Screen):
kinds = [(k.value.title(), k.value) for k in supported_kinds]
yield Select(kinds, value="password", id="kind-select")
yield Label(
"Use `seed-plus` for Seed / Managed Account derivations.",
"Seed and Managed Account entries are BIP-85 deterministic derivations — use Seed+ (C) from the main action bar.",
id="seedplus-hint",
)
@@ -215,7 +215,7 @@ class AddEntryScreen(Screen):
)
return
self.app.notify(f"Added {kind}: {label}")
self.app.notify(f"'{label}' ({kind}) saved — press v to reveal or c to copy.")
self.app.action_refresh()
self.app.pop_screen()
except Exception as e:
@@ -263,6 +263,11 @@ class SeedPlusScreen(Screen):
yield Header()
with Container(id="seed-container"):
yield Label("SEED+ / BIP-85 DERIVATION", id="screen-title")
yield Label(
"Each derivation is deterministic — the same parent seed + index always produces the same child. "
"Choose a unique index per derivation. This entry cannot be deleted without losing the derived key.",
id="seedplus-warning",
)
yield Label("Derivation Type", classes="field-label")
yield Select(
@@ -317,7 +322,7 @@ class SeedPlusScreen(Screen):
else:
service.add_managed_account(label=label, index=index, tags=tags)
self.app.notify(f"Derived {kind}: {label} (Index {index})")
self.app.notify(f"'{label}' ({kind}) derived at index {index} — reproducible from your parent seed.")
self.app.action_refresh()
self.app.pop_screen()
except Exception as e:
+2 -2
View File
@@ -46,8 +46,8 @@ class ActionBar(Static):
f"[@click='app.toggle_settings'][b]S[/b]ettings[/] [@click='app.add_entry'][b]A[/b]dd New Entry[/] "
f"[@click='app.seed_plus'][b]C[/b]reate New Seed[/] "
f"[@click='app.open_atlas_wayfinder'][b]W[/b]ayfinder[/] "
f"[@click='app.open_palette'][b]R[/b]emove Seed[/] "
f"[@click='app.action_db_export'][b]E[/b]xport Data[/] [@click='app.action_db_import'][b]I[/b]mport Data[/] [@click='app.open_palette'][b]B[/b]ackup Data[/]{exit_hint}"
f"[@click='app.open_profile_management'][b]R[/b]emove Seed[/] "
f"[@click='app.open_palette'][b]E[/b]xport Data[/] [@click='app.open_palette'][b]I[/b]mport Data[/] [@click='app.open_backup_parent_seed'][b]B[/b]ackup Data[/]{exit_hint}"
)
# Context Row (added from previous phase parity)
+1
View File
@@ -145,6 +145,7 @@ class EntryDataTable(DataTable):
kind_map = {
"secrets": [
"password",
"stored_password",
"totp",
"ssh",
"pgp",
+24
View File
@@ -281,6 +281,30 @@ async def test_v3_action_bar_labels_include_leading_hotkey_letters() -> None:
assert "Backup Data" in rendered
@pytest.mark.anyio
async def test_v3_action_bar_global_row_uses_correct_handlers() -> None:
app = SeedPassTuiV3(
entry_service_factory=lambda: MockEntryService(),
vault_service_factory=lambda: MockVaultService(),
)
async with app.run_test() as pilot:
await pilot.pause()
bar = app.screen.query_one("#action-bar")
markup = bar.render()
# Backup Data must route to open_backup_parent_seed (has its own screen)
assert "open_backup_parent_seed" in markup
# Remove Seed must route to open_profile_management (not open_palette)
assert "open_profile_management" in markup
# Export/Import Data use open_palette because they need a path argument
# Verify both labels still present
from rich.text import Text
rendered = Text.from_markup(markup).plain
assert "Export Data" in rendered
assert "Import Data" in rendered
assert "Backup Data" in rendered
assert "Remove Seed" in rendered
@pytest.mark.anyio
async def test_v3_action_bar_only_shows_context_actions_for_selected_kind() -> None:
app = SeedPassTuiV3(
+67
View File
@@ -968,6 +968,73 @@ async def test_tui3_word_by_word_seed_entry_feeds_create_profile_screen() -> Non
)
@pytest.mark.anyio
async def test_tui3_recover_profile_sets_success_status_before_bootstrap() -> None:
app = SeedPassTuiV3()
app._list_boot_profiles = lambda: [
{"fingerprint": "EFBE51E70ED1B53A", "label": "Primary (EFBE51E70ED1B53A)"}
]
booted: dict[str, str] = {}
status_at_boot: list[str] = []
def _recover_profile(*, fingerprint: str, seed: str, password: str) -> None:
pass
def _bootstrap(fingerprint: str, password: str) -> None:
booted["fingerprint"] = fingerprint
# Capture status text at the moment bootstrap fires
try:
status_at_boot.append(
str(app.screen.query_one("#recover-status").render())
)
except Exception:
pass
app._recover_profile = _recover_profile
app._bootstrap_profile_session = _bootstrap
async with app.run_test() as pilot:
await pilot.pause()
app.push_screen(RecoverProfileScreen())
await pilot.pause()
screen = app.screen
screen.query_one("#recover-choice", Input).value = "1"
screen.query_one("#recover-seed", Input).value = (
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
)
screen.query_one("#recover-password", Input).value = "hunter2"
await pilot.press("enter")
await pilot.pause()
assert booted.get("fingerprint") == "EFBE51E70ED1B53A"
assert status_at_boot and "Recovery successful" in status_at_boot[0]
@pytest.mark.anyio
async def test_tui3_add_entry_screen_seedplus_hint_mentions_bip85() -> None:
app, _entry, _vault, _search = _build_app()
async with app.run_test() as pilot:
await pilot.pause()
app.action_add_entry()
await pilot.pause()
screen = app.screen
hint = str(screen.query_one("#seedplus-hint").render())
assert "BIP-85" in hint
assert "Seed+" in hint
@pytest.mark.anyio
async def test_tui3_seed_plus_screen_shows_deterministic_warning() -> None:
app, _entry, _vault, _search = _build_app()
async with app.run_test() as pilot:
await pilot.pause()
app.action_seed_plus()
await pilot.pause()
screen = app.screen
warning = str(screen.query_one("#seedplus-warning").render())
assert "deterministic" in warning.lower()
assert "index" in warning.lower()
@pytest.mark.anyio
async def test_tui3_word_entry_flags_invalid_bip39_word() -> None:
app = SeedPassTuiV3()