mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-10-30 17:32:04 +00:00
Add TUI v2 retryable error recovery flows
This commit is contained in:
1 parent
ec0d77d3e6
commit
0d0432ec45
4 files changed
+127
-12
No files matched your search
@@ -30,7 +30,9 @@ This checklist tracks readiness to switch default interactive mode from legacy T
|
||||
- [x] First UX/perf pass for large vaults.
|
||||
- paginated entry list rendering (`p`/`n`, `page-*` palette commands)
|
||||
- document content preview truncation in detail panel
|
||||
- [ ] Error messaging and recovery flow review.
|
||||
- [x] Error messaging and recovery flow review.
|
||||
- unified failure status messaging
|
||||
- retry shortcut (`x`) and palette `retry` command
|
||||
- [ ] Cutover decision memo (`seedpass` default switch timing).
|
||||
|
||||
## Risks Remaining
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ Goal: Introduce a modern, modular TUI (`seedpass tui2`) without rewriting core l
|
||||
### Phase 4: Cutover readiness
|
||||
- [x] Feature parity checklist against legacy TUI.
|
||||
- [x] UX/perf pass for large vaults.
|
||||
- [ ] Error messaging and recovery flows.
|
||||
- [x] Error messaging and recovery flows.
|
||||
- [ ] Decide default switch timing (`seedpass` -> `tui2`).
|
||||
|
||||
## Test Strategy
|
||||
|
||||
@@ -186,3 +186,5 @@
|
||||
- Link panel now tracks selection state and displays active relation filter + selected link position to support KB-style navigation in large graphs.
|
||||
- 2026-03-01: Phase 4 kickoff added `docs/tui_v2_parity_checklist.md` and linked it in docs indexes (`docs/README.md`, `landing/docs.html`) to track legacy-vs-v2 cutover readiness.
|
||||
- TUI v2 large-vault UX/perf pass: entry list pagination (200/page, `p`/`n`, palette `page-next/page-prev/page <n>`) and document detail preview truncation (`content_truncated`) to avoid rendering extremely large payloads in the detail pane.
|
||||
- 2026-03-01: TUI v2 error/recovery pass added unified failure recording (`_record_failure`) and retry workflow (`x` key + palette `retry`) across init/search/select/link/save/archive paths.
|
||||
- Recovery hints are now included in status messages (for example, "Press 'x' to retry") and Phase 4 docs mark error/recovery flow work complete.
|
||||
+121
-10
@@ -92,6 +92,7 @@ def launch_tui2(
|
||||
BINDINGS = [
|
||||
("q", "quit", "Quit"),
|
||||
("r", "refresh", "Refresh"),
|
||||
("x", "retry_last_error", "Retry"),
|
||||
("slash", "focus_search", "Search"),
|
||||
("f", "cycle_filter", "Filter"),
|
||||
("p", "prev_page", "Prev Page"),
|
||||
@@ -121,7 +122,7 @@ def launch_tui2(
|
||||
"link-add <target> [relation] [note] | "
|
||||
"link-rm <target> [relation] | "
|
||||
"link-filter <relation|all> | link-next | link-prev | link-open | "
|
||||
"page-next | page-prev | page <n>"
|
||||
"page-next | page-prev | page <n> | retry"
|
||||
),
|
||||
id="command-palette",
|
||||
classes="hidden",
|
||||
@@ -171,13 +172,20 @@ def launch_tui2(
|
||||
self._result_page = 0
|
||||
self._current_links: list[dict[str, Any]] = []
|
||||
self._current_link_cursor = 0
|
||||
self._last_error: str | None = None
|
||||
self._retry_action: Any | None = None
|
||||
try:
|
||||
self._service = (
|
||||
entry_service_factory() if callable(entry_service_factory) else None
|
||||
)
|
||||
except Exception as exc:
|
||||
self._service = None
|
||||
self._set_status(f"Unable to initialize entry service: {exc}")
|
||||
self._record_failure(
|
||||
"Unable to initialize entry service",
|
||||
exc,
|
||||
retry=self._retry_initialize_service,
|
||||
hint="Press 'x' to retry initialization.",
|
||||
)
|
||||
self.query_one("#entry-detail", Static).update(
|
||||
f"Unable to initialize entry service: {exc}"
|
||||
)
|
||||
@@ -187,6 +195,40 @@ def launch_tui2(
|
||||
def _set_status(self, message: str) -> None:
|
||||
self.query_one("#status", Static).update(message)
|
||||
|
||||
def _record_failure(
|
||||
self,
|
||||
context: str,
|
||||
exc: Exception,
|
||||
*,
|
||||
retry: Any | None = None,
|
||||
hint: str = "",
|
||||
) -> None:
|
||||
self._last_error = f"{context}: {exc}"
|
||||
self._retry_action = retry
|
||||
suffix = f" {hint}" if hint else ""
|
||||
self._set_status(f"{self._last_error}.{suffix}")
|
||||
|
||||
def _clear_failure(self) -> None:
|
||||
self._last_error = None
|
||||
self._retry_action = None
|
||||
|
||||
def _retry_initialize_service(self) -> None:
|
||||
try:
|
||||
self._service = (
|
||||
entry_service_factory() if callable(entry_service_factory) else None
|
||||
)
|
||||
except Exception as exc:
|
||||
self._record_failure(
|
||||
"Unable to initialize entry service",
|
||||
exc,
|
||||
retry=self._retry_initialize_service,
|
||||
hint="Press 'x' to retry initialization.",
|
||||
)
|
||||
return
|
||||
self._clear_failure()
|
||||
self._set_status("Entry service initialized")
|
||||
self._load_entries(self._last_query, reset_page=False)
|
||||
|
||||
def _current_filter_kinds(self) -> list[str] | None:
|
||||
if self.filter_kind == "all":
|
||||
return None
|
||||
@@ -214,6 +256,7 @@ def launch_tui2(
|
||||
"- f cycle kind filter",
|
||||
"- p/n prev/next page",
|
||||
"- r refresh",
|
||||
"- x retry last error",
|
||||
"",
|
||||
"Actions:",
|
||||
"- a archive/restore",
|
||||
@@ -327,10 +370,18 @@ def launch_tui2(
|
||||
self._current_links = []
|
||||
self._current_link_cursor = 0
|
||||
self._update_filters_panel()
|
||||
self._set_status("Failed to load entries")
|
||||
self._record_failure(
|
||||
"Failed to load entries",
|
||||
exc,
|
||||
retry=lambda: self._load_entries(
|
||||
self._last_query, reset_page=False
|
||||
),
|
||||
hint="Press 'x' to retry.",
|
||||
)
|
||||
return
|
||||
|
||||
self._all_results = list(results)
|
||||
self._clear_failure()
|
||||
if reset_page:
|
||||
self._result_page = 0
|
||||
self._render_current_page(preserve_selected=not reset_page)
|
||||
@@ -362,7 +413,12 @@ def launch_tui2(
|
||||
self.query_one("#link-detail", Static).update("Links unavailable.")
|
||||
self._current_links = []
|
||||
self._current_link_cursor = 0
|
||||
self._set_status(f"Failed to load entry {entry_index}")
|
||||
self._record_failure(
|
||||
f"Failed to load entry {entry_index}",
|
||||
exc,
|
||||
retry=lambda: self._show_entry(entry_index),
|
||||
hint="Press 'x' to retry.",
|
||||
)
|
||||
|
||||
def _update_links_panel(self) -> None:
|
||||
if self._service is None or self._selected_entry_id is None:
|
||||
@@ -380,6 +436,12 @@ def launch_tui2(
|
||||
)
|
||||
self._current_links = []
|
||||
self._current_link_cursor = 0
|
||||
self._record_failure(
|
||||
"Failed to load entry links",
|
||||
exc,
|
||||
retry=self._update_links_panel,
|
||||
hint="Press 'x' to retry.",
|
||||
)
|
||||
return
|
||||
|
||||
if self.link_relation_filter == "all":
|
||||
@@ -433,6 +495,7 @@ def launch_tui2(
|
||||
else:
|
||||
lines.append(f"{prefix} {relation} -> {target}")
|
||||
self.query_one("#link-detail", Static).update("\n".join(lines))
|
||||
self._clear_failure()
|
||||
|
||||
def _is_selected_document(self) -> bool:
|
||||
if not isinstance(self._selected_entry, dict):
|
||||
@@ -501,9 +564,12 @@ def launch_tui2(
|
||||
"Palette commands: help, open, search, filter, archive, "
|
||||
"restore, edit-doc, save-doc, cancel-edit, link-add, link-rm, "
|
||||
"link-filter, link-next, link-prev, link-open, page-next, "
|
||||
"page-prev, page <n>"
|
||||
"page-prev, page <n>, retry"
|
||||
)
|
||||
return
|
||||
if cmd == "retry":
|
||||
self.action_retry_last_error()
|
||||
return
|
||||
|
||||
if cmd == "open":
|
||||
if len(args) != 1:
|
||||
@@ -549,9 +615,15 @@ def launch_tui2(
|
||||
self._load_entries(self._last_query, reset_page=False)
|
||||
if current_id in self._entry_ids_in_view:
|
||||
self._show_entry(current_id)
|
||||
self._clear_failure()
|
||||
self._set_status(f"Entry {current_id} {action}")
|
||||
except Exception as exc:
|
||||
self._set_status(f"{cmd} failed: {exc}")
|
||||
self._record_failure(
|
||||
f"{cmd} failed",
|
||||
exc,
|
||||
retry=lambda: self._run_palette_command(cmd),
|
||||
hint="Press 'x' to retry.",
|
||||
)
|
||||
return
|
||||
|
||||
if cmd == "edit-doc":
|
||||
@@ -590,11 +662,17 @@ def launch_tui2(
|
||||
note=note,
|
||||
)
|
||||
self._update_links_panel()
|
||||
self._clear_failure()
|
||||
self._set_status(
|
||||
f"Link added: {self._selected_entry_id} {relation} {target}"
|
||||
)
|
||||
except Exception as exc:
|
||||
self._set_status(f"link-add failed: {exc}")
|
||||
self._record_failure(
|
||||
"link-add failed",
|
||||
exc,
|
||||
retry=lambda: self._run_palette_command(raw),
|
||||
hint="Press 'x' to retry.",
|
||||
)
|
||||
return
|
||||
|
||||
if cmd == "link-rm":
|
||||
@@ -617,11 +695,17 @@ def launch_tui2(
|
||||
relation=relation,
|
||||
)
|
||||
self._update_links_panel()
|
||||
self._clear_failure()
|
||||
self._set_status(
|
||||
f"Link removed: {self._selected_entry_id} -> {target}"
|
||||
)
|
||||
except Exception as exc:
|
||||
self._set_status(f"link-rm failed: {exc}")
|
||||
self._record_failure(
|
||||
"link-rm failed",
|
||||
exc,
|
||||
retry=lambda: self._run_palette_command(raw),
|
||||
hint="Press 'x' to retry.",
|
||||
)
|
||||
return
|
||||
|
||||
if cmd == "refresh":
|
||||
@@ -678,6 +762,21 @@ def launch_tui2(
|
||||
self._update_filters_panel()
|
||||
self._load_entries(query=search, reset_page=False)
|
||||
|
||||
def action_retry_last_error(self) -> None:
|
||||
retry = self._retry_action
|
||||
if retry is None:
|
||||
self._set_status("No retry action available")
|
||||
return
|
||||
try:
|
||||
retry()
|
||||
except Exception as exc:
|
||||
self._record_failure(
|
||||
"Retry failed",
|
||||
exc,
|
||||
retry=retry,
|
||||
hint="Press 'x' to retry again.",
|
||||
)
|
||||
|
||||
def action_focus_search(self) -> None:
|
||||
if self.editing_document:
|
||||
self._set_status("Finish document edit before searching")
|
||||
@@ -821,9 +920,15 @@ def launch_tui2(
|
||||
self._load_entries(self._last_query)
|
||||
if current_id in self._entry_ids_in_view:
|
||||
self._show_entry(current_id)
|
||||
self._clear_failure()
|
||||
self._set_status(f"Entry {current_id} {action}")
|
||||
except Exception as exc:
|
||||
self._set_status(f"Archive/restore failed: {exc}")
|
||||
self._record_failure(
|
||||
"Archive/restore failed",
|
||||
exc,
|
||||
retry=self.action_toggle_archive,
|
||||
hint="Press 'x' to retry.",
|
||||
)
|
||||
|
||||
def action_edit_document(self) -> None:
|
||||
if self._service is None:
|
||||
@@ -884,9 +989,15 @@ def launch_tui2(
|
||||
self._load_entries(self._last_query, reset_page=False)
|
||||
if current_id in self._entry_ids_in_view:
|
||||
self._show_entry(current_id)
|
||||
self._clear_failure()
|
||||
self._set_status(f"Saved document {current_id}")
|
||||
except Exception as exc:
|
||||
self._set_status(f"Failed to save document: {exc}")
|
||||
self._record_failure(
|
||||
"Failed to save document",
|
||||
exc,
|
||||
retry=self.action_save_document,
|
||||
hint="Press 'x' to retry.",
|
||||
)
|
||||
|
||||
def action_cancel_document_edit(self) -> None:
|
||||
if self.palette_open:
|
||||
|
||||
Reference in new issue
Block a user