Die Vaadin-Oberfläche des Rezeptassistenten — Teil 2: Die Schritt-für-Schritt-Ansicht

Sven Ruppert

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:

https://8g8.eu/vlskmz

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.

Total
0
Shares
Previous Post

Immer Up to Date: Erhalte Jederzeit die Neue Kostenlose PDF-Ausgabe!

Next Post

Vaadin 25.2 und der Stellenwert sicherer Voreinstellungen

Related Posts

Die Zukunft von Containern – Was kommt als Nächstes?

Vielleicht haben Sie schon die Schlagworte gehört, die in aller Munde sind, wenn es um die Zukunft von Containern geht. Seltsame Namen wie "Micro-VMs"… "Unikernel"… "Sandboxes"… Haben Sie sich gefragt, was diese Dinge sind und wie Sie sie nutzen können? Oder sollten Sie diese überhaupt verwenden?
Read More