Skip to content

fix(ios): guest removal actually clears the composer chip#7

Merged
boggspa merged 1 commit into
masterfrom
fix/ios-guest-remove
Jun 18, 2026
Merged

fix(ios): guest removal actually clears the composer chip#7
boggspa merged 1 commit into
masterfrom
fix/ios-guest-remove

Conversation

@boggspa

@boggspa boggspa commented Jun 18, 2026

Copy link
Copy Markdown
Owner

The bug

On iOS, tapping the X on the guest chip (or trying to remove the guest at all) didn't work — the guest agent would acknowledge a change in its child chat (the "ensemble participant change" the user saw) but the guest was never removed.

Root cause (two compounding issues)

  1. Removal was invisible to the phone. removeGuestParticipant clears the parent's guestParticipant and marks the guest child chat lifecycleState: 'closed' — it does not delete it (store/index.ts). But iOS detects the active guest purely from that child card (isGuestSideChat, Models.swift), and the projection never sent the lifecycle. So the closed child kept satisfying the detector → the chip lingered.
  2. The X was nearly untappable. It was an 8pt glyph with no .frame/.contentShape → ~8×8pt hit area (HIG minimum is 44pt). Taps missed, so the user fell back to re-picking a provider in the menu, which re-ran setGuestParticipant and made the guest agent acknowledge the change — without removing it.

Fix

  • Mac (RemoteTaskProjection.ts): project sideChatLifecycleState on the task card.
  • iOS (Models.swift, RemoteSessionModel.swift): add RemoteTaskCard.sideChatIsActive (absent ⇒ active, back-compat with older Mac builds); guestParticipant(of:) now requires isGuestSideChat && sideChatIsActive, so a removed guest drops out and the chip clears. The child stays labelled "Guest" in the Side-chats history.
  • iOS UX (TWSharedViews.swift): give the X a real 22pt hit target + accessibility label, and add a destructive "Remove guest" item to the provider/model picker menu (the user's suggestion) as a reliable, discoverable removal path.

Tests

  • RemoteTaskProjection.test.ts: projects active and closed lifecycle.
  • New GuestCardLifecycleTests.swift: pins the decode + sideChatIsActive predicate (active / closed / terminated / absent).

Verification

  • Mac typecheck ✓ · RemoteTaskProjection.test.ts 12/12 ✓
  • iOS swift build ✓ · swift test 73/73 ✓ · simulator app-target build BUILD SUCCEEDED

Rollout note

Takes effect end-to-end only once both a Mac release (the projection) and an iOS build (the detector/UX) ship — same as the turn-based guest fix. The "Remove guest" menu item + bigger X tap target work on the iOS build alone; the chip actually clearing needs the Mac side too.

🤖 Generated with Claude Code

The X on the iOS guest chip never removed the guest. Two compounding causes:

1. removeGuestParticipant marks the guest CHILD chat lifecycleState:'closed'
   (it is not deleted), but the projection never sent the lifecycle, and the
   phone detects the active guest purely from that child card (isGuestSideChat).
   So the closed child kept satisfying the detector and the chip lingered.
2. The inline X was an 8pt glyph with no frame (~8x8pt hit area, well under the
   44pt minimum), so taps usually missed — the user fell back to re-picking a
   provider, which made the guest agent acknowledge the change without removing
   it (the "ensemble participant change" they saw).

Fix:
- Mac: project sideChatLifecycleState on the task card.
- iOS: RemoteTaskCard.sideChatIsActive (absent => active, back-compat);
  guestParticipant(of:) now requires isGuestSideChat && sideChatIsActive, so a
  removed guest drops out and the chip clears.
- iOS UX: give the X a 22pt hit target + a11y label, and add a destructive
  "Remove guest" item to the provider/model picker menu (per the user's
  suggestion) as a reliable, discoverable removal path.

Tests: RemoteTaskProjection projects active/closed lifecycle; new
GuestCardLifecycleTests pins the decode + sideChatIsActive predicate
(active/closed/terminated/absent).

Needs both a Mac release (projection) and an iOS build to take effect end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@boggspa boggspa merged commit ea5b487 into master Jun 18, 2026
16 checks passed
@boggspa boggspa deleted the fix/ios-guest-remove branch June 18, 2026 14:17
boggspa added a commit that referenced this pull request Jun 18, 2026
Build 22 = merged iOS batch:
- PR #6: compact idle composer (collapses to a one-line text + send + model
  pill per shell when not typing/empty; expands on focus or content).
- PR #7: guest removal actually clears the composer chip (Mac projects
  sideChatLifecycleState; iOS filters closed guests; bigger X hit target +
  "Remove guest" picker item).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
boggspa added a commit that referenced this pull request Jun 23, 2026
…lassified

Roadmap #7. In tool-capable turns shouldReleaseOllamaContentDelta gated every
release on `streamed.length + pending.length >= 24 || sentence-end`. Because
the threshold is relative to total visible length, a short released sentence
keeps resetting it, so prose was re-buffered ~24 chars after every short
sentence — the source of Ollama's uniquely choppy cadence. Once prose has
begun streaming (streamed non-empty, having passed the unchanged hold-guard
that vets the cumulative content on every chunk), the turn is classified as
ordinary assistant text, so release per token. The first-exposure length/
sentence gate and the JSON/tool-stub hold-guard are preserved.

Exports the gate and adds focused unit tests, including the per-token
regression that fails under the old re-buffering behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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