dispatch was async fn … -> () with reqwest calls and task spawns inlined, so it could not report intent — 5 of 8 arms were tested and the three with effects were unreachable. It now returns Vec<Effect> and App::run performs them.
Two live bugs the old shape produced, both fixed with regression tests:
The toast could not be dismissed.handle_overlay_input returned Pass for Toast(_) and no path ever cleared it, so spawn failed: … stayed on screen until another overlay replaced it. It now clears the toast and still returns Pass, so the keystroke does its normal job rather than being swallowed.
Closing a tab leaked its emulator. Only submit_kill called SessionEmulators::remove, so a closed tab's WebSocket task and vt100 parser lived on forever. close_tab now reports the session it dropped — and deliberately returns None when another tab still shows the same one.
AppState::overlay is private behind show_overlay / dismiss_overlay / toast. Four sites poked the field directly, which is precisely why the toast bug existed: it was the one piece of state with no method owning its transitions.
Newly testable, and now tested: AttachHighlighted yields Effect::Attach, kill-confirm y yields Effect::Kill and n/Esc yields nothing, spawn-modal submit carries the typed cwd and command. Those needed a live daemon before. The existing dispatch tests dropped #[tokio::test] for plain #[test] — no runtime is involved any more.
I kept state.rs as a module rather than folding it into app.rs. The review called that seam artificial, and it was — but the real complaint was that AppState owned no invariant App couldn't bypass. Making the transitions methods fixes that; moving the file would only have made app.rs 700 lines.
Verification
cargo fmt --all -- --check ok
cargo clippy --all-targets -- -D warnings 0
cargo test --all 301 passed, 0 failed
Third of nine from the architecture review, after #40 and #41.
`dispatch` was `async fn … -> ()` with `reqwest` calls and task spawns inlined, so it could not report intent — 5 of 8 arms were tested and the three with effects were unreachable. It now returns `Vec<Effect>` and `App::run` performs them.
**Two live bugs the old shape produced**, both fixed with regression tests:
- **The toast could not be dismissed.** `handle_overlay_input` returned `Pass` for `Toast(_)` and no path ever cleared it, so `spawn failed: …` stayed on screen until another overlay replaced it. It now clears the toast and still returns `Pass`, so the keystroke does its normal job rather than being swallowed.
- **Closing a tab leaked its emulator.** Only `submit_kill` called `SessionEmulators::remove`, so a closed tab's WebSocket task and vt100 parser lived on forever. `close_tab` now reports the session it dropped — and deliberately returns `None` when another tab still shows the same one.
`AppState::overlay` is private behind `show_overlay` / `dismiss_overlay` / `toast`. Four sites poked the field directly, which is precisely why the toast bug existed: it was the one piece of state with no method owning its transitions.
Newly testable, and now tested: `AttachHighlighted` yields `Effect::Attach`, kill-confirm `y` yields `Effect::Kill` and `n`/Esc yields nothing, spawn-modal submit carries the typed cwd and command. Those needed a live daemon before. The existing dispatch tests dropped `#[tokio::test]` for plain `#[test]` — no runtime is involved any more.
I kept `state.rs` as a module rather than folding it into `app.rs`. The review called that seam artificial, and it was — but the real complaint was that `AppState` owned no invariant `App` couldn't bypass. Making the transitions methods fixes that; moving the file would only have made `app.rs` 700 lines.
<details>
<summary>Verification</summary>
```
cargo fmt --all -- --check ok
cargo clippy --all-targets -- -D warnings 0
cargo test --all 301 passed, 0 failed
```
</details>
Third of nine from the architecture review, after #40 and #41.
dispatch was `async fn -> ()` with reqwest calls and task spawns inlined,
so it could not report intent and the spawn, kill and attach arms had no
test. It now returns Vec<Effect> and App::run performs them.
Two bugs the old shape produced, both fixed and covered:
- The toast overlay could not be dismissed. handle_overlay_input returned
Pass for Toast(_) and nothing cleared it, so "spawn failed: ..." stayed
until another overlay replaced it. It now clears and still returns Pass,
so the keystroke does its normal job instead of being eaten.
- Closing a tab leaked its emulator. Only submit_kill released one, so a
closed tab's WebSocket task and vt100 parser outlived it forever.
close_tab now reports the session it dropped — and holds onto it when
another tab still shows the same one.
AppState::overlay is private behind show_overlay/dismiss_overlay/toast.
Four sites poked the field directly; that is why the toast bug existed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
dispatchwasasync fn … -> ()withreqwestcalls and task spawns inlined, so it could not report intent — 5 of 8 arms were tested and the three with effects were unreachable. It now returnsVec<Effect>andApp::runperforms them.Two live bugs the old shape produced, both fixed with regression tests:
handle_overlay_inputreturnedPassforToast(_)and no path ever cleared it, sospawn failed: …stayed on screen until another overlay replaced it. It now clears the toast and still returnsPass, so the keystroke does its normal job rather than being swallowed.submit_killcalledSessionEmulators::remove, so a closed tab's WebSocket task and vt100 parser lived on forever.close_tabnow reports the session it dropped — and deliberately returnsNonewhen another tab still shows the same one.AppState::overlayis private behindshow_overlay/dismiss_overlay/toast. Four sites poked the field directly, which is precisely why the toast bug existed: it was the one piece of state with no method owning its transitions.Newly testable, and now tested:
AttachHighlightedyieldsEffect::Attach, kill-confirmyyieldsEffect::Killandn/Esc yields nothing, spawn-modal submit carries the typed cwd and command. Those needed a live daemon before. The existing dispatch tests dropped#[tokio::test]for plain#[test]— no runtime is involved any more.I kept
state.rsas a module rather than folding it intoapp.rs. The review called that seam artificial, and it was — but the real complaint was thatAppStateowned no invariantAppcouldn't bypass. Making the transitions methods fixes that; moving the file would only have madeapp.rs700 lines.Verification
Third of nine from the architecture review, after #40 and #41.
No findings.