Skip to content

iOS: switching tasks leaves the old transcript on screen, and sends to the new thread #313

Description

@KesleyDavid

Summary

Switching tasks while a chat is open leaves ChatView rendering the previous task's transcript. Because the composer targets the bot's live thread while the transcript is read from a frozen one, a message typed on that screen is delivered to a conversation the user is not looking at, and never appears.

The screen looks entirely normal throughout — the old transcript is still plausible, so there is nothing to suggest the two halves disagree.

Steps to reproduce

  1. Open a bot's chat.
  2. Tap the name → Tasks, and pick a different task. (Or + → New task, which is a single tap.)
  3. The sheet closes and the transcript does not change — it is still the previous task's.
  4. Type a message and send it. It is delivered to the newly-selected task's thread, and does not appear on screen.

It also happens with no interaction on the phone at all: switch the task on the desktop, and an open ChatView stays on the old thread as the .bot frame lands.

Cause

ChatView is handed a Chat value by navigation (ios/App/ChatListView.swift:114), which is a snapshot of the bot at push time. The file already accounts for that snapshot going stale — current re-reads the live record from the store:

// ios/App/ChatView.swift:44-51
/// The live chat record, so busy/unread stay current as frames land.
private var current: Chat { … session.state.bot(bot.id).map(Chat.bot) ?? chat … }

…but the transcript is read from the frozen one:

// ios/App/ChatView.swift:40-42
private var messages: [Message] {
    session.state.visibleTranscript(forThread: chat.threadId)
}

Switching a task changes bot.threadId (server/store.ts:992, via ios/App/TaskManagerView.swift:23 → ios/App/Session.swift:597-601), so from that moment chat.threadId and current.threadId refer to different conversations.

Nothing rebuilds the view: it has no .id(…) keyed on the thread, none of the three .task {} blocks are keyed, and Chat's ==/hash use only id (ios/App/Session.swift:703-720) — so SwiftUI considers the old and new Chat the same navigation value.

The store keeps the old transcript rather than dropping it (ios/Sources/CompanionCore/Store.swift:196-215 writes messages[bot.threadId] without clearing the previous thread's entry), which is why the stale screen looks healthy instead of empty.

Why it is more than a display bug

The actions on that screen use current — the new thread — over messages rendered from the old one:

  • ios/App/ChatView.swift:509 — session.send(text, to: current) sends by bot id, and the harness delivers to bot.threadId. The message lands in the task the user is not reading.
  • ios/App/ChatView.swift:113 — MessageRow(chat: current, …) propagates the mismatch, so a card answered at :845/:874 pairs a requestId from the old thread with the new threadId, and a reaction at :611/:640 does the same with messageId.
  • ios/App/ChatView.swift:676 — ScreenShot(threadId: chat.threadId) requests an image for a message that is not in the new thread.
  • ios/App/ChatView.swift:169 — the face already reflects the new thread while the transcript shows the old one.

Deleting the active task (ios/App/Session.swift:611-615) leaves the screen showing a thread that no longer exists.

Not a regression from #232

git show 70805c0:ios/App/ChatView.swift has the same shape (chat.threadId for the transcript, current for the live record). #232 moved the code without changing this.

Switching bots is fine

Chat.id is the bot id, so opening a different bot pushes a distinct navigation value and builds a fresh ChatView with the right thread.

The correct pattern already exists in the repo

Session.open(_ hit:) (ios/App/Session.swift:559-574) does it right for search results — it calls switchTask, applies the frame, and then re-reads the record before handing it to navigation:

bot = try await client.switchTask(botId: bot.id, threadId: hit.threadId)
state.apply(.bot(bot))
…
return state.bot(bot.id).map(Chat.bot)

The same re-read is what's missing when the switch happens with ChatView already on screen.

Suggested direction

Read the transcript from the live record (current.threadId) at the six sites that currently use chat.threadId — :41, :84, :91, :126, :129, :199 — and key the view's identity on the thread (.id(current.threadId), or .task(id: current.threadId)) so streaming, scroll position and the .task blocks restart on a switch. Making Chat's equality account for threadId would also let a re-appended value invalidate the navigation entry.

Notes

Found while porting the companion to Android (#241). Verified against d487882.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions