feat: wire semantic controls into legacy and tui v2 interfaces

This commit is contained in:
thePR0M3TH3AN committed 2026-03-02 11:52:30 -05:00
1 parent 165e2db204
commit ec407c611a
7 files changed
+532

No files matched your search

+66
View File
@@ -1060,3 +1060,69 @@
- Updated control-center priorities:
- keep Phase 5 TUI refresh completion ahead of semantic Phase A implementation
- semantic index Phase A now explicit next-track deliverable after current UI slice.
# Memory Update (2026-03-02)
## TUI v2 Phase 5 tree navigation parity landed
- Added explicit left-profile-tree keyboard navigation in `src/seedpass/tui_v2/app.py`:
- `Up` / `Down` for tree cursor movement
- `Ctrl+O` to open/switch the selected profile
- Added palette command parity/discoverability:
- `profile-tree-next`, `profile-tree-prev`, `profile-tree-open`
- Updated left panel rendering with cursor + active profile markers and refreshed tree state after profile add/remove/rename/switch.
- Extended Textual interaction + matrix tests:
- `src/tests/test_tui_v2_textual_interactions.py`
- `src/tests/test_tui_v2_action_matrix.py`
## Semantic index implementation started (Phase A/B baseline)
- Added local derived semantic index core:
- `src/seedpass/core/semantic_index.py`
- profile-local `semantic_index/manifest.json` + `records.json`
- token-overlap retrieval baseline, with high-risk kinds excluded by default (e.g., `seed`, `managed_account`).
- Added profile config flag and service wrappers:
- `semantic_index_enabled` default `false`
- `ConfigManager` + `ConfigService` getters/setters
- new `SemanticIndexService` in `src/seedpass/core/api.py`
- Added CLI command group:
- `seedpass semantic status|enable|disable|build|rebuild|search`
- Added API endpoints:
- `GET /api/v1/semantic/status`
- `POST /api/v1/semantic/build`
- `POST /api/v1/semantic/rebuild`
- `POST /api/v1/semantic/search`
- `POST /api/v1/semantic/config`
- Updated capabilities discovery for semantic CLI/API surfaces.
## Tests executed and passing
- `poetry run pytest -q src/tests/test_tui_v2_textual_interactions.py src/tests/test_tui_v2_action_matrix.py src/tests/test_semantic_index.py src/tests/test_cli_semantic.py src/tests/test_api_new_endpoints.py` => `95 passed`
- Expanded target including CLI capability coverage:
- `poetry run pytest -q src/tests/test_cli_semantic.py src/tests/test_api_new_endpoints.py src/tests/test_semantic_index.py src/tests/test_tui_v2_textual_interactions.py src/tests/test_tui_v2_action_matrix.py src/tests/test_typer_cli.py` => `149 passed`
## Docs/control-plane updates
- Updated semantic roadmap status/progress and phase checkboxes:
- `docs/semantic_vector_index_plan.md`
- Updated unified prioritization doc with current state and next queue:
- `docs/dev_control_center.md`
- Updated UI refresh plan to mark Phase 5 tree-navigation completion:
- `docs/tui_v2_ui_refresh_plan.md`
## Committed + pushed
- Branch: `beta`
- Commit: `165e2db`
- Message: `feat: add semantic index scaffolding, cli/api surfaces, and tui tree nav polish`
## Recommended immediate next slice
1. Implement semantic Phase C in both TUIs (status/build/search commands + visible semantic-state indicator in ribbon/settings).
2. Add incremental semantic index updates on entry/tag/link mutation paths (avoid rebuild-only behavior).
3. Add hybrid ranking mode contract (`keyword|hybrid|semantic`) across CLI/API/TUI with tests.
## Memory Update (2026-03-02, semantic Phase C slice)
- Wired semantic controls into TUI v2 palette and top ribbon semantic state (`SEM: off/stale/ready(n)/err/n/a`).
- Added TUI v2 commands: `semantic-status`, `semantic-enable`, `semantic-disable`, `semantic-build`, `semantic-rebuild`, `semantic-search <query>`.
- Wired semantic service bootstrap into CLI TUI launch path (`_prime_tui2_service` + `launch_tui2` factories).
- Added legacy TUI Settings option `18. Semantic Index` with submenu for status/enable/disable/build/rebuild/search.
- Added tests:
- `test_settings_menu.py` semantic submenu dispatch/search flow
- `test_tui_v2_textual_interactions.py` semantic command success/validation paths
- `test_tui_v2_action_matrix.py` semantic command matrix coverage
- Validation run: `102 passed` across targeted settings/TUI/semantic/CLI suites.
+119
View File
@@ -21,6 +21,7 @@ from utils.color_scheme import color_text
import importlib
from seedpass.core.manager import PasswordManager, restore_backup_index
from seedpass.core.api import SemanticIndexService
from seedpass.core.errors import SeedPassError
from nostr.client import NostrClient
from seedpass.core.entry_types import EntryType
@@ -1112,6 +1113,121 @@ def handle_nostr_menu(password_manager: PasswordManager) -> None:
pause()
def handle_semantic_index_menu(password_manager: PasswordManager) -> None:
"""Submenu for semantic index status/build/search controls."""
service = SemanticIndexService(password_manager)
while True:
fp, parent_fp, child_fp = getattr(
password_manager,
"header_fingerprint_args",
(getattr(password_manager, "current_fingerprint", None), None, None),
)
clear_header_with_notification(
password_manager,
fp,
"Main Menu > Settings > Semantic Index",
parent_fingerprint=parent_fp,
child_fingerprint=child_fp,
)
print(color_text("\nSemantic Index:", "menu"))
print(color_text("1. Show status", "menu"))
print(color_text("2. Enable", "menu"))
print(color_text("3. Disable", "menu"))
print(color_text("4. Build", "menu"))
print(color_text("5. Rebuild", "menu"))
print(color_text("6. Search", "menu"))
choice = input("Select an option or press Enter to go back: ").strip()
try:
if choice == "1":
payload = service.status()
print(
colored(
"Status: "
f"enabled={bool(payload.get('enabled', False))}, "
f"built={bool(payload.get('built', False))}, "
f"records={int(payload.get('records', 0))}",
"green",
)
)
pause()
elif choice == "2":
payload = service.set_enabled(True)
print(
colored(
f"Semantic index enabled (records={int(payload.get('records', 0))}).",
"green",
)
)
pause()
elif choice == "3":
payload = service.set_enabled(False)
print(
colored(
f"Semantic index disabled (records={int(payload.get('records', 0))}).",
"yellow",
)
)
pause()
elif choice == "4":
payload = service.build()
print(
colored(
f"Semantic index built with {int(payload.get('records', 0))} records.",
"green",
)
)
pause()
elif choice == "5":
payload = service.rebuild()
print(
colored(
f"Semantic index rebuilt with {int(payload.get('records', 0))} records.",
"green",
)
)
pause()
elif choice == "6":
query = input("Enter semantic search query: ").strip()
if not query:
print(colored("Search query required.", "yellow"))
pause()
continue
results = service.search(query, k=10, kind=None)
if not results:
print(colored("No semantic matches found.", "yellow"))
pause()
continue
print(color_text("\nSemantic Matches:", "menu"))
for row in results:
entry_id = int(row.get("entry_id", 0))
kind = str(row.get("kind", ""))
label = str(row.get("label", ""))
score = float(row.get("score", 0.0))
print(
colored(
f"- #{entry_id} [{kind}] {label} (score={score:.3f})",
"cyan",
)
)
pause()
elif not choice:
break
else:
print(colored("Invalid choice.", "red"))
except PasswordPromptError as exc:
logging.warning("Semantic submenu action cancelled: %s", exc)
print(colored(f"Action cancelled: {exc}", "yellow"))
pause()
except SeedPassError as exc:
logging.error("Semantic submenu action failed: %s", exc, exc_info=True)
print(colored(f"Action failed: {exc}", "red"))
pause()
except Exception as exc:
logging.error("Semantic submenu action failed: %s", exc, exc_info=True)
print(colored(f"Semantic action failed: {exc}", "red"))
pause()
def handle_settings(password_manager: PasswordManager) -> None:
"""Interactive settings menu with submenus for profiles and Nostr."""
while True:
@@ -1145,6 +1261,7 @@ def handle_settings(password_manager: PasswordManager) -> None:
print(color_text("15. Toggle Secret Mode", "menu"))
print(color_text("16. Toggle Offline Mode (default ON)", "menu"))
print(color_text("17. Toggle Quick Unlock", "menu"))
print(color_text("18. Semantic Index", "menu"))
choice = input("Select an option or press Enter to go back: ").strip()
try:
if choice == "1":
@@ -1248,6 +1365,8 @@ def handle_settings(password_manager: PasswordManager) -> None:
elif choice == "17":
handle_toggle_quick_unlock(password_manager)
pause()
elif choice == "18":
handle_semantic_index_menu(password_manager)
elif not choice:
break
else:
+4
View File
@@ -96,6 +96,7 @@ def _prime_tui2_service(
ProfileService,
ConfigService,
NostrService,
SemanticIndexService,
SyncService,
UtilityService,
VaultService,
@@ -111,6 +112,7 @@ def _prime_tui2_service(
"sync": SyncService(pm) if pm is not None else None,
"utility": UtilityService(pm) if pm is not None else None,
"vault": VaultService(pm) if pm is not None else None,
"semantic": SemanticIndexService(pm) if pm is not None else None,
}, None
except Exception as exc: # pragma: no cover - defensive runtime path
return None, exc
@@ -163,6 +165,7 @@ def main(
sync_service_factory=lambda: services["sync"],
utility_service_factory=lambda: services["utility"],
vault_service_factory=lambda: services["vault"],
semantic_service_factory=lambda: services["semantic"],
)
if launched:
return
@@ -225,6 +228,7 @@ def tui2(
sync_service_factory=lambda: services["sync"],
utility_service_factory=lambda: services["utility"],
vault_service_factory=lambda: services["vault"],
semantic_service_factory=lambda: services["semantic"],
)
if launched:
return
+188
View File
@@ -95,6 +95,7 @@ def launch_tui2(
sync_service_factory: Any | None = None,
utility_service_factory: Any | None = None,
vault_service_factory: Any | None = None,
semantic_service_factory: Any | None = None,
app_hook: Any | None = None,
) -> bool:
"""Launch TUI v2 when runtime dependencies are available.
@@ -343,6 +344,7 @@ def launch_tui2(
"setting-secret <on|off> [delay] | setting-offline <on|off> | "
"setting-quick-unlock <on|off> | setting-timeout <seconds> | "
"setting-kdf-iterations <n> | setting-kdf-mode <mode> | "
"semantic-status | semantic-enable | semantic-disable | semantic-build | semantic-rebuild | semantic-search <query> | "
"relay-list | relay-add <url> | relay-rm <index> | "
"relay-reset | npub | nostr-reset-sync-state | nostr-fresh-namespace | "
"sync-now | sync-bg | "
@@ -413,6 +415,8 @@ def launch_tui2(
self._sync_service = None
self._utility_service = None
self._vault_service = None
self._semantic_service = None
self._semantic_state = "n/a"
self._selected_entry_id: int | None = None
self._selected_entry: dict[str, Any] | None = None
self._last_query = ""
@@ -500,6 +504,16 @@ def launch_tui2(
except Exception as exc:
self._vault_service = None
self._log_activity(f"Vault service unavailable: {exc}")
try:
self._semantic_service = (
semantic_service_factory()
if callable(semantic_service_factory)
else None
)
except Exception as exc:
self._semantic_service = None
self._log_activity(f"Semantic service unavailable: {exc}")
self._refresh_semantic_state()
self._update_filters_panel()
self._update_help_overlay()
self._update_activity_panel()
@@ -563,10 +577,33 @@ def launch_tui2(
f" | Archive: {self.archive_scope}"
f" | Session: {lock_state}"
f" | Density: {self._density_mode}"
f" | SEM: {self._semantic_state}"
f" | Last Sync: {self._last_sync_text}"
)
self.query_one("#top-ribbon", Static).update(text)
def _refresh_semantic_state(self) -> None:
if self._semantic_service is None:
self._semantic_state = "n/a"
return
status = getattr(self._semantic_service, "status", None)
if not callable(status):
self._semantic_state = "n/a"
return
try:
payload = status() or {}
enabled = bool(payload.get("enabled", False))
built = bool(payload.get("built", False))
records = int(payload.get("records", 0))
if not enabled:
self._semantic_state = "off"
elif built:
self._semantic_state = f"ready({records})"
else:
self._semantic_state = "stale"
except Exception:
self._semantic_state = "err"
def _refresh_profile_tree(self) -> None:
profiles: list[str] = []
if self._profile_service is not None:
@@ -668,6 +705,7 @@ def launch_tui2(
"Palette commands: help, help-commands, open, search, filter, "
"onboarding, quickstart, stats, session-status, lock/unlock, "
"density, profile-tree-next/prev/open, "
"semantic-status/build/rebuild/search, "
"archive, restore, archive-filter, edit-doc/save-doc/cancel-edit, "
"link-add/link-rm/link-filter/link-next/link-prev/link-open, "
"add-*, notes/tags/fields, 2fa-*, profile-*, setting-*, relay-*, "
@@ -698,6 +736,7 @@ def launch_tui2(
"2FA: 2fa-board | 2fa-hide | 2fa-refresh | 2fa-copy <entry_id>",
"Profiles: profiles-list | profile-switch | profile-add | profile-remove | profile-rename | profile-tree-next | profile-tree-prev | profile-tree-open",
"Settings: setting-secret | setting-offline | setting-quick-unlock | setting-timeout | setting-kdf-iterations | setting-kdf-mode",
"Semantic: semantic-status | semantic-enable | semantic-disable | semantic-build | semantic-rebuild | semantic-search <query>",
"Nostr: relay-list | relay-add | relay-rm | relay-reset | npub | nostr-reset-sync-state | nostr-fresh-namespace",
"Sync/Utility: sync-now | sync-bg | checksum-verify | checksum-update | db-export | db-import | totp-export | parent-seed-backup",
"Sessions: managed-load [entry_id] | managed-exit | session-status | lock | unlock <password>",
@@ -3309,6 +3348,155 @@ def launch_tui2(
)
return
if cmd == "semantic-status":
if self._semantic_service is None:
self._set_status("Semantic service unavailable")
return
if args:
self._set_status("Usage: semantic-status")
return
status = getattr(self._semantic_service, "status", None)
if not callable(status):
self._set_status("Semantic service does not support status")
return
try:
payload = status() or {}
self._refresh_semantic_state()
self._update_top_ribbon()
self.query_one("#entry-detail", Static).update(
"Semantic Index Status\n---------------------\n\n"
+ json.dumps(payload, indent=2, sort_keys=True)
)
self._clear_failure()
self._set_status("Displayed semantic index status")
except Exception as exc:
self._record_failure(
"semantic-status failed",
exc,
retry=lambda: self._run_palette_command(raw),
hint="Press 'x' to retry.",
)
return
if cmd in {"semantic-enable", "semantic-disable"}:
if self._semantic_service is None:
self._set_status("Semantic service unavailable")
return
if args:
self._set_status(f"Usage: {cmd}")
return
setter = getattr(self._semantic_service, "set_enabled", None)
if not callable(setter):
self._set_status("Semantic service does not support enable/disable")
return
try:
enabled = cmd == "semantic-enable"
payload = setter(enabled) or {}
self._refresh_semantic_state()
self._update_top_ribbon()
self._clear_failure()
self._set_status(
"Semantic index "
+ ("enabled" if enabled else "disabled")
+ f" (records={int(payload.get('records', 0))})"
)
except Exception as exc:
self._record_failure(
f"{cmd} failed",
exc,
retry=lambda: self._run_palette_command(raw),
hint="Press 'x' to retry.",
)
return
if cmd in {"semantic-build", "semantic-rebuild"}:
if self._semantic_service is None:
self._set_status("Semantic service unavailable")
return
if args:
self._set_status(f"Usage: {cmd}")
return
runner = getattr(
self._semantic_service,
"rebuild" if cmd == "semantic-rebuild" else "build",
None,
)
if not callable(runner):
self._set_status("Semantic service does not support build/rebuild")
return
try:
payload = runner() or {}
self._refresh_semantic_state()
self._update_top_ribbon()
self._clear_failure()
verb = "rebuilt" if cmd == "semantic-rebuild" else "built"
self._set_status(
f"Semantic index {verb} (records={int(payload.get('records', 0))})"
)
except Exception as exc:
self._record_failure(
f"{cmd} failed",
exc,
retry=lambda: self._run_palette_command(raw),
hint="Press 'x' to retry.",
)
return
if cmd == "semantic-search":
if self._semantic_service is None:
self._set_status("Semantic service unavailable")
return
if not args:
self._set_status("Usage: semantic-search <query>")
return
searcher = getattr(self._semantic_service, "search", None)
if not callable(searcher):
self._set_status("Semantic service does not support search")
return
query = " ".join(args).strip()
if not query:
self._set_status("Usage: semantic-search <query>")
return
try:
results = list(searcher(query, k=10, kind=None) or [])
self._refresh_semantic_state()
self._update_top_ribbon()
lines = [
f"Semantic Search: {query}",
"---------------------------",
"",
]
if not results:
lines.append("No semantic matches.")
else:
for row in results[:10]:
entry_id = int(row.get("entry_id", 0))
kind = str(row.get("kind", ""))
label = str(row.get("label", ""))
score = float(row.get("score", 0.0))
excerpt = str(row.get("excerpt", ""))
lines.append(
f"- #{entry_id} [{kind}] {label} (score={score:.3f})"
)
if excerpt:
short = (
excerpt
if len(excerpt) <= 120
else excerpt[:117] + "..."
)
lines.append(f" {short}")
self.query_one("#entry-detail", Static).update("\n".join(lines))
self._clear_failure()
self._set_status(f"Semantic matches: {len(results)} for '{query}'")
except Exception as exc:
self._record_failure(
"semantic-search failed",
exc,
retry=lambda: self._run_palette_command(raw),
hint="Press 'x' to retry.",
)
return
if cmd == "relay-list":
if self._nostr_service is None:
self._set_status("Nostr service unavailable")
+48
View File
@@ -358,3 +358,51 @@ def test_nostr_menu_dispatches_fresh_namespace(monkeypatch):
with patch("builtins.input", side_effect=lambda *_: next(inputs)):
main.handle_nostr_menu(pm)
handler.assert_called_once_with(pm)
def test_settings_menu_dispatches_semantic_submenu(monkeypatch):
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
pm, _, _ = setup_pm(tmp_path, monkeypatch)
inputs = iter(["18", ""])
with patch("main.handle_semantic_index_menu") as handler:
with patch("builtins.input", side_effect=lambda *_: next(inputs)):
main.handle_settings(pm)
handler.assert_called_once_with(pm)
def test_semantic_submenu_search_flow(monkeypatch, capsys):
class DummySemanticService:
def __init__(self, _pm):
self.last_query = None
def status(self):
return {"enabled": False, "built": False, "records": 0}
def set_enabled(self, enabled: bool):
return {"enabled": bool(enabled), "records": 0}
def build(self):
return {"enabled": True, "built": True, "records": 2}
def rebuild(self):
return {"enabled": True, "built": True, "records": 2}
def search(self, query: str, *, k: int = 10, kind: str | None = None):
self.last_query = query
return [{"entry_id": 1, "kind": "document", "label": "Doc", "score": 0.5}]
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
pm, _, _ = setup_pm(tmp_path, monkeypatch)
monkeypatch.setattr(main, "SemanticIndexService", DummySemanticService)
monkeypatch.setattr(main, "pause", lambda: None)
inputs = iter(["1", "2", "4", "6", "relay docs", ""])
with patch("builtins.input", side_effect=lambda *_: next(inputs)):
main.handle_semantic_index_menu(pm)
out = capsys.readouterr().out
assert "Status: enabled=False, built=False, records=0" in out
assert "Semantic index enabled" in out
assert "Semantic index built with 2 records" in out
assert "Semantic Matches:" in out
assert "#1 [document] Doc" in out
+8
View File
@@ -560,6 +560,12 @@ async def test_tui2_matrix_actions_palette_events_and_guards() -> None:
app._run_palette_command("setting-timeout 300")
app._run_palette_command("setting-kdf-iterations 100000")
app._run_palette_command("setting-kdf-mode pbkdf2")
app._run_palette_command("semantic-status")
app._run_palette_command("semantic-enable now")
app._run_palette_command("semantic-disable now")
app._run_palette_command("semantic-build now")
app._run_palette_command("semantic-rebuild now")
app._run_palette_command("semantic-search")
app._run_palette_command("relay-list")
app._run_palette_command("npub")
app._run_palette_command("relay-add wss://relay.example")
@@ -601,6 +607,8 @@ async def test_tui2_matrix_actions_palette_events_and_guards() -> None:
app._run_palette_command("lock")
app._run_palette_command("unlock hunter2")
app._run_palette_command("search Doc")
app._run_palette_command("semantic-status")
app._run_palette_command("semantic-search notes")
app.action_focus_left()
app._run_palette_command("profile-tree-next")
app._run_palette_command("profile-tree-prev")
@@ -562,6 +562,49 @@ class FakeVaultService:
return _Resp()
class FakeSemanticService:
def __init__(self) -> None:
self.enabled = False
self.built = False
self.records = 0
self.last_query: str | None = None
def status(self):
return {
"enabled": bool(self.enabled),
"built": bool(self.built),
"records": int(self.records),
}
def set_enabled(self, enabled: bool):
self.enabled = bool(enabled)
return self.status()
def build(self):
self.built = True
self.records = 2
return self.status()
def rebuild(self):
self.built = True
self.records = 2
return self.status()
def search(self, query: str, *, k: int = 10, kind: str | None = None):
self.last_query = str(query)
if not query.strip():
return []
return [
{
"entry_id": 1,
"kind": kind or "document",
"label": "Doc",
"score": 0.7,
"excerpt": f"query={query} k={k}",
}
]
def _build_app(
service: FakeEntryService,
*,
@@ -571,6 +614,7 @@ def _build_app(
sync_service=None,
utility_service=None,
vault_service=None,
semantic_service=None,
):
holder: dict[str, object] = {}
@@ -597,6 +641,9 @@ def _build_app(
vault_service_factory=(
(lambda: vault_service) if vault_service is not None else None
),
semantic_service_factory=(
(lambda: semantic_service) if semantic_service is not None else None
),
app_hook=_hook,
)
assert launched is True
@@ -1547,6 +1594,7 @@ async def test_tui2_textual_profiles_and_settings_palette_commands() -> None:
sync = FakeSyncService()
utility = FakeUtilityService()
vault = FakeVaultService()
semantic = FakeSemanticService()
app = _build_app(
service,
profile_service=profiles,
@@ -1555,6 +1603,7 @@ async def test_tui2_textual_profiles_and_settings_palette_commands() -> None:
sync_service=sync,
utility_service=utility,
vault_service=vault,
semantic_service=semantic,
)
async with app.run_test() as pilot:
@@ -1611,6 +1660,34 @@ async def test_tui2_textual_profiles_and_settings_palette_commands() -> None:
assert config.kdf_mode == "argon2"
assert "KDF mode set to argon2" in _status_text(app)
app._run_palette_command("semantic-status")
await pilot.pause()
assert "Displayed semantic index status" in _status_text(app)
app._run_palette_command("semantic-enable")
await pilot.pause()
assert semantic.enabled is True
assert "Semantic index enabled" in _status_text(app)
app._run_palette_command("semantic-build")
await pilot.pause()
assert semantic.built is True
assert "Semantic index built" in _status_text(app)
app._run_palette_command("semantic-search relay recovery")
await pilot.pause()
assert semantic.last_query == "relay recovery"
assert "Semantic matches: 1 for 'relay recovery'" in _status_text(app)
app._run_palette_command("semantic-rebuild")
await pilot.pause()
assert "Semantic index rebuilt" in _status_text(app)
app._run_palette_command("semantic-disable")
await pilot.pause()
assert semantic.enabled is False
assert "Semantic index disabled" in _status_text(app)
app._run_palette_command("profile-add")
await pilot.pause()
assert "Created profile fp-new-1" in _status_text(app)
@@ -1763,6 +1840,10 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None:
await pilot.pause()
assert "Utility service unavailable" in _status_text(app)
app._run_palette_command("semantic-status")
await pilot.pause()
assert "Semantic service unavailable" in _status_text(app)
app._run_palette_command("db-export /tmp/x")
await pilot.pause()
assert "Vault service unavailable" in _status_text(app)
@@ -1831,6 +1912,14 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None:
await pilot.pause()
assert "Nostr service unavailable" in _status_text(app)
app._run_palette_command("semantic-enable now")
await pilot.pause()
assert "Semantic service unavailable" in _status_text(app)
app._run_palette_command("semantic-search")
await pilot.pause()
assert "Semantic service unavailable" in _status_text(app)
app._run_palette_command("npub")
await pilot.pause()
assert "Nostr service unavailable" in _status_text(app)
@@ -1855,6 +1944,7 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None:
vault = FakeVaultService()
nostr = FakeNostrService(["wss://r1"])
sync = FakeSyncService()
semantic = FakeSemanticService()
app = _build_app(
service,
profile_service=profiles,
@@ -1863,6 +1953,7 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None:
vault_service=vault,
nostr_service=nostr,
sync_service=sync,
semantic_service=semantic,
)
async with app.run_test() as pilot:
await pilot.pause()
@@ -1879,6 +1970,14 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None:
await pilot.pause()
assert "Usage: setting-kdf-mode <mode>" in _status_text(app)
app._run_palette_command("semantic-enable now")
await pilot.pause()
assert "Usage: semantic-enable" in _status_text(app)
app._run_palette_command("semantic-search")
await pilot.pause()
assert "Usage: semantic-search <query>" in _status_text(app)
app._run_palette_command("checksum-update nope")
await pilot.pause()
assert "Usage: checksum-update" in _status_text(app)