Zum Artikel springen
PlanToCodeDocsApp herunterladen

HandbuchiOS-App

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

Ein geparkter Chat behält seine Zeilen. Live-Ausgabe kommt erst bei der Rückkehr.
Ein geparkter Chat behält seine Zeilen. Live-Ausgabe kommt erst bei der Rückkehr.Zwei Chats auf demselben Desktop im Zeitverlauf. Solange Chat A auf dem Bildschirm ist, hält er die Timeline-Lease, und seine Live-Blöcke kommen an. Wenn Sie Chat B öffnen, wird Chat A geparkt: Er behält seine Zeilen, aber seine neuen Blöcke werden nicht zugestellt, weil jetzt B die Lease hält und nur die Live-Blöcke von B ankommen. Chat B beginnt mit einem ersten Laden. Wenn Sie zu Chat A zurückkehren, führt ein einziges Laden die neueren Zeilen mit den behaltenen zusammen, der Chat erscheint wieder am Ende hinter einer kurzen Abdeckung, und seine Live-Blöcke kommen wieder an. Darunter hält der Host-Cache sechs Chats, den zuletzt gewählten zuerst. Ein siebter Chat gibt den am längsten nicht gewählten frei, der danach wieder mit einem ersten Laden beginnt.
Chat A lesen
Chat B öffnen
Zurück zu Chat A
Chat A
Desktop
Chat B
auf dem Bildschirm · Lease alle 20 s erneuert
geparkt: Zeilen bleiben, nichts live
wieder am Ende, hinter kurzer Abdeckung
auf dem Bildschirm · hält die Lease
Live-Blöcke von A
neue Blöcke von A: nicht zugestellt, keine Lease
Erstladen
B live
neuere Zeilen ergänzen den Verlauf
Host-Cachezuletzt gewählter zuerst
G
A
B
C
D
E
F
Chat 7 verdrängt F. F lädt danach neu.
  • 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.

Felder der initialen Verlaufsantwort — Schema-Übersicht
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 | null

Der 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

Ein verspäteter Snapshot fügt seine Historie hinzu, setzt die Live-Zeilen aber nie zurück
Ein verspäteter Snapshot fügt seine Historie hinzu, setzt die Live-Zeilen aber nie zurückDas Chat-View-Model fordert beim Desktop das neueste Fenster an und merkt sich seinen Live-Event-Zähler, in diesem Beispiel 7. Der Desktop liest das Fenster bei Overlay-Revision 41, doch die Antwort braucht länger als ein Live-Event mit Revision 42, das das View-Model sofort zeigt und das den Zähler auf 8 erhöht. Wenn der Snapshot ankommt, prüft das View-Model zuerst sein Overlay, das die ganze Antwort bei einer älteren Epoche oder bei gleicher Revision mit anderen Blöcken verwerfen würde. Weil sich der Zähler bewegt hat, werden die Live-Zeilen und der aktive Run der Antwort verworfen. Ihre kanonischen Zeilen und die Paginierung werden übernommen, und ihr Overlay mit Revision 41 wird ignoriert, also bleibt Revision 42 auf dem Bildschirm.
Desktop
Chat-View-Model
neuestes Fenster laden
Zähler 7 gemerkt
liest das Fenster bei Revision 41
Snapshot · Revision 41
Live-Blöcke · Revision 42
Revision 42 angezeigt · Zähler 8
Snapshot kommt an
In diesem Beispiel würde der Snapshot, so angewendet, wie er ankam, Revision 41 zurück auf den Bildschirm bringen.
ältere Epoche, oder gleiche Revision mit anderen Blöcken?
ja: verwerfen und neu laden
Zähler von 7 auf 8 gestiegen
seine Live-Zeilen und der aktive Run fallen weg
Verlaufsfenster
Verlauf und Seiten übernehmen
Overlay-Revision 41 gegen 42
ignoriert: Revision 42 bleibt sichtbar
  • 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

Streaming zeichnet geänderte sichtbare Zellen höchstens sechsmal pro Sekunde neu und wartet beim Scrollen.
Streaming zeichnet geänderte sichtbare Zellen höchstens sechsmal pro Sekunde neu und wartet beim Scrollen.Zwei Sekunden eines streamenden Chats, maßstabsgetreu gezeichnet. Änderungen am Live-Overlay erreichen das View-Model höchstens alle 100 Millisekunden. SwiftUI rendert sie höchstens sechsmal pro Sekunde, und jedes Rendering konfiguriert nur die sichtbaren Zellen neu, deren Inhalt sich geändert hat. Kommt eine Zeile hinzu, wendet der Koordinator einen Diffable-Snapshot ohne Animation an. Während Sie die Liste ziehen oder sie ausläuft, warten Änderungen an sichtbaren Zeilen oder an der Zeilenliste, und das Rendern läuft weiter. Alle 250 Millisekunden läuft eine Prüfung, und sobald die Liste ruht, gilt nur die neueste zurückgehaltene Aktualisierung.
Text strömt in eine Zeile
Eine Zeile kommt hinzu
Liste ziehen
Overlay-Änderungen
Renderings
Collection-View
SwiftUI zu UIKit
höchstens einmal pro 100 ms
Rendern: max. alle 1/6 s
gleiche Item-IDs: nur geänderte sichtbare Zellen werden neu konfiguriert
neue Item-ID: ein Snapshot, keine Animation
gehalten beim Ziehen
alle 250 ms geprüft
in Ruhe: die neueste gehaltene Aktualisierung gilt einmal
0 s
0,5 s
1 s
1,5 s
2 s
  • 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

Ein neu gezeigter Chat bleibt abgedeckt, bis sein Layout drei Durchläufe lang stillhält, und nie länger als etwa zwei Sekunden
Ein neu gezeigter Chat bleibt abgedeckt, bis sein Layout drei Durchläufe lang stillhält, und nie länger als etwa zwei SekundenDrei erste Aufdeckungen, maßstabsgetreu über zwei Sekunden gezeichnet, mit einer Markierung pro Settle-Durchlauf von 32 Millisekunden. Im ersten Fall warten drei Durchläufe darauf, dass eine Markdown-Zeile fertig aufbereitet ist, ein Durchlauf findet das Layout verschoben, und drei stille Durchläufe folgen, also fällt die Abdeckung nach etwa 0,22 Sekunden. Im zweiten Fall ist der Verlauf kurz: Das Layout kommt zur Ruhe, eine ältere Seite wird angefordert, Durchläufe warten darauf, und die Abdeckung fällt, sobald das Layout wieder ruht. Im dritten Fall ändert das Layout ständig seine Größe, die Zahl stiller Durchläufe erreicht nie drei, und die Abdeckung fällt trotzdem bei Durchlauf 63, nach etwa zwei Sekunden.
Zeilen kommen zur Ruhe
Kurzer Verlauf
Ändert ständig die Größe
Durchlauf 63: die Abdeckung fällt trotzdem
Markdown abwarten, 1 bewegt, 3 still
aufgedeckt nach etwa 0,22 s
stabil, ältere Seite anfordern
wartet auf die Seite
wieder zur Ruhe: aufgedeckt
ändert ständig die Größe: die Zählung erreicht nie drei
0 s
0,5 s
1 s
1,5 s
2 s
Solange die Abdeckung liegt, ist die Liste verborgen und ignoriert Berührungen. Ein Settle, der läuft, nachdem der Chat schon sichtbar war, endet, sobald Sie ziehen.
  • 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.

Das Telefon kann eine verlorene Übermittlung nicht von einer verlorenen Quittung unterscheiden und prüft deshalb zuerst
Das Telefon kann eine verlorene Übermittlung nicht von einer verlorenen Quittung unterscheiden und prüft deshalb zuerstZwei Fälle mit demselben Zeitablauf, oben das Telefon, unten der Desktop. Im ersten geht die Übermittlung verloren, bevor sie den Desktop erreicht. Im zweiten speichert der Desktop die Nachricht und legt ihre Quittung unter dem Idempotenzschlüssel ab, aber die Quittung geht verloren. Das Telefon sieht in beiden Fällen dasselbe: Es wartet 20 Sekunden, lässt den Eintrag in der Warteschlange und lädt die Outbox des Desktops, sobald die Verbindung wieder nutzbar ist oder nach 10 bis 13 Sekunden. Im ersten Fall fehlt der Eintrag überall, also sendet das Telefon ihn mit demselben Schlüssel erneut, und der Desktop speichert ihn einmal. Im zweiten Fall ist der Eintrag aufgelistet, also wird nichts erneut gesendet. Hätte er die Outbox schon verlassen, ohne in der Timeline zu stehen, bekäme das erneute Senden mit demselben Schlüssel die gespeicherte Quittung zurück.
Wenn die Übermittlung den Desktop nie erreichte
Telefon
Desktop
Relay
wartet 20 s
Timeout: bleibt eingereiht
bis die Verbindung steht, oder 10–13 s
Outbox-Prüfung
fehlt
fehlt überall
senden
erneut, gleicher Key
einmal gespeichert
Wenn der Desktop sie speicherte und nur die Quittung verloren ging
Telefon
Desktop
Relay
wartet 20 s
aufgelistet
vorhanden: nicht erneut senden
gespeichert, Quittung mit Key
Quittung
Wenn der Eintrag schon aus der Outbox entfernt ist, liefert erneutes Senden die gespeicherte Quittung.
  • 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.