Skip to content

feat(ios): chat never dead-ends — retry, stop, empty-turn handling#206

Merged
oratis merged 1 commit into
mainfrom
feat/ios-chat-ux
Jul 1, 2026
Merged

feat(ios): chat never dead-ends — retry, stop, empty-turn handling#206
oratis merged 1 commit into
mainfrom
feat/ios-chat-ux

Conversation

@oratis

@oratis oratis commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Fixes the "(no response)" dead end from the phone + a chat UX pass.

Diagnosis

Not a backend bug — I reproduced /chat streaming 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)

  • Per-turn status (streaming/ok/empty/error/cancelled); empty & cancelled are retryable, not failures.
  • Inline Retry on any non-ok terminal turn (replays last message, replaces the failed bubble).
  • Stop while streaming — cancels the turn; the torn-down SSE lets the Mac abort the agent.
  • Typing indicator only for a still-empty streaming turn; composer grows to 5 lines, sends on return.

Server (/chat)

  • Emit {type:"empty"} for a zero-output turn (client can tell empty from a real reply).
  • Per-turn AbortController wired to client disconnect → Stop truly aborts the agent (and unblocks the queued retry). Server-wide shutdown still honored via AbortSignal.any.
  • Guard SSE writes against a closed socket.

Backward-compatible: empty is additive; the new client also self-detects empty, so it works against any backend.

Verified: tsc --noEmit clean · iOS BUILD SUCCEEDED · 27/27 tests pass.

🤖 Generated with Claude Code

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>
@oratis oratis merged commit a8cb01c into main Jul 1, 2026
1 check passed
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.

1 participant