Skip to content

fix(tui): give retry in-app feedback via a shared retry core#50

Merged
mkonopelski-gd merged 3 commits into
mainfrom
fix/tui-retry-in-app-feedback
Jul 14, 2026
Merged

fix(tui): give retry in-app feedback via a shared retry core#50
mkonopelski-gd merged 3 commits into
mainfrom
fix/tui-retry-in-app-feedback

Conversation

@mkonopelski-gd

@mkonopelski-gd mkonopelski-gd commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

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_suspended wrapping 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 PENDING sessions that the backend's reset_for_retry actually 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) -> dict that POSTs /retry and returns the parsed response, raising on failure. No client-side status pre-check, no outcome dataclasses.
  • TUI _retry_flow — calls the helper directly and gives feedback in-app: a toast on success, a MessageScreen carrying the backend's reason on failure. Never suspends. actions.do_retry removed.
  • CLI cmd_retry_generation — POST + print + exit 0/1.
  • MCP retry_generation — POST + chat_json. The client-derived code/pending-guidance fields are dropped in favour of the backend's message (single source of truth).

Latent bug fixedrefresh_status re-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

  • Retry now POSTs and lets the backend decide, so a stuck-PENDING session is retryable (it wasn't before — the client wrongly refused it). Wrong-state and unreachable-backend both surface the backend's message.
  • MCP retry no longer returns the structured 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, press r: dashboard stays full-screen, shows a "Retry queued" toast, and keeps live-updating through pending→running. With the backend stopped, retry shows a MessageScreen with 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's except httpx.HTTPStatusError … status_code == 400 branch is dead code, because call_backend wraps HTTPStatusError into a generic Exception — so a 400 shows "Couldn't cancel: Backend returned HTTP 400: …" instead of the intended "already finished" message. Flagged for a separate fix.

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
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.
@mkonopelski-gd mkonopelski-gd merged commit 139a18f into main Jul 14, 2026
3 checks passed
@mkonopelski-gd mkonopelski-gd deleted the fix/tui-retry-in-app-feedback branch July 14, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants