fix(tui): give retry in-app feedback via a shared retry core#50
Merged
Conversation
Pressing retry on a failed generation suspended the full-screen TUI to the raw terminal (print + Enter prompt), which reads as a crash. The retry decision logic was also duplicated between the CLI handler and the MCP tool. Extract one print-free core, services.retry.retry_generation_core, returning a sum type (AlreadyRunning / PendingRejectedBeforeCodegen / PendingNotFailed / Queued / BackendError) — every field required, no Optionals. It operates on an already-resolved id; resolution and the "no session" case stay at each surface where they genuinely differ. - TUI: _retry_flow calls the core directly and renders in-app (toasts for queued/already-running, MessageScreen for pending/backend errors); no more suspend-to-terminal. actions.do_retry removed. - CLI (cmd_retry_generation) and MCP (retry_generation) become thin callers of the core, keeping their own print/exit-code and chat_json rendering. CLI now also guards the pending state and returns a clean exit 1 on backend error instead of crashing. - refresh_status now re-arms the poll timer when a run leaves a terminal state (failed -> retry -> pending), fixing a latent staleness bug. Tests: new test_retry_core.py and test_server_retry.py; CLI/TUI tests updated. make unit-tests: 673 passed.
akozak-gd
previously approved these changes
Jul 14, 2026
… core Following the cancel flow (PR #48): retry validation belongs to the backend state machine (reset_for_retry accepts FAILED or stuck-PENDING), so the client-side status pre-check was a second validator — and it had drifted, refusing PENDING that the backend accepts. Drop the pre-check and the outcome sum type. services.retry.retry_generation_core is now a one-call helper: POST /retry, return the parsed dict, raise on failure. Each surface POSTs and renders success vs the backend's error message in its own style, mirroring _cancel_flow: - TUI _retry_flow: toast on success, MessageScreen with the backend reason on error. - CLI: print + exit 0/1. - MCP: chat_json; drops the client-derived `code`/pending-guidance fields in favour of the backend's message (single source of truth). No client status round-trip before the POST. Tests updated (core, CLI, MCP, TUI). make unit-tests: 671 passed.
akozak-gd
approved these changes
Jul 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Pressing retry on a failed generation in the TUI tore the full-screen UI down to the raw terminal (printed CLI output, then blocked on
Press Enter), which reads as a crash. Root cause:_retry_flow→_run_suspendedwrapping the print-based CLI handler.The retry decision logic was also duplicated across the CLI handler and the MCP tool — and, as review against the merged cancel feature (PR #48) showed, the client-side status pre-check had drifted from the backend: it refused
PENDINGsessions that the backend'sreset_for_retryactually accepts (the stuck-PENDING case).Change
Retry now defers eligibility to the backend state machine (the single validator), mirroring how PR #48's cancel flow defers to the backend:
services/retry.py— collapsed to one helper:retry_generation_core(gid) -> dictthat POSTs/retryand returns the parsed response, raising on failure. No client-side status pre-check, no outcome dataclasses._retry_flow— calls the helper directly and gives feedback in-app: a toast on success, aMessageScreencarrying the backend's reason on failure. Never suspends.actions.do_retryremoved.cmd_retry_generation— POST + print + exit 0/1.retry_generation— POST +chat_json. The client-derivedcode/pending-guidance fields are dropped in favour of the backend's message (single source of truth).Latent bug fixed —
refresh_statusre-arms the poll timer when a run leaves a terminal state (failed → retry → pending); previously the dashboard repainted once then went stale.Behavior changes to flag
PENDINGsession is retryable (it wasn't before — the client wrongly refused it). Wrong-state and unreachable-backend both surface the backend's message.code/pending fields; it returns the backend's message text.Tests
tests/test_retry_core.py(success returns dict / failure raises),tests/test_server_retry.py(MCP shapes), CLI + TUI retry tests updated, poll-timer self-heal test added.make unit-tests: 671 passed.Verify
Editable install — relaunch
specflow tui, open a FAILED session, pressr: dashboard stays full-screen, shows a "Retry queued" toast, and keeps live-updating through pending→running. With the backend stopped, retry shows aMessageScreenwith the reason and the session stays failed.Note (separate, not in this PR)
While mirroring the cancel flow I found a latent bug in the merged cancel feature:
_cancel_flow'sexcept httpx.HTTPStatusError … status_code == 400branch is dead code, becausecall_backendwrapsHTTPStatusErrorinto a genericException— so a 400 shows "Couldn't cancel: Backend returned HTTP 400: …" instead of the intended "already finished" message. Flagged for a separate fix.