Skip to content

Improve reconnection reliability and surface disconnect events in chat#89

Closed
zmanian wants to merge 185 commits intomcintyre94:mainfrom
zmanian:reconnect-reliability-and-notices
Closed

Improve reconnection reliability and surface disconnect events in chat#89
zmanian wants to merge 185 commits intomcintyre94:mainfrom
zmanian:reconnect-reliability-and-notices

Conversation

@zmanian
Copy link
Copy Markdown
Contributor

@zmanian zmanian commented Mar 15, 2026

Summary

  • Server-side keepalive: Pass max_run_after_disconnect=300 on service creation so Claude keeps running for 5 minutes even if the iOS client disconnects (phone lock, background, network blip)
  • Fewer false timeouts: Increase idle timeout from 30s to 45s — requires 2+ missed heartbeats (every 20s) instead of just 1
  • More post-stop retries: Increase from 1 to 3 retries with exponential backoff (1s, 2s, 4s) when service stops without a result event
  • Exponential backoff on reconnect polling: Starts at 2s, doubles up to 30s max, resets when new events arrive
  • WebSocket ping: ExecSession sends ping every 15s to detect dead connections early instead of waiting for TCP timeout
  • Disconnect visibility in chat: New .systemNotice content type renders inline capsule banners showing "Connection lost — reconnecting...", "Reconnected — response complete", or "Reconnection gave up — response may be incomplete"
  • Claude disconnect awareness: When resuming after a failed reconnect, the next prompt is prefixed with context telling Claude its previous output may be incomplete

Test plan

  • Build succeeds
  • All 276 tests pass
  • Updated existing reconnect retry test to expect 4 attempts (was 2)
  • Updated content count assertions in replay/reconnect tests to account for new system notices
  • Manual: background app during long Claude response, verify "Connection lost" notice appears and reconnect recovers
  • Manual: verify system notices persist across app restart (reload chat from SwiftData)

🤖 Generated with Claude Code

zmanian and others added 30 commits February 23, 2026 11:38
Inspired by the Chowder iOS app's approach to showing AI activity:
instead of a generic bouncing-dots indicator, show what Claude is
actually doing ("Running npm test...", "Reading config.ts...") with
a shimmer animation, and render completed tools as compact step rows
with elapsed time rather than bordered expandable cards.

- ThinkingShimmerView: pulsing purple dot + shimmer gradient text
- ToolStepRow: compact icon + label + elapsed time, tap for details
- ToolDetailSheet: half-sheet with input/output for completed tools
- Haptic feedback on tool completion (light) and first text (medium)
- ToolUseCard.result links tool uses to their results for elapsed time
- 10 new tests for activityLabel, elapsedString, and result linking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ActivityKit Live Activity shows Claude's progress in the Dynamic Island
and Lock Screen while the app is backgrounded. Compact view shows a
status dot and current intent; expanded view adds sprite name, step
count, and previous completed intent. Lock Screen banner shows intent
card stack with depth effect and a live timer.

New files: WispActivityAttributes (shared model), LiveActivityManager
(debounced singleton), WispActivityWidget extension (WispLiveActivity +
bundle). ChatViewModel hooks start/update/end the activity at tool use,
tool result, first text response, and result events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Truncate userTask (200 chars) and intent strings (100 chars) to stay
under ActivityKit's 4KB payload limit. Surface startActivity failures
via return value and lastError property. Improve logging with activity
IDs. Bump build to 4 for TestFlight.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lower URLRequest timeoutInterval from 3600s to 120s on both service
streaming methods so silently dropped connections trigger reconnect
within 2 minutes instead of hanging for up to an hour. Replace silent
try? decode with do/catch + logging to surface malformed service events
that could cause the stream loop to wait indefinitely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document thinking shimmer, inline tool steps, Dynamic Island Live
Activity, and stream reliability improvements over upstream.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace regex-based code highlighter with tree-sitter AST parsing
for correct syntax highlighting across 11 languages (Swift, Python,
JS, TS, Rust, Go, Bash, JSON, YAML, HTML, CSS). Add unified diff
view for Edit tool results, custom markdown theme, screenshot
previews, and various dashboard/auth UI improvements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace default iOS blue with sprites green for AccentColor asset,
user chat bubbles, send button, and thinking shimmer dot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve conflicts keeping Live Activity features from branch,
upstream's streaming/typing sync fix, auto-checkpoint setting,
swipe-to-delete restoration, queriesURL syntax highlighting fix,
and init-time diff computation optimization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…esponse

- Queue prompt without adding to messages; PendingUserBubbleView renders it
- Add user message to messages only when execution actually starts
- PendingUserBubbleView: faded blue bubble with a "Queued" clock label

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve conflicts in ChatViewModel (duplicate cancelQueuedPrompt),
ChatInputBar (send button tint), ChatView (pending bubble edit/cancel,
duplicate onChange), and PendingUserBubbleView (take upstream edit+cancel
buttons over local swipe-to-dismiss).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace confirmationDialog with inline Menu on attachment button
- Add AttachedFile model and attachment chips view instead of raw paths
- Show removable capsule chips above input bar for pending attachments
- Add upload success feedback with auto-clearing filename indicator
- Convert SpriteFileBrowserView from binding to callback pattern
- Change file browser loading label to "Connecting to sprite..."
- Fix duplicate PendingUserBubbleView entry in project file

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Render TodoWrite tool uses as a dedicated plan card instead of a generic
tool step row. Shows todo items with status-colored icons (completed,
in_progress, pending), a progress counter, and collapsible item list
that auto-collapses when the tool result arrives.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	Wisp/Assets.xcassets/AccentColor.colorset/Contents.json
#	Wisp/Assets.xcassets/AppIcon.appiconset/Contents.json
#	Wisp/Views/SpriteDetail/Chat/ChatInputBar.swift
Also strip alpha channel from app icon to fix App Store Connect
upload validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
zmanian and others added 28 commits March 14, 2026 14:32
SpritesAPIClient stream methods now use the existing instance
decoder. GitHubAPIClient shares a single static decoder across
all decode sites.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a shellEscape() helper for safe single-quote wrapping and
apply it to all shell command construction sites:
- ChatViewModel: workingDirectory, tokens, sessionId, MCP config
- SpriteOverviewViewModel: GitHub and Sprites CLI token auth
- ClaudeQuestionTool: validate sessionId chars in file paths/JSON

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	Wisp.xcodeproj/project.pbxproj
# Conflicts:
#	Wisp/Services/KeychainService.swift
# Conflicts:
#	Wisp/Views/SpriteDetail/Chat/ChatInputBar.swift
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously processServiceStream only upgraded status from .connecting to
.streaming, so a reconnect that was actively receiving data stayed stuck
on .reconnecting the whole time. Extend the same transition to also fire
from .reconnecting so the UI reflects that streaming is live.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
reconnectIfNeeded now captures the service status it fetches rather than
discarding it. If the service is already stopped, we pass serviceAlreadyStopped
to reconnectToServiceLogs so it exits after a single log fetch instead of
making a redundant status API call and looping.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously ChatViewModel was recreated on every chat switch, cancelling
the active stream and requiring a reconnect on return. ChatSessionManager
is an @observable cache keyed by chat UUID, injected app-wide, so VMs
(and their streams) survive switching chats or navigating between sprites.

switchToChat now looks up the cached VM instead of creating a new one.
The scenePhase handler resumes all cached VMs on foreground, not just the
active one. clearAllChats detaches all VMs before wiping the chat list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts:
#	Wisp.xcodeproj/project.pbxproj
#	Wisp/Views/SpriteDetail/Chat/ChatView.swift
Remove stale SideChat references in ChatView ThinkingShimmerView
and remove invalid isStreaming parameter from ScreenshotPreviews.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…t-6e54d330' into zaki-main

# Conflicts:
#	Wisp/App/WispApp.swift
Remove duplicate addAttachedFile tests and update ServiceInfo
mock constructor to match new upstream API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Index user and assistant text content in SpriteChat.fullTextContent
(updated on every saveMessages call). Wire up .searchable in both
ChatSwitcherSheet (iPhone modal) and SpriteNavigationPanel (iPad/Mac
sidebar) to filter chats by name or conversation content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a global Quick Messages store (SwiftData) with full CRUD in Settings,
and a picker sheet accessible from the chat attachment menu that appends
the selected message to the current input.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts:
#	Wisp/Views/QuickActions/QuickActionsView.swift
…ts-c59cf06d' into zaki-main

# Conflicts:
#	Wisp/Views/SpriteDetail/Chat/ChatSwitcherSheet.swift
…af6' into zaki-main

# Conflicts:
#	Wisp/App/WispApp.swift
#	Wisp/Views/SpriteDetail/Chat/ChatInputBar.swift
#	Wisp/Views/SpriteDetail/Chat/ChatView.swift
Add QuickMessage files to Xcode project, add QuickMessage.self
to model container, fix ChatInputBar parameter order in ChatView.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Pass max_run_after_disconnect=300 on service creation so Claude keeps
  running for 5 minutes even if the client disconnects
- Increase idle timeout from 30s to 45s to avoid false timeouts during
  Claude thinking pauses (requires 2+ missed heartbeats)
- Increase post-stop retries from 1 to 3 with exponential backoff (1s, 2s, 4s)
- Add exponential backoff to reconnect polling (2s to 30s, resets on new events)
- Add WebSocket ping every 15s in ExecSession to detect dead connections
- Add .systemNotice content type to show disconnect/reconnect events inline
  in chat history (persisted across sessions)
- Prepend disconnect context to next --resume prompt so Claude knows its
  previous output may be incomplete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@zmanian
Copy link
Copy Markdown
Contributor Author

zmanian commented Mar 15, 2026

Replacing with a cleaner branch based off origin/main

@zmanian zmanian closed this Mar 15, 2026
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