enhancement
major
minor
defect
critical
major
minor
major
#30126
tl-chat: Chat-Aktionen laufen als React-Commands des Fensters statt über einen eigenen ChatServlet; der Proxy-Workaround des Chats entfällt zugunsten des Pollings der Engine
Kontext
Der Chat der React-Oberfläche (Modul tl-chat: <chat-conversation>, <chat-rail>, <chat-dock>, Sections) besteht aus React-Controls, sprach mit dem Server aber zum größten Teil über eine eigene JSON-API: ChatServlet unter /chat/* mit rund 20 Endpunkten (send, respond, cancel, regenerate, truncate, feedback, sessions, preferences, approval-mode, improve-prompt, subagents, interactions, capabilities, updates, stream), aufgerufen per fetch aus tl-chat/ui/src/api.ts. Die API stammte aus der Zeit, als der Chat eine eigenständige Seite war.
Daraus folgte:
- Jeder Aufruf musste sein Fenster und seine Unterhaltung mitschicken (windowId, conversation); der Server fand sie über ChatSessionRegistry (Rückwärtssuche der Sub-Session, Map CHAT_TRACKERS) und den zwischengespeicherten Fensterzustand ChatViewBinding wieder. Zwischengespeicherter Zustand dieser Art hat wiederholt veraltete Werte geliefert (Layout, Sprache, Anmeldezustand #30109).
- Eigene Zugangsprüfung (ChatSessionFilter, 401 für Anonyme).
- Die Aufrufe liefen an der Befehlsreihenfolge des Fensters (command-channel.ts) vorbei und waren für Script-Recorder, Replay und headless Agents (ScriptingSession) unsichtbar.
Zusätzlich brachte tl-chat einen eigenen Ausweg für Proxys mit, die den Event-Stream des Fensters zurückhalten (script/tl-window-stream.js, WindowPollServlet, PolledWindow, Gitea #940). Das Skript ersetzte vor der Bridge window.EventSource und umhüllte window.fetch. Seit #29878 pollt die Engine selbst (react-api/poll, PollServlet, sse-client.ts), das Skript prüfte das aber nicht: In jeder Anwendung mit tl-chat liefen beide Mechanismen übereinander.
Änderung
- Der Proxy-Workaround des Chats entfällt ersatzlos. Hinter einem puffernden Proxy übernimmt das Polling der Engine.
- Die Aktionen des Chats sind React-Commands der Chat-Controls. Zustand kommt über den Control-Zustand, laufende Antworten weiter über den Event-Stream des Fensters, Anhänge über den UploadHandler der Engine, große Lesezugriffe (Transkripte) über die fensterbezogene Route /react-api/data.
- Damit entfallen ChatServlet, ChatSessionFilter, der Rückwärts-Lookup in ChatSessionRegistry und der zwischengespeicherte Fensterzustand in ChatViewBinding.
Lösung
- Antwort eines React-Commands (Engine, com.top_logic.layout.react): Ein @ReactCommandHandler oder UploadHandler kann seinem HandlerResult eine Antwort mitgeben (ReactCommandReply.create().put(...).toResult()); ReactServlet schreibt deren Felder neben success in die Antwort des Befehls. Im Client liefern useTLCommand() und useTLUpload() diese Antwort (CommandResponse). Der Script-Recorder ignoriert die Antwort; der headless Zugang (/agent-api /act) gibt sie als reply mit aus. Beschrieben im Artikel view-layer/client-server-protocol ("Answering a command").
- Weitere Engine-Ergänzungen: SSEUpdateQueue.registerListener lässt Patches an eine Listener-Id, die kein Control ist (der Frame-Strom des Chats), das Ende einer Interaktion überstehen. Der Zustand eines ReactControl darf msgbuf-Objekte enthalten. CommandOptions.ignoreStaleUi lässt einen Befehl, der nur hinter etwas Verschwundenem aufräumt, bei einer veralteten Seite verwerfen statt die Seite neu zu laden.
- Befehle der Chat-Controls: Alle Chat-Controls (<chat-conversation>, <chat-rail>, <chat-dock>, Sections) haben die gemeinsame Basis ChatControl mit den Befehlen: Stream an-/abmelden, Laufzustand anfordern, Unterhaltung öffnen und löschen, Senden (legt beim ersten Senden die Unterhaltung an), Bearbeiten und erneut senden (ein Befehl, damit ein abgelehntes Senden keine Antworten verliert), Neu generieren, Anhang entfernen, Freigabe/Rückfrage beantworten, Lauf abbrechen, Bewertung, Freigabemodus, Prompt verbessern und abbrechen, Nachricht an einen Subagenten, Subagent abbrechen, Layout speichern. Ablehnungen (beschäftigt, keine Berechtigung, Limit erreicht) kommen als Status und Begründung in der Antwort des Befehls.
- Zustand statt Abfragen: Fähigkeiten (Agenten, Modelle, MCP-Server) und die Liste der Unterhaltungen stehen im Control-Zustand; ändert sich die Liste, schreibt der Server sie neu. Transkripte (auch von Subagenten) liest der Browser über die Daten-Route /react-api/data des Controls. Laufzustand, offene Freigaben/Rückfragen und Subagenten kommen beim Anmelden des Streams in einem run_state-Frame und beim Öffnen einer Unterhaltung in der Antwort des Befehls. Die Kontext-Fakten der Seite (<context>) wertet der Server beim Senden selbst aus.
- Anhänge werden über den UploadHandler des Controls hochgeladen und liegen bis zum Senden serverseitig in der Anhangsliste der Unterhaltung (Zustand draftAttachments); kein Base64 mehr im JSON. Typ, Größe (10 MB bzw. das Upload-Limit der Anwendung) und Anzahl (5) prüft der Server mit eigener Meldung.
- Prompt-Verbesserung läuft im Hintergrund; ihr Ergebnis kommt als prompt_improved-Frame, damit der LLM-Aufruf die Befehlsreihenfolge des Fensters nicht blockiert.
- Anmeldung im selben Fenster: Der Chat berechnet Anmeldezustand und Namen bei jedem Rendern. Wechselt die Person eines Fensters, bekommt sie eine eigene Bindung und einen eigenen Tracker; laufende Antworten, Freigaben und Rückfragen der vorigen Person in diesem Fenster werden abgebrochen, und ihre Präsentationen erreichen die Seite der neuen Person nicht.
- Entfallen: ChatServlet (/chat/*) samt Request-/Response-DTOs, ChatSessionFilter, ChatSessionRegistry.windowId/`resolveSubSession`, die zwischengespeicherten Werte in ChatViewBinding, api.ts-Aufrufe per fetch, expectFrames(), der Proxy-Workaround (tl-window-stream.js, WindowPollServlet, PolledWindow). ReactViewComponent (tl-chat-classic) nimmt den Fensternamen von seiner Sub-Session.
Geprüft
- Testsuiten von com.top_logic.layout.react, com.top_logic.layout.view, tl-chat (Java und vitest) und tl-chat-classic; die bisherigen Servlet-Tests sind auf die Befehle umgestellt (ChatCommandDriver).
- tl-chat/proxy-rig/: alle 14 Profile (direkt, puffernde, komprimierende, SSE verweigernde und blockierende Proxys; React-Seite und klassischer Tab) bestehen auf dem Stand nach dem Merge von master; hinter einem puffernden Proxy übernimmt das Polling der Engine.
- Durchgang im Browser in tl-ai-demo (React-Seite und klassischer Tab).
Migration
Nur nötig für Anwendungen, die die Chat-API selbst ansprechen oder Klassen von tl-chat aus eigenem Code verwenden:
- Die JSON-API /chat/* (ChatServlet) gibt es nicht mehr; ein eigener Client ruft stattdessen die Befehle der Chat-Controls auf (Namenskonstanten in ChatControl), z. B. über den headless Zugang /agent-api.
- ChatSessionRegistry: windowId(session, subSession), resolveSubSession(session, windowId) und resolveTracker(session, windowId, conversation) entfallen. Den Tracker eines Fensters liefert tracker(subSession, conversation, account); der Fenstername steht an der Sub-Session (subSession.getLayoutContext().getWindowId().getWindowName()).
- TaskProgressTracker: get/setUserName, get/setWelcomeName und get/setWindowId entfallen; das Konto des Trackers liefert getAccount().
- ChatViewBinding: writeSharedState, subjectOf, windowId, contextPath, userName, welcomeName, language und authenticated entfallen (der Zustand wird bei jedem Rendern von writeRenderState geschrieben). presentable und presentableNames erwarten zusätzlich das Konto des Laufs.
- useTLCommand() und useTLUpload() liefern jetzt die Antwort des Befehls (Promise<CommandResponse | undefined> statt Promise<void>); bestehende Aufrufer, die das Ergebnis ignorieren, bleiben unverändert.