iOS: Chat-Hosts, die Collection-View und die Outbox
Wie die iPhone-App besuchte Chats behält, Snapshots gegen neueren Live-Zustand anwendet, die Timeline in einer UIKit-Collection-View darstellt, das Scrollen verankert und eine Sendung prüft, deren Bestätigung verloren ging.
Gegen den Quellcode geprüft am 17. September 2026
Auf dieser Seite
Sechs zwischengespeicherte Hosts, einer auf dem Bildschirm
- Host-CacheWorkspaceChatHostCache
Hält bis zu sechs Chat-View-Models, das zuletzt gewählte zuerst, und hängt nur das gewählte ein. Ein siebter Chat gibt das am längsten nicht genutzte frei.
- Timeline-Leasechat:timeline-interest
Nur der Chat auf dem Bildschirm hält sie. Das Telefon erneuert sie alle 20 Sekunden, und das Relay bewahrt sie 60 Sekunden auf. Ohne sie erreichen die Live-Blöcke dieses Chats das Telefon nie.
- Nachladenrun.codexChatLoad
Die Rückkehr startet ein Laden des neuesten Fensters, das in die behaltenen Zeilen einfließt. Ein Chat auf dem Bildschirm prüft außerdem etwa alle 60 Sekunden, oder alle 10, solange ein Run, eine Nachricht oder ein Laden aussteht.
WorkspaceChatHostCache behält die View-Models kürzlich besuchter Chats, geordnet nach Desktop, Projektordner und Session, und hält höchstens sechs. Die Auswahl eines Chats während eines SwiftUI-Renderings merkt seinen Eintrag nur vor, und die Aufgabe der Auswahl übernimmt ihn danach, sodass der Cache nie mitten in einer View-Aktualisierung eine Änderung veröffentlicht. Nur der ausgewählte Host ist eingehängt.
Jeder Host besitzt einen Scheduler für das neueste Fenster mit einer aktiven Operation und einer ausstehenden Anfrage für sein aktuelles Ziel. Anfragen werden zusammengefasst, und ein Snapshot wird nur angewendet, wenn sein Ziel und die erfassten Zäune noch passen. Ein geparkter Host behält seine Zeilen und sein Live-Overlay, und sein Scheduler startet nichts, bis der Host wieder ausgewählt und aktiv ist. Scroll-Position und Aufdeckungszustand gehören zur nicht eingehängten View, daher kehrt er am Ende hinter einer kurzen Abdeckung zurück. Die iPhone-App hat keinen Batch-Abgleich und lädt keinen Chat, den Sie gerade nicht ansehen.
Ein beendeter, gescheiterter, abgebrochener oder gestoppter Run, eine Desktop-Wiederherstellung, die Rückkehr in den Vordergrund, ein anderer gewählter Desktop, eine ausdrückliche Aktualisierung, die erneute Auswahl des Chats und ein neueres updatedAt der Session markieren das gewählte Ziel als veraltet und können sein Laden einreihen. Solange der Chat auf dem Bildschirm bleibt, lädt er außerdem etwa alle 60 Sekunden das neueste Fenster, oder alle 10 Sekunden, solange ein Run, eine Nachricht oder ein Laden aussteht, und auf jede Wartezeit kommen bis zu 30 Prozent hinzu. Ein gescheitertes Laden lässt die sichtbaren Zeilen, wie sie sind, und markiert den Host für ein frisches Laden bei seiner nächsten Aktivierung. Subagent-Threads öffnen sich in einem eigenen Viewer mit einem separaten View-Model, das über run.codexChatLoadThread und run.codexChatLoadThreadPageBefore lädt.
Der Verlaufsumschlag wird exakt dekodiert
WorkspaceChatLoadEnvelope verlangt genau die kanonische Menge an Schlüsseln auf oberster Ebene. Mehrere Felder müssen vorhanden sein, auch wenn ihr Wert null ist. Der Decoder prüft ein nicht leeres boundaryToken und dekodiert Timeline-Blöcke über den gemeinsamen Seitendecoder. Jede Zeile braucht einen type aus user, system, activity, agent-reply und thinking, eine nicht leere id, einen origin von canonical oder live, ein nicht leeres sourceOrder aus vorzeichenlosen Ganzzahlen und einen ganzzahligen timestamp; Live-Zeilen brauchen eine runId und Live-Benutzerzeilen eine operationId, und ein doppelter Splice-Schlüssel lässt die ganze Seite scheitern.
boundaryToken: nonblank string
codexSessionId: string | null
loadState: typed load state
storageFormat: "indexed"
loadedEntries: integer
hasOlderEntries: boolean
oldestCursor: string | null
timeline: typed blocks[]
liveOverlay: { settlementEpoch, revision, blocks }
runtime: object | null
activeRun: object | nullDer Live-Overlay-Decoder erfordert exakte ganzzahlige Epochen- und Revisionswerte innerhalb des sicheren JavaScript-Bereichs. Eine Revision von null kann keine Blöcke enthalten. Jeder Overlay-Block muss den Ursprung live haben, und typisierte Blockidentitäten müssen eindeutig sein. Das Zurückweisen eines ungültigen Envelopes verhindert, dass fehlerhafte Daten zu einem scheinbar gültigen leeren Verlauf werden.
Ein Snapshot wird auf einen neueren Live-Zustand angewendet
- Live-Event-ZählertimelineLiveEventRevision
Steigt, wenn ein Live-Event die Overlay-Revision erhöht oder ein Run beginnt oder endet. Jedes Laden des neuesten Fensters merkt sich den Wert, mit dem es begann.
- Overlay-VersionsettlementEpoch · revision
Innerhalb einer Epoche gewinnt die höhere Revision, und eine niedrigere wird ignoriert. Ein Live-Event mit neuerer Epoche wird abgelehnt und löst ein erneutes Laden des neuesten Fensters aus.
- Veraltete TeileremovingStaleLiveRows
Hat sich der Zähler während des Ladens bewegt, werden die Live-Zeilen und der aktive Run der Antwort verworfen. Ihre kanonischen Zeilen gelten weiterhin.
Ist die Historie nicht verfügbar, bleiben das sichtbare Fenster und die Paginierung erhalten, und nur ein Overlay aus derselben Epoche wird angewendet. Danach werden Outbox-Einträge und Echos mit der bestätigten Historie abgeglichen, die Timeline-Revision steigt, wenn sich etwas geändert hat, und ein Aktualisieren, das ein nicht leeres Fenster durch Zeilen ohne Überschneidung ersetzt, rückt die Darstellungsepoche vor.
Eine UIKit-Collection-View in SwiftUI
- Render-TaktliveStreamingRenderMinInterval
Änderungen am Live-Overlay erreichen SwiftUI höchstens alle 1/6 Sekunde, oder alle 0,5 Sekunden, solange der Composer oder eine Vorschau offen ist.
- KoordinatorWorkspaceChatTimelineCollectionView.Coordinator
Vergleicht Item-IDs mit dem angewendeten Snapshot. Bei gleichen IDs konfiguriert er nur die geänderten sichtbaren Zellen neu, und Zeilen außerhalb des Bildschirms übernehmen neue Inhalte, wenn sie hereinscrollen.
- AnzeigezeilenWorkspaceChatTimelineLayerProjectionCache
Vom Chat-Tab aus den Zeilen des View-Models gebaut. Thinking-Zeilen verlassen die Liste, das Live-Thinking des aktiven Runs erscheint als angehefteter Status, und Subagent-Panel-Zeilen werden ausgeblendet.
- Zurückgehaltene AktualisierungdeferredUpdateRecoveryIntervalNanoseconds
Während Sie ziehen oder die Liste ausläuft, warten Änderungen an sichtbaren Zeilen oder an der Zeilenliste. Nur die neueste wird angewendet, sobald die Liste stoppt oder eine Prüfung alle 250 ms sie in Ruhe findet.
Die Timeline ist eine UICollectionView in einem UIViewRepresentable. Sie verwendet ein Compositional Layout mit einer vertikalen Sektion und geschätzten Höhen von 80 Punkten, eine Diffable Data Source, deren Elemente Zeilen-ID-Strings sind, und Zellen, deren Inhalt eine UIHostingConfiguration um die SwiftUI-Zeile ist. Die Invalidierung der Selbstgrößen schließt Constraints ein, sodass eine wachsende Zeile neu vermessen wird. Der Chat-Tab baut die Anzeigezeilen aus den Zeilen des View-Models: Thinking-Zeilen verlassen die Liste, das Live-Thinking des aktiven Runs wird zum angehefteten Status, und Subagent-Panel-Zeilen werden ausgeblendet. Die Collection selbst fügt nie eine Nachricht hinzu und entfernt keine.
Änderungen am Live-Overlay erreichen SwiftUI höchstens sechsmal pro Sekunde, oder zweimal pro Sekunde, solange der Composer oder eine Vorschau offen ist, und während eines Ziehens bleibt nur die neueste zurückgehaltene Aktualisierung erhalten.
Erste Anzeige, ältere Seiten und das Folgen am unteren Ende
- Settle-DurchlaufpassDelayNanoseconds
Alle 32 ms vergleicht die View Grenzen, Inhaltshöhe, Offset, Insets sowie Oberkante und Höhe jeder sichtbaren Zeile mit dem Durchlauf davor. Still heißt innerhalb von 0,5 pt, daher zählt der erste Durchlauf nach einem Reset nie.
- Aufbereitete ZeilenvisiblePreparedMarkdownRowsAreReady
Durchläufe zählen nicht, solange eine sichtbare Markdown-Zeile aufbereitet oder ein Snapshot angewendet wird.
- Kurzer VerlaufolderTimelinePreloadThreshold
Liegen höchstens 1.800 pt über dem Bildschirm, fordert die ruhende View eine ältere Seite an und kommt erneut zur Ruhe, bis zu zwei Seiten, bevor die Abdeckung fällt.
- FristmaximumSettlementPassCount
Nach 63 Durchläufen, etwa zwei Sekunden, fällt die Abdeckung, auch wenn das Layout nie stillhielt.
Die Abdeckung fällt nach etwa zwei Sekunden, auch wenn das Layout nie stillhält, und ein kurzer Verlauf lädt darunter zuerst bis zu zwei ältere Seiten. Die Darstellungsepoche verbindet die Session mit einer Timeline-Identität pro Viewport, parent-chat oder subagent-<thread ID>. Sie rückt vor, wenn ein Aktualisieren das Fenster durch Zeilen ohne Überschneidung ersetzt, wenn ein leerer, unbestätigter Chat ein frisches Laden startet oder wenn der Thread wechselt, sodass Scroll-Annahmen eines Fensters nie in ein anderes übergehen.
Ältere Seiten laden, wenn Sie bis auf 1.800 Punkte an den Anfang scrollen, höchstens alle 0,25 Sekunden, und ein Ladevorgang gilt nach 8 Sekunden als veraltet. Bevor Zeilen vorangestellt werden, merkt sich die Ansicht die erste sichtbare Zeile und ihren Offset, und nach der Aktualisierung scrollt sie diese Zeile zurück an ihren Platz und stellt den Offset wieder her. Live-Zeilen werden bis zu 1,5 Sekunden zurückgehalten, solange die Wiederherstellung läuft, damit eine streamende Antwort die Zeilen, die Sie lesen, nicht verschiebt.
Das Folgen am unteren Ende läuft auf einem CADisplayLink. Die Geschwindigkeit ist die verbleibende Strecke geteilt durch 1,5 Sekunden, begrenzt auf 80 bis 2.200 Punkte pro Sekunde, und mit aktivierter Option „Bewegung reduzieren“ springt die Ansicht stattdessen. Einrastdurchläufe laufen alle 32 Millisekunden, oder 350 Millisekunden nach einem animierten Scrollen, und enden nach zwei stabilen Durchläufen innerhalb von 1 Punkt und einem halben Punkt Höhenänderung oder nach acht Durchläufen.
Die Kopie der Outbox auf dem Telefon
Die Zustellung gehört dem Desktop. Das Telefon hält eine Wiederholungsmenge in UserDefaults unter workspace-chat-local-outbox-v2, wobei jeder Session-Bereich seine Operationsreihenfolge speichert und jede Operation entweder als ausstehend, mit dem vollständigen Eintrag, oder als abgeschlossen. Eine abgeschlossene Operation sperrt veraltete Schreiber für den Rest des Prozesses aus und wird beim nächsten Start, der eine neue Generation beginnt, entfernt. Ein Eintrag trägt seine Operations-ID, seinen Status, ob der Desktop ihn angenommen hat, eine ausstehende Mutation mit eigener Mutations-ID und den Sendemodus, und das Steuern eines laufenden Turns hält zusätzlich die aktive Run-ID fest.
Outbox-Änderungen einer Session laufen als serielle Kette. Eine Übermittlung wartet 20 Sekunden auf die Quittung des Desktops. Eine endgültige Ablehnung legt den Text zurück in den Composer, wenn sich der Entwurf wiederherstellen lässt.
- Unbekannter AusgangServerRelayError.timeout
Ein Timeout nach 20 Sekunden oder eine abgebrochene Verbindung ist keine Ablehnung. Der Eintrag bleibt mit „Desktop did not confirm this message“ in der Warteschlange, und noch wird nichts erneut gesendet.
- Outbox-Prüfungrun.codexChatOutboxLoad
Läuft, sobald die Verbindung nutzbar wird, oder alle 10 bis 13 Sekunden, solange im offenen Chat eine Nachricht wartet. Ist der Eintrag aufgelistet, in der Timeline sichtbar oder schon angenommen, wird nichts erneut gesendet.
- Derselbe Schlüsselrun.codexChatOutboxSubmit:<mutation ID>
Ein erneutes Senden verwendet die ausstehende Mutation und den gespeicherten Eintrag wieder, also wiederholen sich Schlüssel und Parameter. Der Desktop hebt eine fertige Antwort 48 Stunden auf und gibt sie zurück, statt doppelt zu speichern.
Anhänge werden vor der Nachricht hochgeladen. Der Desktop gibt eine Upload-ID und eine Blockgröße zurück, das Telefon begrenzt Blöcke auf 1 MiB und kodiert sie in Base64, und die Aufrufe verwenden die Schlüssel <upload key>:begin, :chunk:<n>, :finish und :cancel. Der Upload-Schlüssel ist files.chatAttachmentUpload:<session>:, gefolgt von einem 64-Bit-FNV-1a-Hash aus Session, Projekt, Dateiname, MIME-Typ, Pfad und Größe, sodass ein erneuter Versuch mit derselben Datei dieselben Schlüssel verwendet. Dateien dürfen bis zu 1 GiB groß sein, und ein PNG wird als JPEG mit Qualität 0,88 neu kodiert, wenn sein dekodiertes Bild höchstens 64 MiB braucht.
Tests, die dieses Verhalten festhalten
- WorkspaceChatTimelineSnapshotOrderingTests: Nur die aktive Operation darf ihren Snapshot anwenden, und eine geänderte Zielgeneration weist eine frühere ab.
- WorkspaceChatTimelinePaginationReloadTests: Ein Wiederherstellungs-Neuladen behält die Zeilen, bis sein Snapshot angewendet ist, und ein ausstehendes Neuladen übersteht einen vorübergehenden Verbindungsverlust.
- WorkspaceChatLiveTimelineConfirmationTests: Eine Live-Zeile wird nur über eine gemeinsame typisierte Identität abgeschlossen, nie über übereinstimmenden Text oder Zeitstempel.
Diese Suiten liegen in VibeUITests, das in der App gehostet läuft, deshalb brauchen sie ein iOS-Simulator-Ziel. Verhalten auf echten Geräten, etwa die erste Anzeige mit gespeichertem Markdown-Verlauf, wird weiterhin von Hand geprüft.