feat(ios): chat never dead-ends — retry, stop, empty-turn handling#206
Merged
Conversation
The phone showed a bare "(no response)" bubble with no way forward. Root cause
wasn't the backend (verified: /chat streams cleanly over loopback AND the phone's
LAN+token path) — it was an empty agent turn (e.g. a provider/proxy hiccup returns
an empty completion) landing in a UI dead end.
iOS (ChatView):
- Model each Lisa turn with a status (streaming/ok/empty/error/cancelled) instead
of a lone isError flag. Empty and cancelled are retryable, not failures.
- Every non-ok terminal turn gets an inline **Retry** that replays the last
message (drops the failed bubble so it doesn't stack).
- **Stop** button while streaming (replaces send) — cancels the turn; the torn-down
SSE connection lets the Mac abort the agent too.
- Typing indicator only stands in for a still-empty streaming turn; composer grows
to 5 lines and sends on return.
Server (/chat):
- Emit an explicit `{type:"empty"}` when a turn produces zero text, zero tools, and
no error, so the client can distinguish "nothing came back" from a real reply.
- Per-turn AbortController wired to client disconnect (`req` close) → Stop actually
aborts the agent instead of leaving it running (and blocking the queued retry).
Combined with the server-wide shutdown signal via AbortSignal.any.
- Guard SSE writes against a closed socket (no "write after end" from the loop).
Backward-compatible: `empty` is additive (old clients ignore it); the new client
also self-detects an empty turn, so the fix works against any backend.
Verified: tsc --noEmit clean; iOS BUILD SUCCEEDED; 27/27 LisaPocketTests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 1, 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.
Fixes the "(no response)" dead end from the phone + a chat UX pass.
Diagnosis
Not a backend bug — I reproduced
/chatstreaming cleanly over both loopback and the phone's exact LAN+token path (mood→text→done). The phone's(no response)only happens when a turn genuinely returns zero text/tools/error (a provider/proxy hiccup → empty completion) and the old UI turned that into a dead end.iOS (ChatView)
Server (/chat)
{type:"empty"}for a zero-output turn (client can tell empty from a real reply).AbortSignal.any.Backward-compatible:
emptyis additive; the new client also self-detects empty, so it works against any backend.Verified:
tsc --noEmitclean · iOS BUILD SUCCEEDED · 27/27 tests pass.🤖 Generated with Claude Code