Fix beta TUI startup preflight and installer fallback hints

This commit is contained in:
thePR0M3TH3AN committed 2026-03-01 17:47:17 -05:00
1 parent ba93a1cd5a
commit f02b360062
4 files changed
+73 -2

No files matched your search

+9
View File
@@ -240,3 +240,12 @@
- Harness validation results:
- `scripts/ai_tui2_agent_test.py --scenario extended`: passed
- `scripts/ai_tui_agent_test.py --scenario core`: passed
## 2026-03-01 Installer + TUI v2 preflight fix (gray-screen mitigation)
- Root cause found for "blank/gray screen" reports: TUI v2 was initializing `PasswordManager` inside Textual mount via `entry_service_factory`, so password/setup prompts could occur behind the Textual screen and appear as a frozen UI.
- Updated `src/seedpass/cli/__init__.py` to pre-initialize entry service before launching Textual (`_prime_tui2_service`), then pass the ready service into `launch_tui2`.
- Added explicit fallback/error behavior:
- default `seedpass`: preflight failures now fall back to legacy TUI with details.
- `seedpass tui2 --no-fallback-legacy`: preflight failures now return exit code 1 with a clear error.
- Updated installer `scripts/install.sh` to run `seedpass tui2 --check` after install and warn when Textual runtime is unavailable, plus print a legacy fallback hint for users seeing blank/gray startup.
- Added regression coverage in `src/tests/test_typer_cli.py` for preflight behavior and adapted existing TUI launch tests to mock preflight service creation.
+9
View File
@@ -218,6 +218,14 @@ main() {
if ! "$VENV_DIR/bin/python" -c "import seedpass.cli; print('ok')"; then
print_error "SeedPass CLI import check failed."
fi
if "$VENV_DIR/bin/seedpass" tui2 --check >/tmp/seedpass-tui2-check.json 2>/dev/null; then
if ! grep -q '"textual_available":[[:space:]]*true' /tmp/seedpass-tui2-check.json; then
print_warning "TUI v2 runtime check reports Textual as unavailable."
print_warning "You can run legacy mode with: seedpass --legacy-tui"
fi
else
print_warning "Unable to run 'seedpass tui2 --check' during install validation."
fi
deactivate
@@ -257,6 +265,7 @@ EOF2
# 8. Final instructions
print_success "Installation/update complete!"
print_info "You can now launch the interactive TUI by typing: seedpass"
print_info "If your terminal only shows a blank/gray screen, run: seedpass --legacy-tui"
print_info "'seedpass' resolves to: $(command -v seedpass)"
if [[ ":$PATH:" != *":$LAUNCHER_DIR:"* ]]; then
print_warning "Directory '$LAUNCHER_DIR' is not in your PATH."
+40 -2
View File
@@ -82,6 +82,20 @@ def _gui_backend_available() -> bool:
return False
def _prime_tui2_service(
ctx: typer.Context,
) -> tuple[Optional[object], Optional[Exception]]:
"""Initialize entry service before Textual mounts.
PasswordManager initialization may prompt for credentials. Doing that before
entering Textual prevents hidden prompt stalls that appear as a blank screen.
"""
try:
return _get_entry_service(ctx), None
except Exception as exc: # pragma: no cover - defensive runtime path
return None, exc
@app.callback(invoke_without_command=True)
def main(
ctx: typer.Context,
@@ -107,9 +121,18 @@ def main(
tui = importlib.import_module("main")
raise typer.Exit(tui.main(fingerprint=fingerprint))
service, preflight_error = _prime_tui2_service(ctx)
if preflight_error is not None:
typer.echo(
"TUI v2 preflight failed; falling back to legacy TUI. "
f"Details: {preflight_error}"
)
tui = importlib.import_module("main")
raise typer.Exit(tui.main(fingerprint=fingerprint))
launched = launch_tui2(
fingerprint=fingerprint,
entry_service_factory=lambda: _get_entry_service(ctx),
entry_service_factory=lambda: service,
)
if launched:
return
@@ -150,9 +173,24 @@ def tui2(
return
fingerprint = (ctx.obj or {}).get("fingerprint")
service, preflight_error = _prime_tui2_service(ctx)
if preflight_error is not None:
if fallback_legacy:
typer.echo(
"TUI v2 preflight failed; falling back to legacy TUI. "
f"Details: {preflight_error}"
)
tui = importlib.import_module("main")
raise typer.Exit(tui.main(fingerprint=fingerprint))
typer.echo(
f"TUI v2 preflight failed: {preflight_error}",
err=True,
)
raise typer.Exit(1)
launched = launch_tui2(
fingerprint=fingerprint,
entry_service_factory=lambda: _get_entry_service(ctx),
entry_service_factory=lambda: service,
)
if launched:
return
+15
View File
@@ -981,6 +981,7 @@ def test_root_tui2_forward_fingerprint(monkeypatch):
called["factory"] = kwargs.get("entry_service_factory")
return True
monkeypatch.setattr(cli, "_get_entry_service", lambda _ctx: object())
monkeypatch.setattr(cli, "launch_tui2", fake_launch)
result = runner.invoke(app, ["--fingerprint", "abc"])
assert result.exit_code == 0
@@ -1019,6 +1020,7 @@ def test_tui2_check(monkeypatch):
def test_tui2_unavailable_without_fallback(monkeypatch):
monkeypatch.setattr(cli, "_get_entry_service", lambda _ctx: object())
monkeypatch.setattr(cli, "launch_tui2", lambda **_: False)
result = runner.invoke(app, ["tui2", "--no-fallback-legacy"])
assert result.exit_code == 1
@@ -1027,6 +1029,7 @@ def test_tui2_unavailable_without_fallback(monkeypatch):
def test_tui2_fallback_legacy(monkeypatch):
called = {}
monkeypatch.setattr(cli, "_get_entry_service", lambda _ctx: object())
monkeypatch.setattr(cli, "launch_tui2", lambda **_: False)
def fake_main(*, fingerprint=None):
@@ -1046,6 +1049,7 @@ def test_tui2_fallback_legacy(monkeypatch):
def test_root_fallback_legacy_when_tui2_unavailable(monkeypatch):
called = {}
monkeypatch.setattr(cli, "_get_entry_service", lambda _ctx: object())
monkeypatch.setattr(cli, "launch_tui2", lambda **_: False)
def fake_main(*, fingerprint=None):
@@ -1063,6 +1067,17 @@ def test_root_fallback_legacy_when_tui2_unavailable(monkeypatch):
assert "falling back to legacy TUI" in result.stdout
def test_tui2_preflight_failure_without_fallback(monkeypatch):
monkeypatch.setattr(
cli,
"_get_entry_service",
lambda _ctx: (_ for _ in ()).throw(RuntimeError("unlock failed")),
)
result = runner.invoke(app, ["tui2", "--no-fallback-legacy"])
assert result.exit_code == 1
assert "preflight failed" in result.stderr.lower()
def test_gui_command(monkeypatch):
called = {}