Vom Ergebnis zum Prozess
Der erste Teil dieser Reihe hat die Hauptansicht behandelt — die Gegenüberstellung zweier Antworten als didaktische Bühne. Sie beantwortet eine Ja-oder-Nein-Frage, und sie beantwortet sie überzeugend: Der angebundene Server bewirkt einen sichtbaren Unterschied. Doch sie schweigt darüber, wie dieser Unterschied zustande kommt. Welche Werkzeuge das Modell in welcher Reihenfolge wählt, mit welchen Argumenten es sie aufruft, wie deren Ergebnisse in den weiteren Dialog zurückfließen und an welcher Stelle aus mehreren Durchläufen schließlich eine zusammenhängende Empfehlung wird — all dies läuft im automatischen Betrieb verborgen ab. Die mitlaufende Zeitleiste der Hauptansicht deutet den Verkehr an, hält ihn aber nicht an.
Eben hier setzt der vorliegende zweite Teil an. Er behandelt die Schritt-für-Schritt-Ansicht, eine eigenständige Ansicht unter der Route mcp-walkthrough mit dem Titel „MCP Server — Step by Step“. Wo die Hauptansicht das Ergebnis zweier Anfragen nebeneinanderstellt und den Mechanismus dabei verbirgt, kehrt die Schritt-für-Schritt-Ansicht das Verhältnis um. Sie hält die Pipeline an, macht jede Phase einzeln auslösbar und verwandelt den ansonsten flüchtigen Ablauf in einen lesbaren, zeitlich gegliederten Gegenstand. Der didaktische Gewinn liegt im Verlangsamen: Was die Hauptansicht in einem Zug abspult, zerlegt die Vorführansicht in nachvollziehbare Schritte, die der Betrachter in seinem eigenen Tempo durchschreiten kann.
Der Quelltext befindet sich auf GitHub unter:

Der Rundgang dieses Teils folgt deshalb nicht mehr den Bestandteilen einer Oberfläche, sondern den Hürden, die das Anhalten und Sichtbarmachen eines an sich automatischen Vorgangs mit sich bringt. Das erste und tragende Kapitel behandelt die architektonische Voraussetzung: die Zerlegung der automatischen Werkzeugschleife in einzelne, fortsetzbare Phasen, die beide Ansichten gemeinsam nutzen. Darauf folgen die Verarbeitungsspur als Zeitachse, das einzelne Auslösen der Phasen, das Datenpanel neben der Spur, die Darstellung paralleler Werkzeugaufrufe sowie das Streaming und Rendern innerhalb einer einzelnen Phasenkarte. Ein Schlusskapitel führt beide Teile der Reihe zusammen.
Wie schon im ersten Teil wird allgemeine Vertrautheit mit Vaadin vorausgesetzt; besondere Elemente werden in ihrer Wirkung ausführlich dargestellt. Die technische Grundlage entspricht der des ersten Teils: JDK 26, Vaadin Flow 25.1.6 mit eingebettetem Jetty 12.1.10 und Jackson zur JSON-Verarbeitung, ohne Spring und ohne Jakarta EE. Beide Ansichten beruhen auf derselben Protokolllogik und unterscheiden sich allein in ihrem Antrieb und in ihrer Darstellung — eine Tatsache, die das folgende Kapitel zum Ausgangspunkt nimmt.
Vom automatischen Loop zur angehaltenen Pipeline
Die Schritt-für-Schritt-Ansicht und die Hauptansicht zeigen denselben Vorgang. Das ist keine Behauptung, sondern eine architektonische Tatsache, und sie ist die Voraussetzung für alles, was dieser Teil noch behandelt. Die Hauptansicht treibt die Werkzeugschleife automatisch: Sie ruft das Modell auf, führt die angeforderten Werkzeuge aus, fügt deren Ergebnisse dem Dialog hinzu und ruft das Modell erneut auf, bis keine weiteren Werkzeuge mehr verlangt werden. Die Vorführansicht muss genau diese Schleife anhalten und in einzeln auslösbare Schritte zerlegen — ohne dass die Protokolllogik dabei verdoppelt wird. Würde sie verdoppelt, so zeigten die beiden Ansichten womöglich zwei verschiedene Abläufe; der didaktische Wert der Vorführung beruht aber darauf, dass sie denselben Ablauf zeigt, nur verlangsamt.
Die Lösung liegt in einem eigenen Träger der Protokolllogik, dem WalkthroughEngine. Jede seiner Methoden führt genau eine Phase aus, verändert den übergebenen Zustand und gibt das Ergebnis als Phasendatensatz zurück, den die Oberfläche darstellen kann.
/** Phase 1. Build a fresh state seeded with the system + user message. */
public WalkthroughState startRun(String systemPrompt, String userMessage) {
return new WalkthroughState(systemPrompt, userMessage);
}
/** Phase 2. Fetch the MCP tool catalogue and attach it to the state. */
public WalkthroughPhase.ToolCatalog fetchToolCatalogue(WalkthroughState state) {
ArrayNode tools = toolBridge.toolsForOllama();
state.setToolsArray(tools);
List<String> names = new ArrayList<>(tools.size());
for (JsonNode tool : tools) {
String name = tool.path(“function”).path(“name”).asText(null);
if (name != null) names.add(name);
}
WalkthroughPhase.ToolCatalog phase = new WalkthroughPhase.ToolCatalog(names, tools);
state.setCurrentPhase(phase);
return phase;
}
Der entscheidende Gedanke steht im Klassenkommentar des Motors: Die Protokolllogik liegt an einer einzigen Stelle, und lediglich der Antrieb unterscheidet sich. Der Klassenkommentar formuliert es so: „The engine is shared by the comparison view (driving the phases in an auto loop) and the walkthrough view (driving them one user click at a time); the protocol logic therefore lives in a single place.“ Die fünf Phasen sind als Methoden ausgeführt — startRun, fetchToolCatalogue, runModelQuery, executeTools und finalise —, und jede ist für sich aufrufbar. Eben weil jede Phase eine Methode ist, kann die Vorführansicht sie einzeln auf einen Tastendruck hin aufrufen, während die Hauptansicht sie in einer einzigen Schleife auf einem Virtual Thread abspult.
Damit die Oberfläche jede Phase einheitlich darstellen kann, sind die Phasenergebnisse als versiegelte Schnittstelle modelliert. Jeder Datensatz trägt das Teilergebnis, das die Vorführansicht zeigen soll, und benennt zugleich die Handlung, die zur folgenden Phase fortschreitet.
public sealed interface WalkthroughPhase
permits WalkthroughPhase.Initial,
WalkthroughPhase.ToolCatalog,
WalkthroughPhase.ModelQuery,
WalkthroughPhase.ToolExecution,
WalkthroughPhase.FinalAnswer {
/** Short title of the phase, shown as the card heading. */
String title();
/** Plain-English description of what just happened. */
String description();
/** Label of the button that advances to the next phase. */
String nextActionLabel();
/** True when this phase is the last one; the run is complete. */
default boolean terminal() {
return false;
}
Die versiegelte Schnittstelle benennt abschließend ihre fünf zulässigen Ausprägungen. Jede ist als record ausgeführt und kennt drei Auskünfte: einen Titel für die Kartenüberschrift, eine Beschreibung dessen, was gerade geschehen ist, und die Beschriftung der Schaltfläche, die zur nächsten Phase führt. So trägt jede Phase ihre eigene Darstellungsanweisung bei sich; die Oberfläche muss nicht wissen, in welcher Phase sie sich befindet, sondern fragt den Datensatz nach Titel, Beschreibung und nächster Handlung. Die letzte Phase meldet über terminal, dass der Lauf abgeschlossen ist.
Der Ablauf der Phasen lässt sich als Zustandsautomat lesen. Die ersten beiden Phasen treten je einmal auf; zwischen Modellanfrage und Werkzeugausführung verläuft eine Schleife, die so lange wiederholt wird, bis das Modell keine Werkzeuge mehr verlangt; sodann mündet der Lauf in die abschließende Antwort.

Auf eben diesem Zustandsautomaten ruhen beide Ansichten. Die Hauptansicht durchläuft ihn selbsttätig, ohne Pause; die Vorführansicht hält an jedem Übergang inne und überlässt dem Betrachter den nächsten Schritt. Welcher Antrieb gerade wirkt, ist eine Eigenschaft der Ansicht, nicht des Motors.

Die Zerlegung in fortsetzbare Phasen ist damit weit mehr als eine bauliche Bequemlichkeit. Sie ist die Gewähr dafür, dass die verlangsamte Darstellung der Vorführansicht nicht eine nachgebaute Nachahmung des Ablaufs zeigt, sondern den Ablauf selbst — denselben, den die Hauptansicht in einem Zug abspult. Auf dieser Grundlage kann das folgende Kapitel zeigen, wie die einzelnen Phasen zu einer wachsenden Spur werden.
3. Die Verarbeitungsspur als Zeitachse
Wo die Hauptansicht nur das Endergebnis zeigt, bewahrt die Vorführansicht den gesamten Weg dorthin. Das zentrale visuelle Element, in dem dieser Weg sichtbar wird, ist die Verarbeitungsspur: eine senkrechte Folge von Karten, die mit jeder abgeschlossenen Phase um eine Karte wächst. Die Spur ist damit nicht nur ein Anzeigeelement, sondern das eigentliche Protokoll des Vorgangs — ein Protokoll, das der Betrachter im Nachhinein lesen kann, weil es bestehen bleibt, statt wie der Strom der Hauptansicht zu verfließen.
Der Träger der Spur ist ein schlichter Container, der bei jeder Phase eine Karte aufnimmt. Er entsteht beim Aufbau der Ansicht neben dem Datenpanel.
public WalkthroughView() {
addClassName(“recipe-shell”);
addClassName(“recipe-walkthrough”);
setSizeFull();
Div body = new Div(buildTrack(), dataPanel);
body.addClassName(“recipe-body”);
body.setSizeFull();
add(body);
configureControls();
}
Die linke Hälfte des Körpers bildet die Spur, die rechte das Datenpanel, dem ein eigenes Kapitel gewidmet ist. Innerhalb der Spur sammelt ein trackContainer die Karten. Jede Phase besitzt eine eigene Aufbaumethode, die eine Karte erzeugt und sie an den Container anhängt. Die erste Phase, die Ausgangslage, zeigt das Muster in seiner reinsten Form.
private void renderInitialCard(WalkthroughPhase.Initial phase, Runnable onNext) {
Div card = newCard(null, “recipe-card–initial”);
Span chip = makeChip(“recipe-chip–initial”, VaadinIcon.FLAG_O, phase.title());
Div header = headerRow(chip);
Span subtitle = makeSubtitle(
“Conversation context, ready to be sent. Nothing has hit the model yet.”);
Div result = new Div();
result.addClassName(“recipe-card__result”);
result.add(makeSection(“System prompt”, phase.systemPrompt()));
result.add(makeSection(“User message”, phase.userMessage()));
Div description = makeDescriptionBlock(phase.description());
card.add(header, subtitle, result, description);
trackContainer.add(card);
addNextButton(card, phase.nextActionLabel(), onNext);
}
Jede Karte folgt demselben Aufbau: eine Kopfzeile mit einem zur Phase gehörenden Symbol und dem Titel, eine Unterzeile, ein Ergebnisbereich mit dem jeweiligen Inhalt der Phase und eine Beschreibung dessen, was geschehen ist. Titel, Beschreibung und die Beschriftung der nächsten Handlung stammen dabei unmittelbar aus dem Phasendatensatz — eben jene Auskünfte, die das vorige Kapitel an der versiegelten Schnittstelle gezeigt hat. Die Karte muss daher nicht wissen, welche Phase sie darstellt; sie fragt den Datensatz. Am Ende fügt trackContainer.add die Karte an die Spur an, und addNextButton versieht sie mit einer Schaltfläche, die zur nächsten Phase führt.
Die Reihenfolge der Karten ist nicht beliebig, sondern trägt eine Bedeutung. Untereinander gestapelte Karten bezeichnen eine sequenzielle Abhängigkeit: Die eine Phase folgt aus der vorigen. Zwischen aufeinanderfolgenden Karten wird daher ein rein über CSS gezogener Verbinder dargestellt — eine Linie mit einer Pfeilspitze, die den Fluss auf einen Blick sichtbar macht. Diese Verbinder sind kein Bestandteil des Java-Quelltexts, sondern werden im Stylesheet über die Pseudoelemente ::before und ::after gezeichnet; die Karten selbst bleiben dadurch schlicht.

Die Nummerierung der Phasen spiegelt diese zeitliche Ordnung wider. Die Ausgangslage trägt die Eins, der Werkzeugkatalog die Zwei; die Modellanfragen tragen die Drei mit einer Unternummer je Durchlauf (3.1, 3.2 …), die Werkzeugausführungen die Vier mit derselben Unternummer (4.1, 4.2 …), und die abschließende Zusammenfassung die Fünf. So lässt sich an der Beschriftung jeder Karte ablesen, an welcher Stelle des Laufs sie steht und wie oft die Schleife zwischen Modell und Werkzeugen bereits durchlaufen wurde.
Die letzte Karte fällt aus dem Muster, denn sie zeigt keine weitere Phase des Protokolls, sondern eine Zusammenfassung des Laufs: einige Kennzahlen — die Gesamtdauer, die Zahl der Modelldurchläufe, die Zahl der Werkzeugaufrufe und eine geschätzte Zahl ausgegebener Token — sowie ein kleines Balkendiagramm, das die Dauer der einzelnen Phasen ins Verhältnis setzt. Die eigentliche Antwort wiederholt diese Karte nicht, denn sie wurde bereits in der letzten Modellanfrage-Karte gerendert — ein Umstand, dem das siebte Kapitel nachgeht. Mit der Verarbeitungsspur ist damit die Bühne bereitet; das folgende Kapitel zeigt, wie der Betrachter die einzelnen Karten zum Erscheinen bringt.
Phasen einzeln auslösen
Die Verarbeitungsspur wächst nicht von selbst. Der Betrachter bestimmt das Tempo, indem er jede Phase einzeln auslöst — und eben dieses Interaktionsmodell ist der Kern der Vorführansicht. Das vorige Kapitel zeigte, dass jede Karte am Ende eine Schaltfläche erhält, deren Beschriftung die nächste Phase benennt: „Fetch tool catalogue from MCP“, „Send initial model query“, „Execute the tool“, „Send follow-up model query“. Ein Druck auf diese Schaltfläche ruft die zugehörige Methode des Motors auf, deren Ergebnis als neue Karte erscheint, die ihrerseits die Schaltfläche zur übernächsten Phase trägt. So schreitet der Lauf Schritt für Schritt voran, jeder Schritt eine bewusste Handlung.
Die Frage, was „fortsetzbar“ in einer serverseitigen Oberfläche bedeutet, lässt sich an dieser Stelle genau beantworten. Der Zustand des Laufs ruht zwischen zwei Schritten in einem Feld der Ansicht — dem WalkthroughState, den der Motor bei jedem Aufruf verändert und zurückgibt. Ein Schritt nimmt diesen Zustand auf, führt seine Phase aus und legt das Ergebnis wieder im Zustand ab. Zwischen den Schritten geschieht nichts; die Ansicht verharrt in einem klar erkennbaren, ruhenden Zustand und wartet auf die nächste Handlung des Betrachters.
Da die einzelnen Phasen blockierende Aufrufe an das Modell und an die Werkzeuge enthalten, greift auch hier das aus dem ersten Teil bekannte Aktualisierungsmodell: Die Ausführung erfolgt auf einem Hintergrund-Thread, und das Ergebnis wird über @Push und UI.access in den Komponentenbaum eingeschleust. Zwei Marken sichern dabei den geordneten Ablauf: phaseInFlight zeigt an, dass eine Phase gerade läuft, und autoRunning, ob der Betrachter den automatischen Durchlauf gewählt hat.
Neben dem schrittweisen Fortschreiten bietet die Ansicht nämlich einen zweiten Weg: die Schaltfläche „Run to End“, die die verbleibenden Phasen selbsttätig durchläuft. Während eines solchen automatischen Laufs werden die Schaltflächen der einzelnen Karten unterdrückt; die Karten erscheinen weiterhin, sobald ihre Phase aufgelöst ist, doch zwischen den Phasen wird eine kurze Pause eingefügt, damit der Fortschritt lesbar bleibt. Diese Pause beträgt 350 Millisekunden und wird über einen eigenen Virtual Thread eingelegt.
private void scheduleAutoContinue() {
UI ui = UI.getCurrent();
Thread.startVirtualThread(() -> {
try {
Thread.sleep(AUTO_RUN_PAUSE_MS);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return;
}
try {
ui.access(this::continueAuto);
} catch (UIDetachedException ignored) {
}
});
}
Der Hintergrund-Thread schläft die festgelegte Pause und löst sodann über UI.access den nächsten Schritt aus. Der try-Block um ui.access fängt die UIDetachedException ab — der Fall, dass der Betrachter die Ansicht inzwischen verlassen hat; dann verläuft der automatische Lauf still ins Leere, statt einen Fehler zu werfen. An welcher Stelle der automatische und der schrittweise Antrieb auseinandertreten, zeigt der Abschluss einer Phase. Nach der Werkzeugausführung etwa entscheidet eine einzige Verzweigung, ob es automatisch weitergeht oder ob dem Betrachter die nächste Schaltfläche angeboten wird.
phaseInFlight = false;
if (autoRunning) {
scheduleAutoContinue();
} else {
addNextButton(card, exec.nextActionLabel(), this::advanceToModelQuery);
updateRunToEndAvailability();
}
Ist der automatische Lauf gewählt, so plant die Phase ihre Fortsetzung selbst; andernfalls erhält die Karte eine Schaltfläche, deren Beschriftung wiederum dem Phasendatensatz entstammt und deren Betätigung zur nächsten Phase führt. Der Unterschied zur automatischen Schleife der Hauptansicht ist damit genau benannt: Nicht die Mechanik der Übertragung ändert sich — sie ist in beiden Ansichten dieselbe —, sondern allein der Auslöser. An die Stelle der selbsttätigen Fortsetzung tritt die bewusste Handlung des Betrachters.

Eine „Reset“-Schaltfläche neben dem Eingabefeld räumt die Spur und die erfassten Kennzahlen wieder ab, sodass ein neuer Prompt eingegeben werden kann. Damit verfügt der Betrachter über drei Bedienhandlungen: einen Lauf beginnen, ihn schrittweise oder selbsttätig zu Ende führen und ihn zurücksetzen. Welcher Weg gewählt wird, ändert nichts am gezeigten Ablauf, sondern allein an seinem Tempo — und genau darin liegt der didaktische Wert: Der Betrachter kann denselben Vorgang einmal in Ruhe studieren und einmal im Zusammenhang ablaufen lassen. Das folgende Kapitel wendet sich dem zweiten großen Bestandteil der Ansicht zu, dem Datenpanel.
Das Datenpanel
Neben der Verarbeitungsspur steht ein zweiter Bereich, dem dieses Kapitel gilt: das Datenpanel. Während die Spur das zeitliche Nacheinander der Phasen festhält, zeigt das Datenpanel den jeweils aktuellen Stand der Daten, auf die das Modell zugreift — den Vorrat und das Ernährungsprofil. Das Verhältnis der beiden Bereiche ist das von Verlauf und Zustand: Die Spur beantwortet die Frage, wie es zum gegenwärtigen Stand gekommen ist; das Datenpanel beantwortet die Frage, wie dieser Stand in diesem Augenblick aussieht.
Bemerkenswert ist, womit das Datenpanel gebaut ist. Es ist keine eigene Komponente, sondern dieselbe Seitenleiste, die bereits die Hauptansicht verwendet — hier lediglich unter einem anderen Namen geführt.
private final Sidebar dataPanel = new Sidebar(services.pantry(), services.profile());
Die Wiederverwendung ist kein Zufall, sondern Ausdruck desselben Grundsatzes, der schon den ersten Teil durchzog: Der fachliche Zustand wird an genau einer Stelle gepflegt, und beide Ansichten greifen über dieselben REST-Clients darauf zu. Was im
ersten Teil zum Schreiben und zum Neuladen der Seitenleiste gesagt wurde, gilt hierunverändert. Der einzige Unterschied liegt in der Rolle: In der Hauptansicht ist die Seitenleiste das Werkzeug des Vorführenden; in der Vorführansicht tritt sie zusätzlich als Anschauungsobjekt auf, das zeigt, was das Modell lesen kann.
Im Aufbau der Ansicht erscheint das Datenpanel als rechte Hälfte des Körpers, der Spur gegenübergestellt.
Div body = new Div(buildTrack(), dataPanel);
body.addClassName(“recipe-body”);
body.setSizeFull();
add(body);
Die Untertitelzeile der Ansicht benennt die Aufgabe des Panels ausdrücklich: „The data panel on the right reflects what the model can read; the track below grows one card per step.“ Damit ist das Zusammenspiel der beiden Bereiche auf den Punkt gebracht. Links wächst die Spur Karte um Karte und zeigt den Vorgang; rechts steht das Datenpanel und zeigt den Gegenstand, auf den der Vorgang sich bezieht.

Das Datenpanel ist nicht starr, sondern wird im Verlauf des Laufs aufgefrischt — und zwar an der Stelle, an der das Modell den Zustand tatsächlich verändert haben kann. Nach der Werkzeugausführung, wenn ein zustandsänderndes Werkzeug wie das Verbrauchen einer Zutat oder das Anpassen des Profils gelaufen ist, lädt die Ansicht das Panel neu.
dataPanel.refreshGrid();
dataPanel.refreshProfileForm();
Eben hier zeigt sich der didaktische Gewinn der Gegenüberstellung von Verlauf und Zustand. Der Betrachter sieht in der Spur, dass ein Werkzeug aufgerufen wurde, das den Vorrat verändert; und er sieht im selben Augenblick im Datenpanel, wie sich der Vorrat dadurch geändert hat. Das eine erklärt das andere: Die Werkzeugausführung in der Spur ist die Ursache, die veränderte Anzeige im Datenpanel die Wirkung. Wo die Hauptansicht die Auffrischung erst am Ende eines ganzen Laufs vornimmt, geschieht sie hier nach jeder einzelnen Werkzeugausführung — passend zum verlangsamten Tempo der Vorführung, das jede Wirkung einzeln sichtbar machen will.
So fügen sich Spur und Datenpanel zu zwei Blickwinkeln auf denselben Vorgang. Der eine zeigt die Abfolge der Handlungen, der andere ihren Niederschlag im Zustand. Das folgende Kapitel kehrt zur Spur zurück und betrachtet einen Fall, der eine eigene Darstellung verlangt: die gleichzeitige Ausführung mehrerer Werkzeuge.
Parallele Werkzeugaufrufe darstellen
Verlangt das Modell in einer Antwort mehrere Werkzeuge zugleich, so stellt sich eine doppelte Aufgabe: Die Aufrufe sind nebenläufig auszuführen, und ihre Gleichzeitigkeit ist zugleich als solche darzustellen. Beide Seiten dieser Aufgabe löst die Vorführansicht in einer Weise, die die technische Wirklichkeit des Motors unmittelbar abbildet.
Die Ausführung übernimmt der Motor in seiner vierten Phase. Er öffnet einen Ausführer mit je einem Virtual Thread pro Aufgabe, reicht für jeden Werkzeugaufruf ein CompletableFuture ein, wartet mit allOf auf deren Abschluss und sammelt die Ergebnisse in der ursprünglichen Reihenfolge der Anforderung ein.
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<CompletableFuture<WalkthroughPhase.ToolOutcome>> futures =
new ArrayList<>(calls.size());
for (WalkthroughPhase.PendingToolCall call : calls) {
futures.add(CompletableFuture.supplyAsync(
() -> invokeSafely(call), executor));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
List<WalkthroughPhase.ToolOutcome> outcomes = new ArrayList<>(calls.size());
for (CompletableFuture<WalkthroughPhase.ToolOutcome> f : futures) {
outcomes.add(f.join());
}
Drei Eigenschaften dieser Ausführung verdienen Beachtung. Erstens werden die Aufrufe wirklich gleichzeitig geführt, jeder auf einem eigenen Virtual Thread. Zweitens werden die Ergebnisse in der Reihenfolge der Anforderung eingesammelt, nicht in der Reihenfolge ihres Eintreffens; die Darstellung bleibt dadurch stabil, gleich welcher Aufruf zuerst antwortet. Drittens wirft ein fehlgeschlagener Aufruf keine Ausnahme, sondern wird über invokeSafely als Fehlerergebnis zurückgegeben, sodass das Modell im nächsten Durchlauf darauf reagieren kann.
Der Darstellung dieser Gleichzeitigkeit liegt eine bewusste Entscheidung zugrunde: Mehrere zugleich angeforderte Aufrufe werden nicht als getrennte, untereinander gestapelte Karten gezeigt, sondern als nebeneinanderliegende Spuren innerhalb einer einzigen Werkzeugausführungs-Karte, versehen mit einer Parallelmarkierung, die nennt, wie viele nebenläufige Virtual Threads laufen. Untereinander gestapelte Karten bezeichnen, wie das dritte Kapitel zeigte, eine sequenzielle Abhängigkeit; nebeneinanderliegende Spuren bezeichnen die Gleichzeitigkeit. Die Form der Darstellung trägt damit selbst eine Aussage über die Nebenläufigkeit.
Während ein Aufruf noch läuft, zeigt seine Spur den Namen des Werkzeugs, die Argumente und einen Hinweis auf das ausstehende Ergebnis.
private Div buildLaneRunning(WalkthroughPhase.PendingToolCall c) {
Div lane = new Div();
lane.addClassName(“recipe-walkthrough__lane”);
lane.addClassName(“recipe-walkthrough__lane–running”);
Span name = new Span(c.name() + “(…)”);
name.addClassName(“recipe-walkthrough__tool-name”);
Pre args = new Pre(prettyJson(c.argumentsJson()));
args.addClassName(“recipe-walkthrough__json”);
Span pending = new Span(“Awaiting MCP response…”);
pending.addClassName(“recipe-walkthrough__pending”);
lane.add(name, args, pending);
return lane;
}
Ist die Ausführung abgeschlossen, so ersetzt die Karte die laufenden Spuren durch die aufgelösten und frischt zugleich das Datenpanel auf.
private void finishToolExecutionCard(Div card, Div description,
Div result, Div lanes,
Div progressWrap,
WalkthroughPhase.ToolExecution exec) {
result.remove(progressWrap);
card.removeClassName(“running”);
lanes.removeAll();
for (WalkthroughPhase.ToolOutcome oc : exec.outcomes()) {
lanes.add(buildLaneResolved(oc));
}
description.removeAll();
description.add(makeDescriptionLabel(“Description”));
description.add(new Div(new Span(exec.description())));
dataPanel.refreshGrid();
dataPanel.refreshProfileForm();
Jede aufgelöste Spur zeigt den Namen des Werkzeugs, die Argumente und das sauber formatierte Ergebnis-JSON; ein fehlerhafter Aufruf färbt seine Spur rot. Bemerkenswert ist, dass die Auffrischung des Datenpanels genau hier geschieht — nach der Ausführung, wenn die Werkzeuge den Zustand verändert haben könnten —, womit sich der Bogen zum vorigen Kapitel schließt.
Die Aussage der Darstellung — sequenziell untereinander, parallel nebeneinander — entspricht der Wirklichkeit des Motors. Die Beschreibung des Phasendatensatzes bringt dies auf den Punkt: „The MCP server executed all N tools concurrently (parallel virtual threads). Each lane shows arguments and result independently.“

So macht die Vorführansicht die Nebenläufigkeit zugleich wirksam und sichtbar. Sie führt die Aufrufe wirklich gleichzeitig, und sie zeigt sie in einer Form, die diese Gleichzeitigkeit nicht verschleiert, sondern ausstellt — ohne dabei die zeitliche Einordnung der Aufrufe in den Gesamtablauf zu verlieren, denn alle Spuren wohnen in ein und derselben Karte an ihrer Stelle in der Spur.

Das folgende Kapitel kehrt zu einem Detail zurück, das in der Modellanfrage-Karte wohnt und den Bogen zum ersten Teil schließt: dem Streaming des Textes und seiner abschließenden Darstellung als Markdown innerhalb einer einzelnen Karte.
Streaming und Markdown in der Phasenkarte
Das fünfte Kapitel des ersten Teils endete mit einem Vorausverweis. Dort wurde gezeigt, dass die Hauptansicht den Strom unmittelbar als Markdown rendert, und zugleich angekündigt, dass die Schritt-für-Schritt-Ansicht den umgekehrten Weg geht: erst Rohtext, dann gerendert. Diesen Gegenentwurf löst das vorliegende Kapitel ein. Er wohnt in der Modellanfrage-Karte, jener Karte, die einen einzelnen Aufruf an das Modell darstellt.
Während der Strom läuft, wächst der Text in einem schlichten Bereich an. Die Karte wird beim Aufbau mit einem solchen streamDiv, einer unbestimmten Fortschrittsleiste und — als Besonderheit der Vorführansicht — einer Darstellung der gesendeten Anfrage versehen. Sodann läuft auf einem Virtual Thread der Aufruf des Motors, dessen Rückruf jedes Fragment an den Rohtextbereich anhängt.
StringBuilder buffer = new StringBuilder();
Div header = headerRow(chip);
Div description = makeDescription();
Div result = new Div(requestSection, replyLabel, progressWrap, streamDiv);
result.addClassName(“recipe-card__result”);
card.add(header, subtitle, result, description);
trackContainer.add(card);
java.time.Instant t0 = java.time.Instant.now();
int iterationLabel = nextIter;
Thread.startVirtualThread(() -> {
try {
WalkthroughPhase.ModelQuery query = engine.runModelQuery(state,
chunk -> ui.access(() -> {
buffer.append(chunk);
streamDiv.setText(buffer.toString());
}));
Der Rückruf hängt jedes Fragment an den Puffer an und setzt dessen Inhalt als reinen Text auf den streamDiv. Es ist setText, nicht das Setzen von Markdown-Inhalt: Während des Stroms erscheint die Antwort bewusst als Rohtext. Damit ist die im ersten Teil angekündigte Zweistufigkeit in ihrer ersten Stufe sichtbar. Bemerkenswert ist zudem die Darstellung der Anfrage, die der Karte vorausgeht: Sie zeigt genau die Botschaften, die an das Modell gesandt wurden — System- und Benutzernachricht sowie, in späteren Durchläufen, die vorangegangenen Antworten und Werkzeugergebnisse —, sodass der Betrachter genau erkennt, was das Modell empfangen hat.
Die zweite Stufe tritt ein, sobald der Strom abgeschlossen ist. Dann entscheidet die Karte anhand der angeforderten Werkzeuge, wie sie sich abschließend darstellt.
if (query.toolCalls().isEmpty()) {
result.remove(streamDiv);
Markdown rendered = new Markdown(streamedText.isBlank() ? “_(no text)_” : streamedText);
rendered.addClassName(“recipe-walkthrough__rendered”);
result.add(rendered);
phaseInFlight = false;
// The streamed text is already rendered as Markdown here, so the
// dedicated “final answer” card would be redundant. Skip straight
// into the run-summary card with timing + token stats.
if (autoRunning) {
scheduleAutoContinue();
} else {
scheduleAutoSummaryAdvance();
}
} else {
if (streamedText.isBlank()) {
streamDiv.setText("(no text emitted; the model went straight to tool calls)");
streamDiv.addClassName(“recipe-walkthrough__stream–empty”);
}
Verlangt das Modell keine Werkzeuge, so handelt es sich um die endgültige Antwort. Die Karte entfernt den Rohtextbereich und ersetzt ihn durch eine frisch erzeugte Markdown-Komponente — die zweite Stufe der angekündigten Zweistufigkeit. Eben weil die endgültige Antwort hier bereits gerendert ist, wäre eine eigene Karte für die „endgültige Antwort“ überflüssig; der Kommentar im Quelltext sagt es ausdrücklich. Die Ansicht schreitet daher unmittelbar zur Zusammenfassungs-Karte fort, die das dritte Kapitel als letzte Karte der Spur beschrieben hat. Verlangt das Modell hingegen Werkzeuge, so bleibt der Rohtext stehen — gegebenenfalls mit einem Hinweis, falls das Modell unmittelbar zu den Werkzeugaufrufen übergegangen ist —, und darunter erscheint eine Aufstellung der angeforderten Aufrufe.
Der Grund für diese andere Wahl gegenüber der Hauptansicht liegt, wie schon im ersten Teil dargelegt, im anderen Zweck. Die lehrende Ansicht bevorzugt ein ruhiges, stabiles Bild jeder Phase. Während des Stroms zeigt sie den Rohtext, der ruhig anwächst, ohne dass sich unvollständige Auszeichnungen unter dem Blick des Betrachters umordnen; erst nach Abschluss tritt an seine Stelle das gerenderte Dokument. Die Phasenbeschreibung bestätigt es: „Streaming is complete and the text is rendered as Markdown.“
flowchart TD
A[“card created: requestSection + streamDiv + progress bar”] –> B[“stream runs:buffer.append(chunk)streamDiv.setText(buffer) — raw text”]
B –> C{“stream complete:tool calls requested?”}
C –>|“no”| D[“remove streamDivadd Markdown(streamedText) — rendered”]
C –>|“yes”| E[“keep raw textappend ‘Requested N tool calls’ panel”]
D –> F[“advance to run summary”]
E –> G[“advance to tool execution”]
Damit schließt sich der Bogen, den der erste Teil gespannt hat. Die beiden Ansichten beantworten dieselbe Gestaltungsfrage auf zwei begründete Weisen: Die Hauptansicht rendert fortlaufend, um die Lebendigkeit des Entstehens zu zeigen; die Vorführansicht zeigt erst Rohtext und rendert nach Abschluss, um die Ruhe des Studierens zu wahren. Beide Wege folgen demselben Grundsatz, dass eine Oberfläche den Zustand ihres Inhalts ehrlich abbilden sollte. Das folgende Kapitel kehrt zu einer Karte zurück, die der Lauf früh durchschritten hat und die einer näheren Betrachtung lohnt: der Werkzeugkatalog-Karte.
Die Werkzeugkatalog-Karte
Der Lauf durchschreitet zu Beginn eine Karte, die das vorangegangene Erzählen rasch hinter sich gelassen hat: die Werkzeugkatalog-Karte, die zweite Phase. Sie verdient eine nähere Betrachtung, denn sie ist mehr als eine bloße Liste. Sie macht sichtbar, welche Werkzeuge der angebundene Server dem Modell überhaupt anbietet, und sie tut dies in einer Form, die das bloße Aufzählen übersteigt: mit anklickbaren Abzeichen und vollständigen JSON-Deskriptoren, zwischen denen ein sanftes Scrollen vermittelt.
Die Karte führt zwei Bereiche: eine Reihe von Abzeichen und eine Folge von Einträgen. Jeder Eintrag stellt ein Werkzeug ausführlich dar — mit Namen, Beschreibung und dem vollständigen JSON-Deskriptor —, während die Abzeichen als knappe, anklickbare Sprungmarken dienen. Der Aufbau der Einträge geht aus dem Werkzeug-Array hervor, das der Motor in der zweiten Phase beschafft hat.
Map<String, Div> entryByName = new java.util.LinkedHashMap<>();
for (JsonNode tool : phase.toolsArray()) {
String toolName = tool.path(“function”).path(“name”).asText();
if (toolName == null || toolName.isBlank()) continue;
Div entry = new Div();
entry.addClassName(“recipe-walkthrough__tool-entry”);
entry.getElement().setAttribute(“data-tool-name”, toolName);
Span entryTitle = new Span(new Icon(VaadinIcon.CODE), new Span(toolName));
entryTitle.addClassName(“recipe-walkthrough__tool-entry-title”);
String description = tool.path(“function”).path(“description”).asText("");
if (!description.isBlank()) {
Span entryDesc = new Span(description);
entryDesc.addClassName(“recipe-walkthrough__tool-entry-desc”);
entry.add(entryTitle, entryDesc);
} else {
entry.add(entryTitle);
}
Pre json = new Pre(prettyJson(tool.toString()));
json.addClassName(“recipe-walkthrough__json”);
entry.add(json);
entries.add(entry);
entryByName.put(toolName, entry);
}
Zweierlei verdient Beachtung. Erstens werden Name und Beschreibung unmittelbar aus dem JSON-Knoten des Werkzeugs gelesen — über path und asText —, sodass die Karte genau das zeigt, was der Server tatsächlich anbietet, nicht eine gepflegte Zweitfassung. Zweitens hält eine LinkedHashMap die Zuordnung von Werkzeugnamen zu Eintrags-Div fest und bewahrt dabei die Reihenfolge; eben diese Zuordnung ermöglicht es den Abzeichen, gezielt zu ihrem Eintrag zu springen. Der vollständige Deskriptor wird als nichtproportional gesetzter Pre-Block beigefügt.
Die Abzeichen entstehen in einem zweiten Durchlauf, diesmal über die Namensliste der Phase. Findet sich zu einem Namen ein Eintrag, so wird das Abzeichen anklickbar gemacht und mit den nötigen Merkmalen für Bedienbarkeit und Barrierefreiheit versehen.
for (String name : phase.toolNames()) {
Span tag = new Span(name);
tag.addClassName(“recipe-walkthrough__tool-tag”);
Div target = entryByName.get(name);
if (target != null) {
tag.addClassName(“recipe-walkthrough__tool-tag–clickable”);
tag.getElement().setAttribute(“title”,
“Jump to " + name + " descriptor”);
tag.getElement().setAttribute(“role”, “button”);
tag.getElement().setAttribute(“tabindex”, “0”);
tag.getElement().addEventListener(“click”,
ev -> scrollToToolEntry(target));
tag.getElement().addEventListener(“keydown”,
ev -> scrollToToolEntry(target))
.setFilter(“event.key === ‘Enter’ || event.key === ’ ‘”);
}
tags.add(tag);
}
Hier zeigt sich, wie viel sich mit den Bordmitteln von Vaadin Flow am Element selbst regeln lässt. Das Abzeichen erhält über setAttribute die Rolle einer Schaltfläche, einen Tooltip und einen Tabulatorindex, sodass es nicht nur mit der Maus, sondern auch mit der Tastatur erreichbar ist. An das Element werden zwei Lauscher gehängt — einer für den Mausklick, einer für die Tastatur —, wobei der Tastaturlauscher über setFilter auf die Eingabe- und die Leertaste eingeschränkt wird. Dieser Filter ist bemerkenswert: Er ist ein clientseitiger Ausdruck, der schon im Browser entscheidet, ob das Ereignis überhaupt an den Server gemeldet wird; unnötige Serverrunden unterbleiben so.
Das eigentliche Springen besorgt eine kleine Methode, die clientseitiges Verhalten über executeJs anstößt.
private void scrollToToolEntry(Div target) {
target.getElement().executeJs(
“this.scrollIntoView({behavior:‘smooth’, block:‘start’});”
+ “this.classList.add(‘recipe-walkthrough__tool-entry–flash’);”
+ “setTimeout(() => “
+ “this.classList.remove(‘recipe-walkthrough__tool-entry–flash’),”
+ " 1500);”);
}
Der übergebene JavaScript-Ausdruck lässt den Zieleintrag sanft in den sichtbaren Bereich rollen und fügt ihm sodann für anderthalb Sekunden eine Stilklasse hinzu, die ihn kurz aufleuchten lässt, bevor sie wieder entfernt wird. So findet der Betrachter nach einem Klick auf ein Abzeichen den zugehörigen Deskriptor nicht nur wieder, sondern sein Blick wird durch das Aufleuchten unmittelbar dorthin geführt. Dass this innerhalb des Ausdrucks auf das Zielelement verweist, ist die Brücke zwischen der serverseitigen Komponente und ihrer clientseitigen Entsprechung.
So fügt sich die Katalogkarte aus drei einfachen Mitteln: dem Lesen des realen Werkzeug-Arrays, der Verknüpfung von Abzeichen und Eintrag über eine geordnete Zuordnung und einem schmalen Stück clientseitigen Verhaltens für das Springen und Aufleuchten. Eine Diagrammbibliothek oder ein zusätzliches Rahmenwerk ist dafür nicht nötig; das Element und seine Methoden genügen.

Das folgende Kapitel wendet sich der letzten Karte des Laufs zu, die ebenfalls von Hand gefügt ist und kein zugekauftes Diagramm verwendet: der Zusammenfassungskarte.
Die Zusammenfassungskarte
Die letzte Karte des Laufs zeigt keine weitere Phase des Protokolls, sondern eine Bilanz: einige Kennzahlen und ein Balkendiagramm, das die Dauer der einzelnen Phasen ins Verhältnis setzt. Das dritte Kapitel hat sie bereits als Abschluss der Spur erwähnt; hier sei sie ausführlich betrachtet, denn sie ist reine, von Hand gefügte Vaadin-Komposition — ohne Diagrammbibliothek, allein aus elementaren Komponenten und proportional gesetzten Breiten.
Die Karte selbst ist schlicht aufgebaut. Sie reiht eine Reihe von Kennzahlenkacheln und ein Balkendiagramm aneinander; die eigentliche Antwort wiederholt sie nicht, da diese bereits in der letzten Modellanfrage-Karte gerendert wurde.
private void renderFinalAnswerCard(WalkthroughPhase.FinalAnswer phase) {
Div card = newCard(null, “recipe-card–final”);
Span chip = makeChip(“recipe-chip–final”, VaadinIcon.CHART_LINE,
“5. Run summary”);
Div header = headerRow(chip);
Span subtitle = makeSubtitle(
“Timing and approximate token usage per phase. Token counts are “
+ “estimated as streamed-chars / 4.”);
Div result = new Div();
result.addClassName(“recipe-card__result”);
result.add(buildSummaryTiles(phase));
result.add(buildPhaseChart());
Div description = makeDescriptionBlock(phase.description());
card.add(header, subtitle, result, description);
trackContainer.add(card);
}
Die Kennzahlen entstehen aus den Statistikwerten, die während des Laufs Phase für Phase erfasst wurden. Jede abgeschlossene Phase hat einen PhaseStat-Datensatz hinterlassen — mit Bezeichnung, Farbklasse, Dauer und einer Größenangabe samt Einheit, etwa der Zahl der Werkzeuge oder der Zeichenzahl des Stroms. Aus diesen Werten summiert die Karte die Gesamtdauer, die Zahl der Werkzeugaufrufe und die Gesamtzahl der gestreamten Zeichen.
private Div buildSummaryTiles(WalkthroughPhase.FinalAnswer phase) {
long totalChars = 0;
long totalDurationMs = 0;
int toolCalls = 0;
for (PhaseStat s : phaseStats) {
totalDurationMs += s.duration().toMillis();
if (“chars”.equals(s.sizeLabel())) totalChars += s.sizeValue();
if (“tool”.equals(s.sizeLabel()) || “tools”.equals(s.sizeLabel())) {
toolCalls += (int) s.sizeValue();
}
}
long wallClockMs = runStartedAt == null ? totalDurationMs
: java.time.Duration.between(runStartedAt, java.time.Instant.now()).toMillis();
long approxTokens = totalChars / 4;
Div tiles = new Div();
tiles.addClassName(“recipe-walkthrough__summary-tiles”);
tiles.add(buildTile(VaadinIcon.CLOCK,
formatMillis(wallClockMs), “Total time”));
tiles.add(buildTile(VaadinIcon.REFRESH,
Integer.toString(phase.iterations()), “Model iterations”));
tiles.add(buildTile(VaadinIcon.COGS,
Integer.toString(toolCalls), “Tool calls”));
tiles.add(buildTile(VaadinIcon.FILE_TEXT_O,
“≈ " + approxTokens, “Output tokens (~chars/4)”));
return tiles;
}
Zwei Feinheiten verdienen Beachtung. Die Gesamtdauer wird, wenn der Startzeitpunkt bekannt ist, als tatsächlich verstrichene Wanduhrzeit zwischen Beginn und Gegenwart gemessen, nicht als bloße Summe der Phasendauern; Pausen zwischen den Schritten gehen so in die ausgewiesene Gesamtzeit ein. Und die Zahl der ausgegebenen Token wird nicht gemessen, sondern aus der Zeichenzahl geschätzt — als Zeichenzahl geteilt durch vier —, weshalb die Kachel das Ungefähr-Zeichen trägt. Es ist eine bewusst grobe Näherung, die dem Betrachter eine Größenordnung gibt, ohne mehr zu versprechen, als sie halten kann.
Jede einzelne Kachel ist ein kleiner, immer gleicher Baustein aus Symbol, Wert und Beschriftung.
private Div buildTile(VaadinIcon icon, String value, String label) {
Div tile = new Div();
tile.addClassName(“recipe-walkthrough__tile”);
Icon i = new Icon(icon);
i.addClassName(“recipe-walkthrough__tile-icon”);
Span valueSpan = new Span(value);
valueSpan.addClassName(“recipe-walkthrough__tile-value”);
Span labelSpan = new Span(label);
labelSpan.addClassName(“recipe-walkthrough__tile-label”);
tile.add(i, valueSpan, labelSpan);
return tile;
}
Das Balkendiagramm ist der lehrreichste Teil der Karte, denn es zeigt, wie sich ein brauchbares Diagramm ohne jede Diagrammbibliothek allein mit den Mitteln der Plattform zeichnen lässt. Zunächst wird der größte Dauerwert ermittelt, der als Bezugsgröße für die Skalierung dient; sodann erhält jede Phase eine Zeile.
private Div buildPhaseChart() {
Div chart = new Div();
chart.addClassName(“recipe-walkthrough__chart”);
Span chartTitle = makeSectionLabel(VaadinIcon.BAR_CHART_H,
“Per-phase duration”);
chart.add(chartTitle);
long maxMs = 1;
for (PhaseStat s : phaseStats) {
maxMs = Math.max(maxMs, s.duration().toMillis());
}
for (PhaseStat s : phaseStats) {
chart.add(buildChartRow(s, maxMs));
}
return chart;
}
Der Kern liegt in der einzelnen Zeile. Aus der Dauer der Phase und dem zuvor ermittelten Größtwert wird ein Prozentwert berechnet, der unmittelbar als Breite des Balkens gesetzt wird. Eine Untergrenze von zwei Prozent sorgt dafür, dass auch sehr kurze Phasen als sichtbarer Balken erscheinen und nicht ganz verschwinden.
private Div buildChartRow(PhaseStat s, long maxMs) {
Div row = new Div();
row.addClassName(“recipe-walkthrough__chart-row”);
Span label = new Span(s.label());
label.addClassName(“recipe-walkthrough__chart-label”);
long ms = s.duration().toMillis();
double pct = Math.max(2.0, (ms * 100.0) / maxMs);
Div barTrack = new Div();
barTrack.addClassName(“recipe-walkthrough__chart-track”);
Div bar = new Div();
bar.addClassName(“recipe-walkthrough__chart-bar”);
bar.addClassName(“recipe-walkthrough__chart-bar–” + s.colorClass());
bar.getStyle().set(“width”, String.format(java.util.Locale.ROOT,
“%.1f%%", pct));
barTrack.add(bar);
Span duration = new Span(formatMillis(ms));
duration.addClassName(“recipe-walkthrough__chart-duration”);
Span size = new Span(s.sizeValue() + " " + s.sizeLabel());
size.addClassName(“recipe-walkthrough__chart-size”);
row.add(label, barTrack, duration, size);
return row;
}
Die Breite des Balkens wird über getStyle().set("width", …) als Prozentangabe unmittelbar am Element gesetzt; die Farbe entstammt einer Stilklasse, deren Name aus der Farbklasse der Phase gebildet wird, sodass die Balken farblich zu den Abzeichen der Phasen passen. Neben dem Balken zeigt die Zeile die Dauer und die Größenangabe. Die Dauer wird dabei lesbar formatiert: unterhalb einer Sekunde in Millisekunden, darüber in Sekunden mit zwei Nachkommastellen.
private static String formatMillis(long ms) {
if (ms < 1000) return ms + " ms”;
return String.format(java.util.Locale.ROOT, “%.2f s”, ms / 1000.0);
}
So entsteht aus wenigen Bausteinen ein vollständiges kleines Diagramm: eine Bezugsgröße, je Phase eine Zeile, ein in der Breite proportional gesetzter Balken und eine ablesbare Beschriftung. Bemerkenswert ist, was hier alles nicht nötig war — keine Diagrammbibliothek, kein Zeichnen auf einer Leinwand, kein zusätzliches Rahmenwerk. Ein Div, dem man eine prozentuale Breite gibt, ist bereits ein Balken; mehr braucht es nicht.

Damit sind auch die beiden Karten betrachtet, die der Lauf rasch durchschritten oder zuletzt erreicht hat. Das folgende und letzte Kapitel zieht die Summe und führt beide Teile der Reihe zusammen.
Schluss
Der erste Teil dieser Reihe zeigte, dass der angebundene Server einen Unterschied bewirkt; der zweite hat gezeigt, wie dieser Unterschied zustande kommt. Damit ist der Bogen geschlossen, den die Einleitung des ersten Teils gespannt hat — der Übergang vom Was zum Wie. Die Schritt-für-Schritt-Ansicht hält an, was die Hauptansicht in einem Zug abspult, und macht aus dem flüchtigen Ablauf einen lesbaren Gegenstand.
Die tragende Einsicht dieses Teils war architektonischer Natur: Beide Ansichten ruhen auf demselben Motor. Die Protokolllogik lebt an einer einzigen Stelle, im WalkthroughEngine, dessen fünf Phasen als einzeln aufrufbare Methoden ausgeführt sind. Eben diese Zerlegung in fortsetzbare Phasen ist die Gewähr dafür, dass die verlangsamte Vorführung nicht eine Nachahmung, sondern denselben Ablauf zeigt, den die Hauptansicht selbsttätig durchläuft. Auf dieser Grundlage standen die übrigen Kapitel: die Verarbeitungsspur, die den Vorgang Karte um Karte als Zeitachse bewahrt; das einzelne Auslösen der Phasen, das dem Betrachter das Tempo überlässt; das Datenpanel, das den Zustand neben den Verlauf stellt; die Darstellung paralleler Werkzeugaufrufe, deren Form selbst die Nebenläufigkeit ausstellt; das Streaming mit abschließendem Markdown-Rendern in der Phasenkarte, das den im ersten Teil angekündigten Gegenentwurf einlöste; die Werkzeugkatalog-Karte, die den dem Modell angebotenen Werkzeugen anklickbare Abzeichen und vollständige JSON-Deskriptoren zuordnet; und schließlich die Zusammenfassungskarte, die den Lauf in Kennzahlen und einem von Hand gezeichneten Balkendiagramm beschließt.
So verschieden die beiden Ansichten in ihrer Wirkung sind, verbindet sie derselbe Grundsatz, der bereits den ersten Teil durchzog. Eine Oberfläche für modellgesteuerte Werkzeugnutzung hat nicht nur Ergebnisse anzuzeigen, sondern den Vorgang ihrer Entstehung nachvollziehbar zu halten. Die Hauptansicht erfüllt diesen Grundsatz durch Gegenüberstellung und Gleichzeitigkeit; die Vorführansicht erfüllt ihn durch Anhalten und Zergliederung. Beide sind unterschiedliche Antworten auf dieselbe Forderung, und gerade ihre Verschiedenheit zeigt, dass die Forderung mehr als eine Umsetzung zulässt.

Es bleibt der Rückblick auf die Reihe als Ganzes. Sie begann mit einem MCP-Server, der mit den Bordmitteln des JDK gebaut war, ohne Spring und ohne Jakarta EE. Sie setzte sich fort mit einer Oberfläche, die denselben Verzicht auf schwergewichtige Rahmenwerke wahrt und sich auf Vaadin Flow mit eingebettetem Jetty beschränkt. Und sie schloss mit zwei Ansichten auf denselben Vorgang, die zeigen, dass sich der Nutzen eines angebundenen Servers nicht behaupten, sondern vorführen lässt — einmal als Ergebnis, einmal als Prozess. Wer die Reihe von Anfang bis Ende verfolgt hat, verfügt damit nicht nur über einen lauffähigen Demonstrator, sondern über ein Verständnis dafür, weshalb eine sorgfältig geführte Oberfläche für die Vermittlung modellgesteuerter Werkzeugnutzung unentbehrlich ist.




