Zum Inhalt springen

Agentenstatus in Paneflow: von Bildschirmregeln zu OSC 7501

Seit Paneflow 0.17.7 übernimmt die Seitenleiste den Turn-Status eines Agenten aus genau einer Quelle: einem Status, den der Agent in sein eigenes Terminal schreibt. Ein Agent, der für eine Berechtigung anhält, schreibt eine Sequenz dieser Form (das Beispiel stammt aus der Spezifikation):

ESC ] 7501 ; state=blocked:kind=permission:app=terraform:msg=QXBwbHkgMyB0byBhZGQs... ESC \

Die übliche Annahme lautet, dass ein Tool, das Agenten ausführt, sie von außen beobachten muss: den Spinner erkennen, den Berechtigungsdialog finden, merken, dass das Eingabefeld zurück ist. Das habe ich gebaut, mehrmals, und wieder gelöscht. Heute bin ich der Meinung, dass der Turn-Status dem Agenten gehört, dem einzigen Prozess, der ihn tatsächlich kennt, und dass das Terminal die Meldung nur weitertragen soll, ohne zu raten.

Das ist der Stand in Paneflow 0.19.0. Er gilt für die Agenten, die OSC 7501 senden: Claude Code ab v2.1.295 und Codex, das die Unterstützung im Oktober mit openai/codex#52725 und #54114 gemergt hat. Ein Agent, der nichts meldet, bekommt absichtlich keinen Turn-Status.

Der Artikel folgt einem einzelnen Claude-Code-Turn vom Agenten bis in die Seitenleiste, dann kommen die allgemeine Struktur, die sechs Ansätze davor, die Kosten und der stärkste Einwand.

Was OSC 7501 ist

Wenn du Mitchells Beitrag zum Protokoll gelesen hast, überspring diesen Abschnitt.

Mitchell Hashimoto hat das Program Status Protocol am 6. Oktober 2026 veröffentlicht, als "a completely generic, terminal-native specification" (eine vollständig generische, terminal-native Spezifikation), die aus seiner Arbeit an Superlogical und Ghostty hervorging (sein Beitrag). Ein Programm schreibt OSC 7501 mit key=value-Paaren, getrennt durch Doppelpunkte:

  • state: idle, working, blocked, done oder error, dazu clear als Aktion.
  • kind, nur mit blocked: permission, question oder auth.
  • progress, app, title und msg (base64, bis zu 2.048 Byte).

Ein Programm fragt die Unterstützung mit OSC 7501 ; ? ab. Einen Heartbeat gibt es nicht. Wenn der Shell-Prompt zurückkommt (OSC 133 A) oder das Programm beendet wird, verwirft das Terminal working und blocked; done und error bleiben bestehen, bis das Terminal sie ausblendet, zum Beispiel beim nächsten Tastendruck.

Für Paneflow zählen zwei Eigenschaften. Der Status kommt von dem einen Prozess, der ihn kennt. Und die Lebensdauerregeln sorgen dafür, dass das Terminal nie einen Timeout für einen Status braucht.

Ein Claude-Code-Turn, vom Agenten bis in die Seitenleiste

Folgen wir einem einzelnen Turn durch die drei beteiligten Prozesse: den Sitzungshost, dem die PTYs gehören, den Worker, der den Status reduziert, und das Desktop-Fenster.

Paneflow antwortet wie Ghostty. Der Host startet jede Shell mit TERM_PROGRAM=ghostty (spawn_env.rs), und seine Terminal-Engine ist libghostty-vt. Wenn Claude Code fragt, ob das Terminal das Protokoll unterstützt, antwortet die Engine:

ESC ] 7501 ; ? : states=idle,working,done,blocked,error : kinds=permission,question,auth ESC \

Der Host sieht, wie diese Antwort zum PTY geht, und markiert die Sitzung als program_status_capable. Nur der Host antwortet. Der Desktop bindet libghostty zwar ebenfalls zum Zeichnen ein, registriert aber keinen OSC-7501-Callback, sodass der Agent nie zwei Antworten bekommt; ein Test namens desktop_terminals_neither_answer_nor_report_program_status stellt sicher, dass das so bleibt.

Du schickst einen Prompt ab, Claude Code schreibt state=working. Diese Bytes laufen durch denselben libghostty-Parser, der ohnehin das Grid pflegt, Paneflow scannt sie also nie ein zweites Mal. libghostty übergibt den geparsten Status an einen Callback, und die Event-Loop des Hosts macht daraus einen Eintrag:

ghostty::BackendEvent::ProgramStatus(report) => {
    self.program_status.apply(report);
    self.program_status_capable = true;
}

Ein Host-Thread geht alle 500 ms über die Sitzungen, überspringt die ohne Neuigkeiten und schreibt die aktuelle Meldung nur dann ins Manifest der Sitzung, wenn sie sich geändert hat. Der Worker folgt dem Host über einen Push-Stream.

Der Worker reduziert. paneflow-serve sitzt zwischen dem Host und jedem Client: dem Desktop, der paneflow-CLI, der MCP-Bridge. Er prüft zuerst, dass in der Sitzung ein bekannter Agent läuft, und bildet dann die Meldung ab. Das ist die gesamte State Machine (state.rs):

fn declared_verdict(&self) -> (Status, ActivitySource) {
    let Some(declared) = self.agent_declaration() else {
        return (Status::Idle, ActivitySource::None);
    };
    let running = self.lifecycle.is_running();
    let status = match declared.state.as_str() {
        DECLARED_ERROR => Status::Errored,
        DECLARED_WORKING if running => Status::Busy,
        DECLARED_BLOCKED if running => Status::Attention,
        _ => Status::Idle,
    };
    (status, ActivitySource::Declared)
}

Kein Timer, kein Hook und keine Glocke speist sie. Ein beendeter Prozess ist nie beschäftigt oder wartend, ein error übersteht dagegen das Beenden. Jede Änderung des reduzierten Status erhöht einen state_seq-Zähler, und genau dem folgen paneflow wait --idle und paneflow send --submit.

Claude Code fragt nach einer Berechtigung. Es schreibt state=blocked:kind=permission mit der Frage in msg. Der Worker setzt die Sitzung auf Attention und beschließt, weil es ein Übergang zu Attention ist, eine "Needs input"-Benachrichtigung (Eingabe nötig) mit dieser Nachricht. Der Desktop entscheidet nur, ob du die Pane schon vor dir hast. Die Zeile zeigt Need you und rückt im Agents-Bereich der Seitenleiste nach oben.

Der Turn endet mit state=done. Der Worker veröffentlicht eine "Finished"-Benachrichtigung (Fertig) und markiert die Sitzung als ungelesen. Die Zeile zeigt Done, bis du den Tab öffnest. Der Host behält den done-Eintrag, bis du in die Pane tippst, und Text, der über paneflow send oder das MCP-Tool write_pane gesendet wird, zählt nicht als Tippen.

Die allgemeine Struktur

Jeder Prozess hat eine Aufgabe. Der Host parst und hält Einträge vor, mit Grenzen: 256 Einträge pro Sitzung, und jenseits eines Schubs von 256 KiB ausstehender Status-Events löscht er alles, statt einen unvollständigen Zustand zu behalten. Der Worker reduziert und entscheidet über Benachrichtigungen, und nach einem Neustart baut er jede Sitzung aus den Manifesten neu auf. Der Desktop rendert, was er über einen Push-Stream empfängt, mit zusammengefassten Wakeups.

Was der Agent meldet und was du siehst:

GemeldetSeitenleiste
workingSpinner
blockedNeed you, dazu eine Benachrichtigung mit der Nachricht des Agenten
errorError, dazu eine Benachrichtigung
doneDone, bis du es gesehen hast, dazu eine Benachrichtigung
idleNichts
Nichts nach 3 sNo status

Was die Hooks noch tun

Claude Code und Codex bekommen weiterhin Paneflows Hooks, jeweils neun Events. Sie transportieren, was OSC 7501 nicht abdeckt: die Sitzungs-ID des Anbieters und den Transkriptpfad, mit denen sich ein Gespräch fortsetzen lässt, den Prompt, der den Tab benennt, das letzte Ergebnis, das die Benachrichtigung füllt, sowie Start und Ende von Subagenten. Den Status verändern sie nie, und ein Test hält das fest: a_hook_event_carries_metadata_but_never_moves_the_declared_state.

Ein Fallstrick: Claude Code führt diese Hooks synchron aus ("async": false, timeout: 5). Der Reporter gibt sich 350 ms für den Verbindungsaufbau und 350 ms für die Anfrage und beendet sich dann mit 0, ohne etwas auf stdout auszugeben. Im schlimmsten Fall kostet ein fehlendes Paneflow etwa 700 ms pro Event.

Sechs Ansätze vor diesem

Jede frühere Version dieses Features hat den Status abgeleitet. Der Reihe nach:

  1. Fenstertitel. Anfangs suchte ein Detektor in OSC-Titeln nach Braille-Spinnern. Am 7. April habe ich ihn zugunsten von Hooks aus einem Wrapper-Skript entfernt (107098da1).
  2. PTY-Aktivität. Im Juni bekamen Agenten ohne Hooks eine Heuristik: Ein Ausgabeschub stufte den Agenten auf working hoch, 3 s Ruhe stuften ihn wieder herab, das Echo von Tastendrücken wurde herausgefiltert. Eine Woche später habe ich sie entfernt, weil sie "produced false spinners (a dev server streaming under a bare-shell thread, TUI redraws)" (falsche Spinner erzeugte) (4200e8d49). Ein Fehlschlag.
  3. Ein Inaktivitätstimer. Ein Stalled-Status wurde nach 300 s Stille ausgelöst. Er flog im September raus: "A long turn is a long turn, not a fault." (Ein langer Turn ist ein langer Turn, kein Fehler.)
  4. Quellen ohne Hooks. Eine Organisation kann Claude-Code-Hooks mit disableAllHooks oder allowManagedHooksOnly abschalten. Im August habe ich OSC-9;4-Fortschritt, OSC-777-Benachrichtigungen und Claudes Sitzungsregisterdatei hinzugefügt, gegen die Hooks gewichtet, mit einer Übernahme nach 20 s (0f3da66ea).
  5. Ein dauerhafter Reducer. Im September wurden Hooks zur maßgeblichen Quelle, sobald eine Sitzung eingerastet war, ein fünfminütiger Lease, den Bildschirmänderungen neu stellten, begrenzte ein verlorenes Stop, und ein Viewport-Scan suchte nach Menüs. Im Oktober kam die Terminalglocke als Aufmerksamkeitssignal dazu.
  6. Deklarative Bildschirmregeln. Am 2. Oktober bekam jeder Agent eine screen.toml mit priorisierten Regex-Regeln, dazu einen signierten Remote-Katalog und einen Korpus aufgezeichneter Bildschirme (c1d48e580). Das wurde mit 0.17.6 ausgeliefert.

Die Designnotizen zu Ansatz 5 listen die Symptome auf, die ich immer wieder geflickt habe: Ein verlorenes Stop ließ eine Pane endlos denken, und Claude Code dokumentiert, dass Stop bei einer Unterbrechung durch den Nutzer nicht feuert, sodass ein Escape mitten im Turn den Spinner weiterlaufen ließ. Seine nummerierten Menüs lösen gar keinen Hook aus, also wurde eine Sitzung, die auf eine Auswahl wartete, als beschäftigt angezeigt. Jede Schicht existierte, um die darunterliegende zu korrigieren.

Am 7. Oktober habe ich libghostty neu gepinnt, um an seine OSC-7501-API zu kommen. Drei Tage später machte 328a8fe12 die Meldung zur einzigen Quelle des Turn-Status: 119 Dateien, 1.020 Zeilen hinzugefügt, 12.417 entfernt. Bildschirmregeln, der Remote-Katalog, die Aufzeichnungsbefehle, die Glocken-Aufmerksamkeit und die Escape-Behandlung sind alle verschwunden.

Drei Dinge gaben den Ausschlag. Claude Code und Codex meldeten ihren Status inzwischen selbst, und eine Meldung vom Agenten schlägt jede Ableitung, die ich von außen treffen kann. Die abgeleiteten Status lagen auf eine Weise falsch, die ich immer wieder flicken musste. Und die Regeln hatten Wartungskosten pro Agenten-Release: der Korpus, die Katalogsignierung, die Fixtures.

Auf ihren Fixtures lagen die Regeln allerdings nicht falsch. Die gelöschte Korpus-Baseline erreichte für jeden Agenten eine Genauigkeit von 1,0, bei jeweils 4 bis 7 Aufzeichnungen. Diese Zahl hat die Aufzeichnungen gemessen, die ich hatte, nicht das nächste Agenten-Release.

Die Alternative wäre gewesen, die Regeln als Fallback für Agenten zu behalten, die nichts melden. 0.17.7 liefert keinen Fallback aus: "Paneflow offers no replacement: the status comes back once the agent itself emits OSC 7501." (Paneflow bietet keinen Ersatz: Der Status kommt zurück, sobald der Agent selbst OSC 7501 sendet.)

Die Identität wird abgeglichen, der Status nicht

An einer Stelle ist Paneflow agentenspezifisch: beim Wissen, welcher Agent in einer Pane läuft. Der Katalog runtimes/ beschreibt 13 Agenten, und der Host gleicht den Vordergrund-Job der Pane damit ab.

Unter Linux durchläuft er /proc/<pid>/task/<tid>/children und liest cmdline. Unter macOS verwendet er proc_listpids(PROC_PGRP_ONLY) und KERN_PROCARGS2. Unter Windows, wo es keine Prozessgruppen gibt, durchläuft er bis zu 64 Nachkommen des Session-Leaders und liest die Befehlszeile jedes Prozesses aus dessen PEB. Ein node-, bun- oder python-Wrapper wird über den Skriptpfad aufgelöst, sodass ein per npm installiertes Claude Code, das unter node.exe läuft, trotzdem erkannt wird, während -e, -c und -m nie einen Agenten benennen. Die Startzeit aus dem Kernel wird vor und nach den Argumenten gelesen, damit eine wiederverwendete PID nie dem falschen Agenten zugeordnet wird.

Tipp: Der Alias fx ist auch der Name eines JSON-Viewers. Paneflow zählt fx nur dann als Agenten, wenn der Pane-Titel mit fx v beginnt.

Ein falscher Abgleich kann falsch anzeigen, welcher Agent in einer Pane läuft. Er kann nicht erfinden, was dieser Agent gerade tut.

Was es kostet

Unter Windows 11 Pro, Ryzen 7 7800X3D, RTX 4070 Ti SUPER, Release-Build von 0.18.0 (b57da648), lief in einer Pane 62 s lang ein Fixture-Programm, das über OSC 7501 working meldete (Benchmark-Zusammenfassung):

ProzessCPU (ein Kern)
Host0,00 %
Worker0,05 %
Desktop4,80 %, davon Main-Thread 3,66 %

Der Desktop zeichnete 11,13 Root-Renders pro Sekunde, und das ist der Spinner. Eine gemeinsame Animationsuhr mit 90 ms treibt jeden Spinner in der Seitenleiste an, Tests legen 10 bis 12 Sidebar-Wakeups pro Sekunde für 1 oder 8 arbeitende Agenten fest, und reduce_motion zeichnet einen statischen Spinner ganz ohne Uhr.

Die Latenz bekam einen eigenen Fix. Der Host liest OSC 133 A bereits aus der Shell-Integration, und seit dem 31. Oktober weckt ein Prompt-Start den Scan, statt auf den nächsten 500-ms-Durchlauf zu warten. Vom Beenden des Agenten bis zur Freigabe ging es über fünf Zyklen von 434 bis 491 ms auf 6 bis 9 ms herunter (96c525144).

Wichtig: Das CPU-Argument gegen Bildschirmregeln ist schwächer, als es klingt. Meine liefen mit "20 rules on 200x60 evaluate in 24 us p95". Entfernt habe ich die Wartung und die falschen Status, keine heiße Schleife. Kein Benchmark isoliert die Kosten von OSC 7501 selbst, da es auf einem Parse mitläuft, den das Terminal ohnehin erledigt.

Und Agenten, die nichts sagen?

Das ist der stärkste Einwand, und er ist berechtigt. Claude Code und Codex melden ihren Status. Jeder andere Agent im Katalog bekommt erst einen Turn-Status, wenn er ebenfalls OSC 7501 sendet; bis dahin bekommt er keinen. Nach 3 s ohne Support-Abfrage oder Meldung zeigt die Zeile ein gedämpftes No status-Badge, statt Leerlauf vorzutäuschen, mit einem Tooltip, der die erste meldende Version nennt, sofern Paneflow sie kennt (Claude Code 2.1.295).

Für einen solchen Agenten meldet paneflow send --submit no_signal, und paneflow wait --idle wartet, bis die Ausgabe ruhig wird, statt am Ende des Turns zurückzukehren.

Ein paar Fallstricke aus Claude Codes Terminal-Dokumentation: Innerhalb von tmux prüft Claude Code die Unterstützung bei tmux statt bei deinem Terminal; eine Hintergrundsitzung meldet ihren Status nicht; und CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1 schaltet die Meldung ab. Bei Codex weiß ich noch nicht, welches Release die beiden PRs als erstes ausliefert.

Herdr steht auf der anderen Seite dieses Tauschs und zeigt heute für mehr Agenten einen Status an. Für Claude Code und Codex heißt es in seiner Integrationsdokumentation, der Status "comes from Herdr's screen manifest detection" (stammt aus Herdrs Erkennung per Bildschirm-Manifest), während die Hooks die Sitzungsidentität melden; für Pi, OpenCode und einige andere verwendet es den Status, den deren Hooks melden. Sein Claude-Manifest ist sorgfältige Arbeit: priorisierte Regeln über Bildschirmbereiche, mit Kommentaren, die Claude Codes Spinner-Glyphen Version für Version nachverfolgen. Mitchell merkte an, dass die Historie der Datei "shows ten changes in three months for just Claude Code" (zehn Änderungen in drei Monaten allein für Claude Code zeigt), und fügte hinzu, dass das keine Kritik an Herdr sei. Dem stimme ich zu. Das sind die Kosten, die ich nicht tragen wollte, und No status ist der Preis dafür.

Danke an Mitchell Hashimoto für die Spezifikation, an das Ghostty-Projekt für den libghostty-vt-Parser, der Paneflow den Status übergibt, und an die Teams von Claude Code und Codex dafür, dass sie es ausgeliefert haben.

Wenn du eine CLI pflegst, die jemals auf einen Menschen wartet, sende OSC 7501. Was ein Terminal damit macht, siehst du, wenn du das hier in einer Paneflow-Pane ausführst, deren Kopfzeile dann zehn Sekunden lang blocked: permission · Apply? anzeigt:

printf '\033]7501;state=blocked:kind=permission:app=deploy:msg=QXBwbHk/\033\\'; sleep 10

Diesen Workflow fortsetzen

Alle Beiträge

Starte deinen nächsten Agent in Paneflow.