<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>JSentinel on Sven Ruppert</title><link>https://svenruppert.com/tags/jsentinel/</link><description>Sven Ruppert — Java Veteran, Speaker, Trainer &amp; Bushcrafter. Articles, talks, workshops and videos on Core Java, Cybersecurity, Vaadin and Developer Relations.</description><generator>Hugo</generator><language>en</language><managingEditor>sven.ruppert@gmail.com (Sven Ruppert)</managingEditor><webMaster>sven.ruppert@gmail.com (Sven Ruppert)</webMaster><copyright>© 2026 Sven Ruppert</copyright><atom:link href="https://svenruppert.com/tags/jsentinel/index.xml" rel="self" type="application/rss+xml"/><image><url>https://svenruppert.com/img/sven-ruppert.jpg</url><title>Sven Ruppert</title><link>https://svenruppert.com/tags/jsentinel/</link></image><lastBuildDate>Fri, 26 Jun 2026 13:00:00 +0200</lastBuildDate><item><title>The Vaadin UI of the Recipe Assistant — Part 2: The Step-by-Step View</title><link>https://svenruppert.com/posts/recipe-assistant-vaadin-ui-part-2-the-step-by-step-view/</link><pubDate>Fri, 26 Jun 2026 13:00:00 +0200</pubDate><author>sven.ruppert@gmail.com (Sven Ruppert)</author><dc:creator>Sven Ruppert</dc:creator><guid isPermaLink="true">https://svenruppert.com/posts/recipe-assistant-vaadin-ui-part-2-the-step-by-step-view/</guid><description>Halting the automatic MCP pipeline into individually triggerable phases. Two views, one engine — the step-by-step walkthrough turns the fleeting course into a legible, temporally ordered object that the viewer can walk through at their own pace.</description><content:encoded>&lt;![CDATA[<h2 id="1-introduction-from-the-result-to-the-process">1. Introduction: From the Result to the Process</h2><p>The first part of this series dealt with the main view — the juxtaposition of two
answers as a didactic stage. It answers a yes-or-no question, and it answers it
convincingly: the attached server makes a visible difference. Yet it is silent
about how that difference comes about. Which tools the model chooses and in what
order, with which arguments it calls them, how their results flow back into the
ongoing dialogue, and at which point several runs finally become a coherent
recommendation — all of this runs hidden in automatic operation. The running
timeline of the main view hints at the traffic but does not halt it.</p><p>It is precisely here that the present second part begins. It deals with the
step-by-step view, a view of its own under the route<code>mcp-walkthrough</code> with the
title &ldquo;MCP Server — Step by Step&rdquo;. Where the main view sets the result of two
requests side by side and thereby conceals the mechanism, the step-by-step view
reverses the relationship. It halts the pipeline, makes each phase individually
triggerable, and turns the otherwise fleeting course into a legible, temporally
ordered object. The didactic gain lies in slowing down: what the main view reels
off in one go, the walkthrough view decomposes into comprehensible steps that the
viewer can walk through at his own pace.</p><p>The tour of this part therefore no longer follows the constituents of an interface,
but the hurdles that halting and making visible an inherently automatic process
brings with it. The first and load-bearing chapter treats the architectural
precondition: the decomposition of the automatic tool loop into individual,
resumable phases that both views share. There follow the processing track as a
timeline, the individual triggering of the phases, the data panel beside the track,
the presentation of parallel tool calls, and the streaming and rendering within a
single phase card. A concluding chapter brings both parts of the series together.</p><p>As in the first part, general familiarity with Vaadin is assumed; particular
elements are presented in detail as to their effect. The technical basis matches
that of the first part: JDK 26, Vaadin Flow 25.1.6 with embedded Jetty 12.1.10 and
Jackson for JSON processing, without Spring and without Jakarta EE. Both views rest
on the same protocol logic and differ solely in their drive and in their
presentation — a fact that the following chapter takes as its point of departure.</p><h2 id="2-from-the-automatic-loop-to-the-halted-pipeline">2. From the Automatic Loop to the Halted Pipeline</h2><p>The step-by-step view and the main view show the same process. This is not an
assertion but an architectural fact, and it is the precondition for everything else
this part treats. The main view drives the tool loop automatically: it calls the
model, executes the requested tools, adds their results to the dialogue and calls
the model again, until no further tools are requested. The walkthrough view must
halt precisely this loop and decompose it into individually triggerable steps —
without the protocol logic being duplicated. Were it duplicated, the two views
might show two different courses; yet the didactic value of the walkthrough rests
on its showing the same course, merely slowed down.</p><p>The solution lies in a dedicated bearer of the protocol logic, the<code>WalkthroughEngine</code>. Each of its methods runs exactly one phase, mutates the
supplied state and returns the result as a phase record that the interface can
render.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="cm">/** Phase 1. Build a fresh state seeded with the system + user message. */</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="kd">public</span><span class="w"/><span class="n">WalkthroughState</span><span class="w"/><span class="nf">startRun</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">systemPrompt</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">userMessage</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">WalkthroughState</span><span class="p">(</span><span class="n">systemPrompt</span><span class="p">,</span><span class="w"/><span class="n">userMessage</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="cm">/** Phase 2. Fetch the MCP tool catalogue and attach it to the state. */</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="kd">public</span><span class="w"/><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ToolCatalog</span><span class="w"/><span class="nf">fetchToolCatalogue</span><span class="p">(</span><span class="n">WalkthroughState</span><span class="w"/><span class="n">state</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ArrayNode</span><span class="w"/><span class="n">tools</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">toolBridge</span><span class="p">.</span><span class="na">toolsForOllama</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">state</span><span class="p">.</span><span class="na">setToolsArray</span><span class="p">(</span><span class="n">tools</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">List</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="w"/><span class="n">names</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ArrayList</span><span class="o">&lt;&gt;</span><span class="p">(</span><span class="n">tools</span><span class="p">.</span><span class="na">size</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">JsonNode</span><span class="w"/><span class="n">tool</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">tools</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">name</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">tool</span><span class="p">.</span><span class="na">path</span><span class="p">(</span><span class="s">"function"</span><span class="p">).</span><span class="na">path</span><span class="p">(</span><span class="s">"name"</span><span class="p">).</span><span class="na">asText</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">name</span><span class="w"/><span class="o">!=</span><span class="w"/><span class="kc">null</span><span class="p">)</span><span class="w"/><span class="n">names</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">name</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ToolCatalog</span><span class="w"/><span class="n">phase</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ToolCatalog</span><span class="p">(</span><span class="n">names</span><span class="p">,</span><span class="w"/><span class="n">tools</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">state</span><span class="p">.</span><span class="na">setCurrentPhase</span><span class="p">(</span><span class="n">phase</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">phase</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>The decisive thought stands in the engine&rsquo;s class comment: the protocol logic
resides in a single place, and only the drive differs. The class comment puts it
thus: &ldquo;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.&rdquo; The five phases are realised as
methods —<code>startRun</code>,<code>fetchToolCatalogue</code>,<code>runModelQuery</code>,<code>executeTools</code> and<code>finalise</code> — and each is callable on its own. It is precisely because each phase is
a method that the walkthrough view can call them individually on a button press,
while the main view reels them off in a single loop on one virtual thread.</p><p>So that the interface can render each phase uniformly, the phase results are
modelled as a sealed interface. Each record carries the partial result the
walkthrough view is to show and at the same time names the action that advances to
the following phase.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"/><span class="kd">sealed</span><span class="w"/><span class="kd">interface</span><span class="nc">WalkthroughPhase</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">permits</span><span class="w"/><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">Initial</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ToolCatalog</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ModelQuery</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ToolExecution</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">FinalAnswer</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="cm">/** Short title of the phase, shown as the card heading. */</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="nf">title</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="cm">/** Plain-English description of what just happened. */</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="nf">description</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="cm">/** Label of the button that advances to the next phase. */</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="nf">nextActionLabel</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="cm">/** True when this phase is the last one; the run is complete. */</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">default</span><span class="w"/><span class="kt">boolean</span><span class="w"/><span class="nf">terminal</span><span class="p">()</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="kc">false</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>The sealed interface finally names its five permitted forms. Each is realised as a<code>record</code> and knows three pieces of information: a title for the card heading, a
description of what has just happened, and the label of the button that leads to
the next phase. Each phase thus carries its own rendering instruction with it; the
interface need not know which phase it is in, but asks the record for title,
description and next action. The last phase reports through<code>terminal</code> that the run
is complete.</p><p>The course of the phases can be read as a state machine. The first two phases occur
once each; between the model query and the tool execution runs a loop that is
repeated until the model requests no further tools; then the run issues into the
final answer.</p><figure><img src="/images/2026/06/recipe-assistant-part-2-diagram-01.png" alt="State machine of the five walkthrough phases" loading="lazy" decoding="async"/><p>It is on this very state machine that both views rest. The main view traverses it
of its own accord, without pause; the walkthrough view pauses at each transition
and leaves the next step to the viewer. Which drive is at work is a property of the
view, not of the engine.</p><figure><img src="/images/2026/06/recipe-assistant-part-2-ch02.png" alt="One engine, two drivers: the comparison and walkthrough views share the same protocol logic; only the drive differs" loading="lazy" decoding="async"/><p>The decomposition into resumable phases is thus far more than a structural
convenience. It is the guarantee that the slowed-down presentation of the
walkthrough view does not show a reconstructed imitation of the course, but the
course itself — the same one that the main view reels off in a single go. On this
foundation the following chapter can show how the individual phases become a
growing track.</p><h2 id="3-the-processing-track-as-a-timeline">3. The Processing Track as a Timeline</h2><p>Where the main view shows only the final result, the walkthrough view preserves the
entire path to it. The central visual element in which this path becomes visible is
the processing track: a vertical sequence of cards that grows by one card with every
completed phase. The track is thus not merely a display element but the actual
record of the process — a record the viewer can read in retrospect, because it
persists rather than flowing away like the stream of the main view.</p><p>The bearer of the track is a plain container that takes up a card for each phase. It
arises when the view is assembled, beside the data panel.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"/><span class="nf">WalkthroughView</span><span class="p">()</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">addClassName</span><span class="p">(</span><span class="s">"recipe-shell"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">setSizeFull</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">body</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">(</span><span class="n">buildTrack</span><span class="p">(),</span><span class="w"/><span class="n">dataPanel</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">body</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-body"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">body</span><span class="p">.</span><span class="na">setSizeFull</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">add</span><span class="p">(</span><span class="n">body</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">configureControls</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>The left half of the body forms the track, the right half the data panel, to which
a chapter of its own is devoted. Within the track a<code>trackContainer</code> collects the
cards. Each phase has its own build method that creates a card and appends it to the
container. The first phase, the initial situation, shows the pattern in its purest
form.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">renderInitialCard</span><span class="p">(</span><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">Initial</span><span class="w"/><span class="n">phase</span><span class="p">,</span><span class="w"/><span class="n">Runnable</span><span class="w"/><span class="n">onNext</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">card</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">newCard</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span><span class="w"/><span class="s">"recipe-card--initial"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">chip</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">makeChip</span><span class="p">(</span><span class="s">"recipe-chip--initial"</span><span class="p">,</span><span class="w"/><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">FLAG_O</span><span class="p">,</span><span class="w"/><span class="n">phase</span><span class="p">.</span><span class="na">title</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">header</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">headerRow</span><span class="p">(</span><span class="n">chip</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">subtitle</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">makeSubtitle</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"Conversation context, ready to be sent. Nothing has hit the model yet."</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">result</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">result</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-card__result"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">result</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">makeSection</span><span class="p">(</span><span class="s">"System prompt"</span><span class="p">,</span><span class="w"/><span class="n">phase</span><span class="p">.</span><span class="na">systemPrompt</span><span class="p">()));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">result</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">makeSection</span><span class="p">(</span><span class="s">"User message"</span><span class="p">,</span><span class="w"/><span class="n">phase</span><span class="p">.</span><span class="na">userMessage</span><span class="p">()));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">description</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">makeDescriptionBlock</span><span class="p">(</span><span class="n">phase</span><span class="p">.</span><span class="na">description</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">card</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">header</span><span class="p">,</span><span class="w"/><span class="n">subtitle</span><span class="p">,</span><span class="w"/><span class="n">result</span><span class="p">,</span><span class="w"/><span class="n">description</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">trackContainer</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">card</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">addNextButton</span><span class="p">(</span><span class="n">card</span><span class="p">,</span><span class="w"/><span class="n">phase</span><span class="p">.</span><span class="na">nextActionLabel</span><span class="p">(),</span><span class="w"/><span class="n">onNext</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>Every card follows the same construction: a header row with an icon belonging to the
phase and the title, a subtitle, a result area with the respective content of the
phase, and a description of what has happened. Title, description and the label of
the next action come directly from the phase record — those very pieces of
information that the previous chapter showed on the sealed interface. The card
therefore need not know which phase it renders; it asks the record. Finally,<code>trackContainer.add</code> appends the card to the track, and<code>addNextButton</code> furnishes it
with a button that leads to the next phase.</p><p>The order of the cards is not arbitrary but carries meaning. Cards stacked one above
another denote a sequential dependency: one phase follows from the previous. Between
consecutive cards a connector drawn purely via CSS is therefore shown — a line with
an arrowhead that makes the flow visible at a glance. These connectors are not part
of the Java source but are drawn in the stylesheet through the pseudo-elements<code>::before</code> and<code>::after</code>; the cards themselves thereby remain plain.</p><figure><img src="/images/2026/06/recipe-assistant-part-2-diagram-02.png" alt="The processing track as a numbered sequence of phase cards" loading="lazy" decoding="async"/><p>The numbering of the phases reflects this temporal order. The initial situation
bears the one, the tool catalogue the two; the model queries bear the three with a
sub-number per run (3.1, 3.2 …), the tool executions the four with the same
sub-number (4.1, 4.2 …), and the concluding summary the five. Thus from the label of
each card one can read at which point of the run it stands and how often the loop
between model and tools has already been traversed.</p><p>The last card falls outside the pattern, for it shows no further phase of the
protocol but a summary of the run: a few figures — the total time, the number of
model runs, the number of tool calls and an estimated number of output tokens — and
a small bar chart that sets the duration of the individual phases in proportion. The
actual answer is not repeated by this card, for it was already rendered in the last
model-query card — a circumstance the seventh chapter pursues. With the processing
track the stage is thus set; the following chapter shows how the viewer brings the
individual cards into being.</p><h2 id="4-triggering-phases-individually">4. Triggering Phases Individually</h2><p>The processing track does not grow of its own accord. The viewer sets the pace by
triggering each phase individually — and this very interaction model is the heart of
the walkthrough view. The previous chapter showed that each card receives, at its
end, a button whose label names the next phase: &ldquo;Fetch tool catalogue from MCP&rdquo;,
&ldquo;Send initial model query&rdquo;, &ldquo;Execute the tool&rdquo;, &ldquo;Send follow-up model query&rdquo;. A
press on this button calls the corresponding method of the engine, whose result
appears as a new card, which in turn carries the button to the phase after next.
Thus the run proceeds step by step, each step a deliberate action.</p><p>The question of what &ldquo;resumable&rdquo; means in a server-side interface can be answered
precisely here. The state of the run rests, between two steps, in a field of the
view — the<code>WalkthroughState</code>, which the engine mutates and returns on every call. A
step takes up this state, runs its phase and deposits the result back into the
state. Between the steps nothing happens; the view remains in a clearly recognisable,
resting state and waits for the viewer&rsquo;s next action.</p><p>Since the individual phases contain blocking calls to the model and to the tools,
the update model familiar from the first part applies here too: execution takes
place on a background thread, and the result is channelled into the component tree
by way of<code>@Push</code> and<code>UI.access</code>. Two flags secure the orderly course:<code>phaseInFlight</code> indicates that a phase is currently running, and<code>autoRunning</code>
whether the viewer has chosen the automatic run.</p><p>For besides stepping forward, the view offers a second way: the &ldquo;Run to End&rdquo; button,
which traverses the remaining phases of its own accord. During such an automatic run
the buttons of the individual cards are suppressed; the cards still appear as soon as
their phase is resolved, but a brief pause is inserted between the phases so that the
progression remains legible. This pause amounts to 350 milliseconds and is inserted
by way of a dedicated virtual thread.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">scheduleAutoContinue</span><span class="p">()</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">UI</span><span class="w"/><span class="n">ui</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">UI</span><span class="p">.</span><span class="na">getCurrent</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Thread</span><span class="p">.</span><span class="na">startVirtualThread</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Thread</span><span class="p">.</span><span class="na">sleep</span><span class="p">(</span><span class="n">AUTO_RUN_PAUSE_MS</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">InterruptedException</span><span class="w"/><span class="n">ie</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Thread</span><span class="p">.</span><span class="na">currentThread</span><span class="p">().</span><span class="na">interrupt</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(</span><span class="k">this</span><span class="p">::</span><span class="n">continueAuto</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">UIDetachedException</span><span class="w"/><span class="n">ignored</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">});</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>The background thread sleeps the fixed pause and then triggers the next step by way
of<code>UI.access</code>. The<code>try</code> block around<code>ui.access</code> catches the<code>UIDetachedException</code>
— the case in which the viewer has meanwhile left the view; the automatic run then
runs quietly into the void instead of throwing an error. Where the automatic and the
step-by-step drives part ways is shown by the conclusion of a phase. After the tool
execution, for instance, a single branch decides whether things continue
automatically or whether the next button is offered to the viewer.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="n">phaseInFlight</span><span class="w"/><span class="o">=</span><span class="w"/><span class="kc">false</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">autoRunning</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">scheduleAutoContinue</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">else</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">addNextButton</span><span class="p">(</span><span class="n">card</span><span class="p">,</span><span class="w"/><span class="n">exec</span><span class="p">.</span><span class="na">nextActionLabel</span><span class="p">(),</span><span class="w"/><span class="k">this</span><span class="p">::</span><span class="n">advanceToModelQuery</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">updateRunToEndAvailability</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>If the automatic run is chosen, the phase schedules its own continuation; otherwise
the card receives a button whose label again stems from the phase record and whose
activation leads to the next phase. The difference from the automatic loop of the
main view is thus named precisely: it is not the mechanics of the transmission that
change — they are the same in both views — but solely the trigger. In place of the
self-acting continuation steps the viewer&rsquo;s deliberate action.</p><figure><img src="/images/2026/06/recipe-assistant-part-2-diagram-03.png" alt="Auto-run vs step-by-step — when each card resolves" loading="lazy" decoding="async"/><p>A &ldquo;Reset&rdquo; button next to the input field clears the track and the recorded figures
again, so that a new prompt can be entered. The viewer thus has three operating
actions at his disposal: to begin a run, to carry it through step by step or of its
own accord, and to reset it. Which way is chosen changes nothing about the course
shown, only its pace — and precisely therein lies the didactic value: the viewer can
study the same process once at leisure and once let it run in context. The following
chapter turns to the second major constituent of the view, the data panel.</p><h2 id="5-the-data-panel">5. The Data Panel</h2><p>Beside the processing track stands a second area to which this chapter is devoted:
the data panel. While the track records the temporal succession of the phases, the
data panel shows the current state of the data the model accesses — the pantry and
the dietary profile. The relationship of the two areas is that of course and state:
the track answers the question of how the present state came about; the data panel
answers the question of what this state looks like at this moment.</p><p>Notable is what the data panel is built with. It is not a component of its own but
the same sidebar that the main view already uses — here merely carried under a
different name.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">Sidebar</span><span class="w"/><span class="n">dataPanel</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Sidebar</span><span class="p">(</span><span class="n">services</span><span class="p">.</span><span class="na">pantry</span><span class="p">(),</span><span class="w"/><span class="n">services</span><span class="p">.</span><span class="na">profile</span><span class="p">());</span></span></span></code></pre></div></div><p>The reuse is no accident but the expression of the same principle that ran through
the first part: the business state is maintained in exactly one place, and both
views access it through the same REST clients. What was said in the first part about
the writing and reloading of the sidebar holds here unchanged. The only difference
lies in the role: in the main view the sidebar is the operator&rsquo;s instrument; in the
walkthrough view it additionally appears as an object of illustration showing what
the model can read.</p><p>In the assembly of the view the data panel appears as the right half of the body,
set opposite the track.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">Div</span><span class="w"/><span class="n">body</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">(</span><span class="n">buildTrack</span><span class="p">(),</span><span class="w"/><span class="n">dataPanel</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">body</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-body"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">body</span><span class="p">.</span><span class="na">setSizeFull</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">add</span><span class="p">(</span><span class="n">body</span><span class="p">);</span></span></span></code></pre></div></div><p>The subtitle line of the view names the panel&rsquo;s task expressly: &ldquo;The data panel on
the right reflects what the model can read; the track below grows one card per
step.&rdquo; With that, the interplay of the two areas is brought to the point. On the
left the track grows card by card and shows the process; on the right stands the
data panel and shows the object to which the process refers.</p><figure><img src="/images/2026/06/recipe-assistant-part-2-ch05.png" alt="Layout of the view: on the left the growing processing track, on the right the data panel with pantry and dietary profile" loading="lazy" decoding="async"/><p>The data panel is not rigid but is refreshed in the course of the run — and indeed
at the point at which the model may actually have changed the state. After the tool
execution, when a state-mutating tool such as consuming an ingredient or adjusting
the profile has run, the view reloads the panel.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="n">dataPanel</span><span class="p">.</span><span class="na">refreshGrid</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">dataPanel</span><span class="p">.</span><span class="na">refreshProfileForm</span><span class="p">();</span></span></span></code></pre></div></div><p>It is here that the didactic gain of juxtaposing course and state shows itself. The
viewer sees in the track that a tool was called which changes the pantry; and he
sees, at the same moment, in the data panel how the pantry has thereby changed. The
one explains the other: the tool execution in the track is the cause, the changed
display in the data panel the effect. Where the main view performs the refresh only
at the end of a whole run, here it happens after each individual tool execution —
fitting to the slowed-down pace of the walkthrough, which wishes to make each effect
visible on its own.</p><p>Thus track and data panel come together as two perspectives on the same process.
The one shows the sequence of actions, the other their precipitation in the state.
The following chapter returns to the track and considers a case that demands a
presentation of its own: the simultaneous execution of several tools.</p><h2 id="6-presenting-parallel-tool-calls">6. Presenting Parallel Tool Calls</h2><p>If the model requests several tools at once in a single reply, a twofold task
arises: the calls are to be executed concurrently, and their simultaneity is at the
same time to be presented as such. The walkthrough view solves both sides of this
task in a way that directly reflects the technical reality of the engine.</p><p>The execution is taken on by the engine in its fourth phase. It opens an executor
with one virtual thread per task, submits a<code>CompletableFuture</code> for each tool call,
waits with<code>allOf</code> for their completion, and collects the results in the original
order of the request.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="k">try</span><span class="w"/><span class="p">(</span><span class="n">ExecutorService</span><span class="w"/><span class="n">executor</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">Executors</span><span class="p">.</span><span class="na">newVirtualThreadPerTaskExecutor</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">List</span><span class="o">&lt;</span><span class="n">CompletableFuture</span><span class="o">&lt;</span><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ToolOutcome</span><span class="o">&gt;&gt;</span><span class="w"/><span class="n">futures</span><span class="w"/><span class="o">=</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ArrayList</span><span class="o">&lt;&gt;</span><span class="p">(</span><span class="n">calls</span><span class="p">.</span><span class="na">size</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">PendingToolCall</span><span class="w"/><span class="n">call</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">calls</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">futures</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">CompletableFuture</span><span class="p">.</span><span class="na">supplyAsync</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">invokeSafely</span><span class="p">(</span><span class="n">call</span><span class="p">),</span><span class="w"/><span class="n">executor</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">CompletableFuture</span><span class="p">.</span><span class="na">allOf</span><span class="p">(</span><span class="n">futures</span><span class="p">.</span><span class="na">toArray</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">CompletableFuture</span><span class="o">[</span><span class="n">0</span><span class="o">]</span><span class="p">)).</span><span class="na">join</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">List</span><span class="o">&lt;</span><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ToolOutcome</span><span class="o">&gt;</span><span class="w"/><span class="n">outcomes</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ArrayList</span><span class="o">&lt;&gt;</span><span class="p">(</span><span class="n">calls</span><span class="p">.</span><span class="na">size</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">CompletableFuture</span><span class="o">&lt;</span><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ToolOutcome</span><span class="o">&gt;</span><span class="w"/><span class="n">f</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">futures</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">outcomes</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">f</span><span class="p">.</span><span class="na">join</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>Three properties of this execution deserve attention. First, the calls are genuinely
conducted simultaneously, each on a virtual thread of its own. Second, the results
are collected in the order of the request, not in the order of their arrival; the
presentation thereby remains stable, whichever call answers first. Third, a failed
call throws no exception but is returned through<code>invokeSafely</code> as an error outcome,
so that the model can react to it on the next run.</p><p>The presentation of this simultaneity rests on a deliberate decision: several calls
requested at once are not shown as separate, vertically stacked cards, but as
side-by-side lanes within a single tool-execution card, furnished with a parallel
marker that names how many concurrent virtual threads are running. Vertically
stacked cards denote, as the third chapter showed, a sequential dependency;
side-by-side lanes denote simultaneity. The form of the presentation thus itself
carries a statement about the concurrency.</p><p>While a call is still running, its lane shows the name of the tool, the arguments and
a hint at the pending result.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="n">Div</span><span class="w"/><span class="nf">buildLaneRunning</span><span class="p">(</span><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">PendingToolCall</span><span class="w"/><span class="n">c</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">lane</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">lane</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__lane"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">lane</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__lane--running"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">name</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="n">c</span><span class="p">.</span><span class="na">name</span><span class="p">()</span><span class="w"/><span class="o">+</span><span class="w"/><span class="s">"(...)"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">name</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__tool-name"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Pre</span><span class="w"/><span class="n">args</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Pre</span><span class="p">(</span><span class="n">prettyJson</span><span class="p">(</span><span class="n">c</span><span class="p">.</span><span class="na">argumentsJson</span><span class="p">()));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">args</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__json"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">pending</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="s">"Awaiting MCP response..."</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">pending</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__pending"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">lane</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">name</span><span class="p">,</span><span class="w"/><span class="n">args</span><span class="p">,</span><span class="w"/><span class="n">pending</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">lane</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>Once the execution is complete, the card replaces the running lanes with the
resolved ones and at the same time refreshes the data panel.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">finishToolExecutionCard</span><span class="p">(</span><span class="n">Div</span><span class="w"/><span class="n">card</span><span class="p">,</span><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">description</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">result</span><span class="p">,</span><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">lanes</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">progressWrap</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ToolExecution</span><span class="w"/><span class="n">exec</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">result</span><span class="p">.</span><span class="na">remove</span><span class="p">(</span><span class="n">progressWrap</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">card</span><span class="p">.</span><span class="na">removeClassName</span><span class="p">(</span><span class="s">"running"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">lanes</span><span class="p">.</span><span class="na">removeAll</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ToolOutcome</span><span class="w"/><span class="n">oc</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">exec</span><span class="p">.</span><span class="na">outcomes</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">lanes</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">buildLaneResolved</span><span class="p">(</span><span class="n">oc</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">description</span><span class="p">.</span><span class="na">removeAll</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">description</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">makeDescriptionLabel</span><span class="p">(</span><span class="s">"Description"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">description</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="n">exec</span><span class="p">.</span><span class="na">description</span><span class="p">())));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">dataPanel</span><span class="p">.</span><span class="na">refreshGrid</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">dataPanel</span><span class="p">.</span><span class="na">refreshProfileForm</span><span class="p">();</span></span></span></code></pre></div></div><p>Each resolved lane shows the name of the tool, the arguments and the neatly
formatted result JSON; an erroneous call paints its lane red. Notably, the refresh
of the data panel happens precisely here — after the execution, when the tools may
have changed the state — which closes the arc to the previous chapter.</p><p>The statement of the presentation — sequential below one another, parallel beside
one another — corresponds to the reality of the engine. The description of the phase
record brings this to the point: &ldquo;The MCP server executed all N tools concurrently
(parallel virtual threads). Each lane shows arguments and result independently.&rdquo;</p><figure><img src="/images/2026/06/recipe-assistant-part-2-diagram-04.png" alt="Parallel tool execution — one card with side-by-side lanes" loading="lazy" decoding="async"/><p>Thus the walkthrough view makes the concurrency both effective and visible. It
conducts the calls genuinely simultaneously, and it shows them in a form that does
not veil this simultaneity but displays it — without thereby losing the temporal
placement of the calls within the overall course, for all lanes dwell in one and the
same card at its place in the track.</p><figure><img src="/images/2026/06/recipe-assistant-part-2-ch06.png" alt="Parallel tool execution: one card, side-by-side lanes under a parallel marker" loading="lazy" decoding="async"/><p>The following chapter returns to a detail that dwells in the model-query card and
closes the arc to the first part: the streaming of the text and its final
presentation as Markdown within a single card.</p><h2 id="7-streaming-and-markdown-in-the-phase-card">7. Streaming and Markdown in the Phase Card</h2><p>The fifth chapter of the first part ended with a forward reference. There it was
shown that the main view renders the stream directly as Markdown, and at the same
time it was announced that the step-by-step view takes the opposite path: raw text
first, then rendered. This counter-design the present chapter makes good on. It
dwells in the model-query card, that card which represents a single call to the
model.</p><p>While the stream runs, the text grows in a plain area. On assembly the card is
furnished with such a<code>streamDiv</code>, an indeterminate progress bar and — as a
particularity of the walkthrough view — a presentation of the request that was sent.
Then, on a virtual thread, the call to the engine runs, whose callback appends each
fragment to the raw-text area.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">StringBuilder</span><span class="w"/><span class="n">buffer</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">StringBuilder</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">Div</span><span class="w"/><span class="n">header</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">headerRow</span><span class="p">(</span><span class="n">chip</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">Div</span><span class="w"/><span class="n">description</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">makeDescription</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">Div</span><span class="w"/><span class="n">result</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">(</span><span class="n">requestSection</span><span class="p">,</span><span class="w"/><span class="n">replyLabel</span><span class="p">,</span><span class="w"/><span class="n">progressWrap</span><span class="p">,</span><span class="w"/><span class="n">streamDiv</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">result</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-card__result"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">card</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">header</span><span class="p">,</span><span class="w"/><span class="n">subtitle</span><span class="p">,</span><span class="w"/><span class="n">result</span><span class="p">,</span><span class="w"/><span class="n">description</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">trackContainer</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">card</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">java</span><span class="p">.</span><span class="na">time</span><span class="p">.</span><span class="na">Instant</span><span class="w"/><span class="n">t0</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">java</span><span class="p">.</span><span class="na">time</span><span class="p">.</span><span class="na">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="kt">int</span><span class="w"/><span class="n">iterationLabel</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">nextIter</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">Thread</span><span class="p">.</span><span class="na">startVirtualThread</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ModelQuery</span><span class="w"/><span class="n">query</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">engine</span><span class="p">.</span><span class="na">runModelQuery</span><span class="p">(</span><span class="n">state</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">chunk</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">buffer</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">chunk</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">streamDiv</span><span class="p">.</span><span class="na">setText</span><span class="p">(</span><span class="n">buffer</span><span class="p">.</span><span class="na">toString</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}));</span></span></span></code></pre></div></div><p>The callback appends each fragment to the buffer and sets its content as plain text
on the<code>streamDiv</code>. It is<code>setText</code>, not the setting of Markdown content: during the
stream the answer deliberately appears as raw text. With that, the two-stage approach
announced in the first part is visible in its first stage. Notable, too, is the
presentation of the request that precedes the card: it shows exactly the messages
that were sent to the model — system and user message and, in later runs, the
preceding replies and tool results — so that the viewer recognises precisely what the
model received.</p><p>The second stage sets in as soon as the stream is complete. The card then decides,
on the basis of the requested tools, how it presents itself finally.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">query</span><span class="p">.</span><span class="na">toolCalls</span><span class="p">().</span><span class="na">isEmpty</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">result</span><span class="p">.</span><span class="na">remove</span><span class="p">(</span><span class="n">streamDiv</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Markdown</span><span class="w"/><span class="n">rendered</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Markdown</span><span class="p">(</span><span class="n">streamedText</span><span class="p">.</span><span class="na">isBlank</span><span class="p">()</span><span class="w"/><span class="o">?</span><span class="w"/><span class="s">"_(no text)_"</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">streamedText</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">rendered</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__rendered"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">result</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">rendered</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">phaseInFlight</span><span class="w"/><span class="o">=</span><span class="w"/><span class="kc">false</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// The streamed text is already rendered as Markdown here, so the</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// dedicated "final answer" card would be redundant. Skip straight</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// into the run-summary card with timing + token stats.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">autoRunning</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">scheduleAutoContinue</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">else</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">scheduleAutoSummaryAdvance</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w"/><span class="k">else</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">streamedText</span><span class="p">.</span><span class="na">isBlank</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">streamDiv</span><span class="p">.</span><span class="na">setText</span><span class="p">(</span><span class="s">"(no text emitted; the model went straight to tool calls)"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">streamDiv</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__stream--empty"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>If the model requests no tools, this is the final answer. The card removes the
raw-text area and replaces it with a freshly created<code>Markdown</code> component — the second
stage of the announced two-stage approach. It is precisely because the final answer
is already rendered here that a dedicated card for the &ldquo;final answer&rdquo; would be
superfluous; the comment in the source says so expressly. The view therefore
proceeds directly to the summary card that the third chapter described as the last
card of the track. If, on the other hand, the model requests tools, the raw text
remains — with a hint, where applicable, should the model have gone straight to the
tool calls — and beneath it appears a listing of the requested calls.</p><p>The reason for this different choice from the main view lies, as already set out in
the first part, in the different purpose. The teaching view prefers a calm, stable
picture of each phase. During the stream it shows the raw text, which grows calmly
without incomplete markup rearranging itself beneath the viewer&rsquo;s gaze; only after
completion does the rendered document take its place. The phase description confirms
it: &ldquo;Streaming is complete and the text is rendered as Markdown.&rdquo;</p><figure><img src="/images/2026/06/recipe-assistant-part-2-diagram-05.png" alt="Streaming and rendering inside the model-query card" loading="lazy" decoding="async"/><p>With that, the arc that the first part spanned is closed. The two views answer the
same design question in two well-founded ways: the main view renders progressively,
to show the vividness of the emergence; the walkthrough view shows raw text first and
renders after completion, to preserve the calm of study. Both ways follow the same
principle, that an interface should honestly reflect the state of its content. The
following chapter returns to a card that the run passed through early and that
rewards a closer look: the tool-catalogue card.</p><h2 id="8-the-tool-catalogue-card">8. The Tool-Catalogue Card</h2><p>At its beginning the run passes through a card that the preceding narrative left
behind quickly: the tool-catalogue card, the second phase. It deserves a closer look,
for it is more than a mere list. It makes visible which tools the attached server
offers the model at all, and it does so in a form that surpasses mere enumeration:
with clickable badges and full JSON descriptors, between which a smooth scroll
mediates.</p><p>The card carries two areas: a row of badges and a sequence of entries. Each entry
presents a tool in detail — with name, description and the full JSON descriptor —
while the badges serve as concise, clickable jump marks. The construction of the
entries arises from the tool array that the engine procured in the second phase.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">java</span><span class="p">.</span><span class="na">util</span><span class="p">.</span><span class="na">Map</span><span class="o">&lt;</span><span class="n">String</span><span class="p">,</span><span class="w"/><span class="n">Div</span><span class="o">&gt;</span><span class="w"/><span class="n">entryByName</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">java</span><span class="p">.</span><span class="na">util</span><span class="p">.</span><span class="na">LinkedHashMap</span><span class="o">&lt;&gt;</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">JsonNode</span><span class="w"/><span class="n">tool</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">phase</span><span class="p">.</span><span class="na">toolsArray</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">toolName</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">tool</span><span class="p">.</span><span class="na">path</span><span class="p">(</span><span class="s">"function"</span><span class="p">).</span><span class="na">path</span><span class="p">(</span><span class="s">"name"</span><span class="p">).</span><span class="na">asText</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">toolName</span><span class="w"/><span class="o">==</span><span class="w"/><span class="kc">null</span><span class="w"/><span class="o">||</span><span class="w"/><span class="n">toolName</span><span class="p">.</span><span class="na">isBlank</span><span class="p">())</span><span class="w"/><span class="k">continue</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">entry</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">entry</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__tool-entry"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">entry</span><span class="p">.</span><span class="na">getElement</span><span class="p">().</span><span class="na">setAttribute</span><span class="p">(</span><span class="s">"data-tool-name"</span><span class="p">,</span><span class="w"/><span class="n">toolName</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">entryTitle</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">Icon</span><span class="p">(</span><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">CODE</span><span class="p">),</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="n">toolName</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">entryTitle</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__tool-entry-title"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">description</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">tool</span><span class="p">.</span><span class="na">path</span><span class="p">(</span><span class="s">"function"</span><span class="p">).</span><span class="na">path</span><span class="p">(</span><span class="s">"description"</span><span class="p">).</span><span class="na">asText</span><span class="p">(</span><span class="s">""</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="o">!</span><span class="n">description</span><span class="p">.</span><span class="na">isBlank</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">entryDesc</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="n">description</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">entryDesc</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__tool-entry-desc"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">entry</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">entryTitle</span><span class="p">,</span><span class="w"/><span class="n">entryDesc</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">else</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">entry</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">entryTitle</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Pre</span><span class="w"/><span class="n">json</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Pre</span><span class="p">(</span><span class="n">prettyJson</span><span class="p">(</span><span class="n">tool</span><span class="p">.</span><span class="na">toString</span><span class="p">()));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">json</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__json"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">entry</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">json</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">entries</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">entry</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">entryByName</span><span class="p">.</span><span class="na">put</span><span class="p">(</span><span class="n">toolName</span><span class="p">,</span><span class="w"/><span class="n">entry</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>Two things deserve attention. First, name and description are read directly from the
tool&rsquo;s JSON node — via<code>path</code> and<code>asText</code> — so that the card shows exactly what the
server actually offers, not a maintained second version. Second, a<code>LinkedHashMap</code>
holds the mapping of tool names to entry<code>Div</code> and thereby preserves the order; it is
this very mapping that allows the badges to jump to their entry. The full descriptor
is attached as a monospaced<code>Pre</code> block.</p><p>The badges arise in a second pass, this time over the phase&rsquo;s list of names. If an
entry is found for a name, the badge is made clickable and furnished with the
features necessary for operability and accessibility.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">name</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">phase</span><span class="p">.</span><span class="na">toolNames</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">tag</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="n">name</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">tag</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__tool-tag"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">target</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">entryByName</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="n">name</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">target</span><span class="w"/><span class="o">!=</span><span class="w"/><span class="kc">null</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">tag</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__tool-tag--clickable"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">tag</span><span class="p">.</span><span class="na">getElement</span><span class="p">().</span><span class="na">setAttribute</span><span class="p">(</span><span class="s">"title"</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"Jump to "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">name</span><span class="w"/><span class="o">+</span><span class="w"/><span class="s">" descriptor"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">tag</span><span class="p">.</span><span class="na">getElement</span><span class="p">().</span><span class="na">setAttribute</span><span class="p">(</span><span class="s">"role"</span><span class="p">,</span><span class="w"/><span class="s">"button"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">tag</span><span class="p">.</span><span class="na">getElement</span><span class="p">().</span><span class="na">setAttribute</span><span class="p">(</span><span class="s">"tabindex"</span><span class="p">,</span><span class="w"/><span class="s">"0"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">tag</span><span class="p">.</span><span class="na">getElement</span><span class="p">().</span><span class="na">addEventListener</span><span class="p">(</span><span class="s">"click"</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ev</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">scrollToToolEntry</span><span class="p">(</span><span class="n">target</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">tag</span><span class="p">.</span><span class="na">getElement</span><span class="p">().</span><span class="na">addEventListener</span><span class="p">(</span><span class="s">"keydown"</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ev</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">scrollToToolEntry</span><span class="p">(</span><span class="n">target</span><span class="p">))</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">setFilter</span><span class="p">(</span><span class="s">"event.key === 'Enter' || event.key === ' '"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">tags</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">tag</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>Here it shows how much can be regulated on the element itself with the platform&rsquo;s own
means in Vaadin Flow. The badge receives, through<code>setAttribute</code>, the role of a
button, a tooltip and a tab index, so that it is reachable not only with the mouse but
also with the keyboard. Two listeners are attached to the element — one for the mouse
click, one for the keyboard — whereby the keyboard listener is restricted through<code>setFilter</code> to the Enter and the space key. This filter is notable: it is a
client-side expression that decides already in the browser whether the event is
reported to the server at all; needless server round trips are thereby avoided.</p><p>The actual jumping is taken on by a small method that triggers client-side behaviour
through<code>executeJs</code>.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">scrollToToolEntry</span><span class="p">(</span><span class="n">Div</span><span class="w"/><span class="n">target</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">target</span><span class="p">.</span><span class="na">getElement</span><span class="p">().</span><span class="na">executeJs</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"this.scrollIntoView({behavior:'smooth', block:'start'});"</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">+</span><span class="w"/><span class="s">"this.classList.add('recipe-walkthrough__tool-entry--flash');"</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">+</span><span class="w"/><span class="s">"setTimeout(() =&gt; "</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">+</span><span class="w"/><span class="s">"this.classList.remove('recipe-walkthrough__tool-entry--flash'),"</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">+</span><span class="w"/><span class="s">" 1500);"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>The passed JavaScript expression lets the target entry scroll smoothly into the
visible area and then adds to it, for a second and a half, a style class that makes it
flash briefly before it is removed again. Thus, after a click on a badge, the viewer
not only finds the corresponding descriptor again, but his gaze is led directly to it
by the flash. That<code>this</code> within the expression refers to the target element is the
bridge between the server-side component and its client-side counterpart.</p><p>The catalogue card thus comes together from three simple means: the reading of the
real tool array, the linking of badge and entry through an ordered mapping, and a slim
piece of client-side behaviour for the jumping and flashing. A charting library or an
additional framework is not needed for this; the element and its methods suffice.</p><figure><img src="/images/2026/06/recipe-assistant-part-2-ch08.png" alt="The tool-catalogue card: a row of clickable badges above a list of tool entries with description and JSON descriptor" loading="lazy" decoding="async"/><p>The following chapter turns to the last card of the run, which is likewise built by
hand and uses no bought-in chart: the summary card.</p><h2 id="9-the-summary-card">9. The Summary Card</h2><p>The last card of the run shows no further phase of the protocol but a balance: a few
figures and a bar chart that sets the duration of the individual phases in proportion.
The third chapter already mentioned it as the conclusion of the track; here let it be
considered in detail, for it is pure, hand-assembled Vaadin composition — without a
charting library, solely from elementary components and proportionally set widths.</p><p>The card itself is plainly built. It strings together a row of stat tiles and a bar
chart; the actual answer it does not repeat, since that was already rendered in the
last model-query card.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">renderFinalAnswerCard</span><span class="p">(</span><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">FinalAnswer</span><span class="w"/><span class="n">phase</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">card</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">newCard</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span><span class="w"/><span class="s">"recipe-card--final"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">chip</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">makeChip</span><span class="p">(</span><span class="s">"recipe-chip--final"</span><span class="p">,</span><span class="w"/><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">CHART_LINE</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"5. Run summary"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">header</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">headerRow</span><span class="p">(</span><span class="n">chip</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">subtitle</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">makeSubtitle</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"Timing and approximate token usage per phase. Token counts are "</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">+</span><span class="w"/><span class="s">"estimated as streamed-chars / 4."</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">result</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">result</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-card__result"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">result</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">buildSummaryTiles</span><span class="p">(</span><span class="n">phase</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">result</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">buildPhaseChart</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">description</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">makeDescriptionBlock</span><span class="p">(</span><span class="n">phase</span><span class="p">.</span><span class="na">description</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">card</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">header</span><span class="p">,</span><span class="w"/><span class="n">subtitle</span><span class="p">,</span><span class="w"/><span class="n">result</span><span class="p">,</span><span class="w"/><span class="n">description</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">trackContainer</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">card</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>The figures arise from the statistics captured phase by phase during the run. Each
completed phase has left behind a<code>PhaseStat</code> record — with label, colour class,
duration and a size figure together with a unit, for instance the number of tools or
the character count of the stream. From these values the card sums the total duration,
the number of tool calls and the total of streamed characters.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="n">Div</span><span class="w"/><span class="nf">buildSummaryTiles</span><span class="p">(</span><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">FinalAnswer</span><span class="w"/><span class="n">phase</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kt">long</span><span class="w"/><span class="n">totalChars</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">0</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kt">long</span><span class="w"/><span class="n">totalDurationMs</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">0</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kt">int</span><span class="w"/><span class="n">toolCalls</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">0</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">PhaseStat</span><span class="w"/><span class="n">s</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">phaseStats</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">totalDurationMs</span><span class="w"/><span class="o">+=</span><span class="w"/><span class="n">s</span><span class="p">.</span><span class="na">duration</span><span class="p">().</span><span class="na">toMillis</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="s">"chars"</span><span class="p">.</span><span class="na">equals</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="na">sizeLabel</span><span class="p">()))</span><span class="w"/><span class="n">totalChars</span><span class="w"/><span class="o">+=</span><span class="w"/><span class="n">s</span><span class="p">.</span><span class="na">sizeValue</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="s">"tool"</span><span class="p">.</span><span class="na">equals</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="na">sizeLabel</span><span class="p">())</span><span class="w"/><span class="o">||</span><span class="w"/><span class="s">"tools"</span><span class="p">.</span><span class="na">equals</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="na">sizeLabel</span><span class="p">()))</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">toolCalls</span><span class="w"/><span class="o">+=</span><span class="w"/><span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="w"/><span class="n">s</span><span class="p">.</span><span class="na">sizeValue</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kt">long</span><span class="w"/><span class="n">wallClockMs</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">runStartedAt</span><span class="w"/><span class="o">==</span><span class="w"/><span class="kc">null</span><span class="w"/><span class="o">?</span><span class="w"/><span class="n">totalDurationMs</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">:</span><span class="w"/><span class="n">java</span><span class="p">.</span><span class="na">time</span><span class="p">.</span><span class="na">Duration</span><span class="p">.</span><span class="na">between</span><span class="p">(</span><span class="n">runStartedAt</span><span class="p">,</span><span class="w"/><span class="n">java</span><span class="p">.</span><span class="na">time</span><span class="p">.</span><span class="na">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">()).</span><span class="na">toMillis</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kt">long</span><span class="w"/><span class="n">approxTokens</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">totalChars</span><span class="w"/><span class="o">/</span><span class="w"/><span class="n">4</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">tiles</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">tiles</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__summary-tiles"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">tiles</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">buildTile</span><span class="p">(</span><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">CLOCK</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">formatMillis</span><span class="p">(</span><span class="n">wallClockMs</span><span class="p">),</span><span class="w"/><span class="s">"Total time"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">tiles</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">buildTile</span><span class="p">(</span><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">REFRESH</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Integer</span><span class="p">.</span><span class="na">toString</span><span class="p">(</span><span class="n">phase</span><span class="p">.</span><span class="na">iterations</span><span class="p">()),</span><span class="w"/><span class="s">"Model iterations"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">tiles</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">buildTile</span><span class="p">(</span><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">COGS</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Integer</span><span class="p">.</span><span class="na">toString</span><span class="p">(</span><span class="n">toolCalls</span><span class="p">),</span><span class="w"/><span class="s">"Tool calls"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">tiles</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">buildTile</span><span class="p">(</span><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">FILE_TEXT_O</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"≈ "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">approxTokens</span><span class="p">,</span><span class="w"/><span class="s">"Output tokens (~chars/4)"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">tiles</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>Two subtleties deserve attention. The total duration, when the start instant is known,
is measured as the actually elapsed wall-clock time between beginning and present, not
as a mere sum of the phase durations; pauses between the steps thus enter the reported
total time. And the number of output tokens is not measured but estimated from the
character count — as character count divided by four — which is why the tile carries
the approximately sign. It is a deliberately coarse approximation that gives the
viewer an order of magnitude without promising more than it can hold.</p><p>Each individual tile is a small, ever-identical building block of icon, value and
label.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="n">Div</span><span class="w"/><span class="nf">buildTile</span><span class="p">(</span><span class="n">VaadinIcon</span><span class="w"/><span class="n">icon</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">value</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">label</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">tile</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">tile</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__tile"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Icon</span><span class="w"/><span class="n">i</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Icon</span><span class="p">(</span><span class="n">icon</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">i</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__tile-icon"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">valueSpan</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="n">value</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">valueSpan</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__tile-value"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">labelSpan</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="n">label</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">labelSpan</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__tile-label"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">tile</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">i</span><span class="p">,</span><span class="w"/><span class="n">valueSpan</span><span class="p">,</span><span class="w"/><span class="n">labelSpan</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">tile</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>The bar chart is the most instructive part of the card, for it shows how a usable
chart can be drawn without any charting library, solely with the means of the
platform. First the largest duration value is determined, which serves as the
reference magnitude for the scaling; then each phase receives a row.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="n">Div</span><span class="w"/><span class="nf">buildPhaseChart</span><span class="p">()</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">chart</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">chart</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__chart"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">chartTitle</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">makeSectionLabel</span><span class="p">(</span><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">BAR_CHART_H</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"Per-phase duration"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">chart</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">chartTitle</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kt">long</span><span class="w"/><span class="n">maxMs</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">1</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">PhaseStat</span><span class="w"/><span class="n">s</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">phaseStats</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">maxMs</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">Math</span><span class="p">.</span><span class="na">max</span><span class="p">(</span><span class="n">maxMs</span><span class="p">,</span><span class="w"/><span class="n">s</span><span class="p">.</span><span class="na">duration</span><span class="p">().</span><span class="na">toMillis</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">PhaseStat</span><span class="w"/><span class="n">s</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">phaseStats</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">chart</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">buildChartRow</span><span class="p">(</span><span class="n">s</span><span class="p">,</span><span class="w"/><span class="n">maxMs</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">chart</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>The core lies in the individual row. From the duration of the phase and the previously
determined maximum a percentage is calculated which is set directly as the width of
the bar. A lower bound of two per cent ensures that even very short phases appear as a
visible bar and do not vanish entirely.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="n">Div</span><span class="w"/><span class="nf">buildChartRow</span><span class="p">(</span><span class="n">PhaseStat</span><span class="w"/><span class="n">s</span><span class="p">,</span><span class="w"/><span class="kt">long</span><span class="w"/><span class="n">maxMs</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">row</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">row</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__chart-row"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">label</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="na">label</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">label</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__chart-label"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kt">long</span><span class="w"/><span class="n">ms</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">s</span><span class="p">.</span><span class="na">duration</span><span class="p">().</span><span class="na">toMillis</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kt">double</span><span class="w"/><span class="n">pct</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">Math</span><span class="p">.</span><span class="na">max</span><span class="p">(</span><span class="n">2</span><span class="p">.</span><span class="na">0</span><span class="p">,</span><span class="w"/><span class="p">(</span><span class="n">ms</span><span class="w"/><span class="o">*</span><span class="w"/><span class="n">100</span><span class="p">.</span><span class="na">0</span><span class="p">)</span><span class="w"/><span class="o">/</span><span class="w"/><span class="n">maxMs</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">barTrack</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">barTrack</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__chart-track"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">bar</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">bar</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__chart-bar"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">bar</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__chart-bar--"</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">s</span><span class="p">.</span><span class="na">colorClass</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">bar</span><span class="p">.</span><span class="na">getStyle</span><span class="p">().</span><span class="na">set</span><span class="p">(</span><span class="s">"width"</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="p">.</span><span class="na">format</span><span class="p">(</span><span class="n">java</span><span class="p">.</span><span class="na">util</span><span class="p">.</span><span class="na">Locale</span><span class="p">.</span><span class="na">ROOT</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"%.1f%%"</span><span class="p">,</span><span class="w"/><span class="n">pct</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">barTrack</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">bar</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">duration</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="n">formatMillis</span><span class="p">(</span><span class="n">ms</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">duration</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__chart-duration"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">size</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="na">sizeValue</span><span class="p">()</span><span class="w"/><span class="o">+</span><span class="w"/><span class="s">" "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">s</span><span class="p">.</span><span class="na">sizeLabel</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">size</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__chart-size"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">row</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">barTrack</span><span class="p">,</span><span class="w"/><span class="n">duration</span><span class="p">,</span><span class="w"/><span class="n">size</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">row</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>The width of the bar is set directly on the element through<code>getStyle().set("width", …)</code> as a percentage; the colour stems from a style class whose name is formed from the
colour class of the phase, so that the bars match the badges of the phases by colour.
Beside the bar the row shows the duration and the size figure. The duration is
formatted legibly: below a second in milliseconds, above it in seconds with two
decimal places.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">String</span><span class="w"/><span class="nf">formatMillis</span><span class="p">(</span><span class="kt">long</span><span class="w"/><span class="n">ms</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">ms</span><span class="w"/><span class="o">&lt;</span><span class="w"/><span class="n">1000</span><span class="p">)</span><span class="w"/><span class="k">return</span><span class="w"/><span class="n">ms</span><span class="w"/><span class="o">+</span><span class="w"/><span class="s">" ms"</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">String</span><span class="p">.</span><span class="na">format</span><span class="p">(</span><span class="n">java</span><span class="p">.</span><span class="na">util</span><span class="p">.</span><span class="na">Locale</span><span class="p">.</span><span class="na">ROOT</span><span class="p">,</span><span class="w"/><span class="s">"%.2f s"</span><span class="p">,</span><span class="w"/><span class="n">ms</span><span class="w"/><span class="o">/</span><span class="w"/><span class="n">1000</span><span class="p">.</span><span class="na">0</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>Thus from a few building blocks a complete little chart arises: a reference magnitude,
a row per phase, a bar set proportionally in its width and a readable label. Notable is
all that was not needed here — no charting library, no drawing on a canvas, no
additional framework. A<code>Div</code> given a percentage width is already a bar; no more is
required.</p><figure><img src="/images/2026/06/recipe-assistant-part-2-ch09.png" alt="The summary card: four stat tiles above a horizontal bar chart of the phase durations" loading="lazy" decoding="async"/><p>With that, the two cards that the run passed through quickly or reached last have also
been considered. The following and final chapter draws the sum and brings both parts
of the series together.</p><h2 id="10-conclusion">10. Conclusion</h2><p>The first part of this series showed that the attached server makes a difference; the
second has shown how that difference comes about. With that, the arc spanned by the
introduction to the first part is closed — the passage from the what to the how. The
step-by-step view halts what the main view reels off in one go and turns the fleeting
course into a legible object.</p><p>The load-bearing insight of this part was architectural in nature: both views rest on
the same engine. The protocol logic lives in a single place, in the<code>WalkthroughEngine</code>, whose five phases are realised as individually callable methods.
It is precisely this decomposition into resumable phases that guarantees the
slowed-down walkthrough shows not an imitation but the same course that the main view
traverses of its own accord. On this foundation stood the remaining chapters: the
processing track, which preserves the process card by card as a timeline; the
individual triggering of the phases, which leaves the pace to the viewer; the data
panel, which sets the state beside the course; the presentation of parallel tool
calls, whose form itself displays the concurrency; the streaming with a final
Markdown rendering in the phase card, which made good on the counter-design announced
in the first part; the tool-catalogue card, which assigns clickable badges and full
JSON descriptors to the tools offered to the model; and finally the summary card,
which concludes the run in figures and a hand-drawn bar chart.</p><p>However different the two views are in their effect, they are connected by the same
principle that already ran through the first part. An interface for model-driven tool
use must not merely display results; it must keep the process of their emergence
comprehensible. The main view fulfils this principle through juxtaposition and
simultaneity; the walkthrough view fulfils it through halting and decomposition. Both
are different answers to the same demand, and precisely their difference shows that
the demand admits of more than one realisation.</p><figure><img src="/images/2026/06/recipe-assistant-part-2-ch10.png" alt="One engine, five phases, two views: the comparison and walkthrough views drive the same five phases — the one shows that, the other how" loading="lazy" decoding="async"/><p>There remains the retrospect on the series as a whole. It began with an MCP server
built using nothing but the platform&rsquo;s own means, without Spring and without Jakarta
EE. It continued with an interface that preserved the same renunciation of
heavyweight frameworks and confined itself to Vaadin Flow with embedded Jetty. And it
concluded with two views on the same process, which show that the value of an
attached server cannot merely be asserted but can be demonstrated — once as a result,
once as a process. Whoever has followed the series from beginning to end thus
possesses not only a runnable demonstrator but an understanding of why a carefully
conducted interface is indispensable for conveying model-driven tool use.</p>
]]></content:encoded><category>Java</category><category>Vaadin</category><category>MCP</category><media:content url="https://svenruppert.com/images/2026/06/recipe-assistant-vaadin-ui-part-2-hero.jpg" medium="image"/><media:thumbnail url="https://svenruppert.com/images/2026/06/recipe-assistant-vaadin-ui-part-2-hero.jpg"/><enclosure url="https://svenruppert.com/images/2026/06/recipe-assistant-vaadin-ui-part-2-hero.jpg" type="image/jpeg" length="0"/></item><item><title>The Vaadin UI of the Recipe Assistant — Part 1: The Main View</title><link>https://svenruppert.com/posts/recipe-assistant-vaadin-ui-part-1-the-main-view/</link><pubDate>Fri, 26 Jun 2026 12:00:00 +0200</pubDate><author>sven.ruppert@gmail.com (Sven Ruppert)</author><dc:creator>Sven Ruppert</dc:creator><guid isPermaLink="true">https://svenruppert.com/posts/recipe-assistant-vaadin-ui-part-1-the-main-view/</guid><description>Building a Vaadin Flow interface that puts two model answers side by side — with and without an MCP server attached — to make the value of the protocol immediately perceivable. A tour through six concrete challenges, from running Vaadin without a starter to keeping state consistent under two writers.</description><content:encoded>&lt;![CDATA[<h2 id="1-introduction-the-interface-as-proof">1. Introduction: The Interface as Proof</h2><p>The preceding article in this series built an MCP server using nothing but the
facilities of the JDK and placed the Model Context Protocol in its conceptual
context. Its outcome, however, was not the server itself but a single image: two
answers from the same language model, set side by side, on the left without an
attached server and on the right with one. The left-hand answer draws solely on
the model&rsquo;s general knowledge of the world and proposes recipes that bear no
relation to the user&rsquo;s actual circumstances. The right-hand answer reaches, by
way of the MCP server, into the actual pantry and the stored dietary profile, and
recommends what can be prepared from the available ingredients while respecting
the diet and the medical constraints.</p><p>This image is more than an illustration. It is the actual argument of the entire
project. The value of a protocol for tool integration may be disputed at length
so long as it is merely asserted; yet once two visibly different answers come into
being before the viewer&rsquo;s eyes at the very same moment, the dispute dissolves. The
juxtaposition replaces assertion with immediate perception. For precisely this
reason, it is not only the server but also the interface that produces this image
which merits an examination of its own. Where the first article dealt with the
mechanics of the protocol, the present part descends to the question of its
perceptibility.</p><p>However plain the two-column arrangement may appear to the viewer, its production
is anything but self-evident. For the compelling image to arise, two requests to
the same model must be conducted concurrently, the one without and the other with
an attached tool catalogue. Their answers arrive character by character and at
differing speeds, and they must flow simultaneously into two separate areas
without intermingling. The protocol traffic that makes the right-hand answer
possible in the first place runs hidden during normal operation and must be
rendered legible by deliberate design. And all of this has to happen without the
interface stalling or losing the impression of simultaneity on which its effect
depends.</p><p>From this follows the guiding observation of this part: the persuasive force of
the juxtaposition rests less on the logic of the answers than on the discipline
of their presentation. An interface for model-driven tool use must not merely
display results; it must keep the process of their emergence comprehensible.</p><p>The present part is therefore conceived as a tour through the concrete challenges
rather than as an exhaustive catalogue of every constituent of the interface. Each
of the following chapters takes up a challenge of its own: running Vaadin without
the comfort of a framework, conducting two requests concurrently, channelling a
character-by-character stream into a server-side component tree, drawing an honest
distinction between an unfinished and a completed answer, making the protocol
visible, and finally preserving a consistent state. The beginning is made by the
question of how the application comes to run at all — and that without Spring,
without Jakarta EE, and with as few dependencies as possible.</p><h2 id="2-running-vaadin-without-a-starter">2. Running Vaadin Without a Starter</h2><p>Anyone setting up a Vaadin application usually reaches for a starter. A single
dependency entry, an annotation, and the interface is up and running; the servlet
is registered, the initialisers are discovered, the static resources are mounted,
and the WebSocket channel stands ready. All of this happens behind a façade that
the developer neither sees nor needs to understand. For a project that has made a
deliberate point of forgoing Spring and Jakarta EE, however, this very façade is
the object of interest. The present chapter opens it and shows the few, clearly
nameable steps of which the start-up in fact consists.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-ch02.png" alt="Comparison: with a starter (hidden) versus without a starter (explicit)" loading="lazy" decoding="async"/><p>The entry point could hardly be plainer. The module&rsquo;s<code>Main</code> class first sets the
default locale of the virtual machine to English and then starts the embedded
server on the centrally held port.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kn">package</span><span class="w"/><span class="nn">com.svenruppert.mcp.ui</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"/><span class="nn">com.svenruppert.dependencies.core.logger.HasLogger</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"/><span class="nn">com.svenruppert.mcp.common.Ports</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"/><span class="nn">java.util.Locale</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="kd">public</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="kd">class</span><span class="nc">Main</span><span class="w"/><span class="kd">implements</span><span class="w"/><span class="n">HasLogger</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">main</span><span class="p">(</span><span class="n">String</span><span class="o">[]</span><span class="w"/><span class="n">args</span><span class="p">)</span><span class="w"/><span class="kd">throws</span><span class="w"/><span class="n">Exception</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Locale</span><span class="p">.</span><span class="na">setDefault</span><span class="p">(</span><span class="n">Locale</span><span class="p">.</span><span class="na">ENGLISH</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Main</span><span class="p">().</span><span class="na">run</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">run</span><span class="p">()</span><span class="w"/><span class="kd">throws</span><span class="w"/><span class="n">Exception</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">logger</span><span class="p">().</span><span class="na">info</span><span class="p">(</span><span class="s">"vaadin-ui starting on port {}"</span><span class="p">,</span><span class="w"/><span class="n">Ports</span><span class="p">.</span><span class="na">VAADIN_UI</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">new</span><span class="w"/><span class="n">UiServer</span><span class="p">(</span><span class="n">Ports</span><span class="p">.</span><span class="na">VAADIN_UI</span><span class="p">).</span><span class="na">start</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>Setting the locale is no incidental detail. The entire application presents itself
in English, right down to the<code>lang</code> attribute that Vaadin writes into the
delivered HTML. Without this fixing, the application would adopt the locale of the
host machine and would unexpectedly speak German on a German system. The actual
work is carried out by<code>UiServer</code>, whose start-up routine structures the remainder
of the chapter.</p><p>The first step erects the server and the servlet context. Here the first
non-obvious decision already arises.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">start</span><span class="p">()</span><span class="w"/><span class="kd">throws</span><span class="w"/><span class="n">Exception</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">server</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Server</span><span class="p">(</span><span class="n">port</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ServletContextHandler</span><span class="w"/><span class="n">context</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ServletContextHandler</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">setContextPath</span><span class="p">(</span><span class="s">"/"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">setSessionHandler</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">SessionHandler</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// Sharing the parent classloader avoids the server/system class isolation</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// that a WebAppContext would impose.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">setClassLoader</span><span class="p">(</span><span class="n">Thread</span><span class="p">.</span><span class="na">currentThread</span><span class="p">().</span><span class="na">getContextClassLoader</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// Expose every META-INF/resources/ directory from the classpath as a</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// virtual webapp root. Servlet 3.0 lets JARs ship static assets there</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// (Vaadin's flow-push delivers vaadinPush-min.js under</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// META-INF/resources/VAADIN/static/push/), but ServletContextHandler</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// does not auto-mount them the way WebAppContext does.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">setBaseResource</span><span class="p">(</span><span class="n">metaInfResources</span><span class="p">(</span><span class="n">context</span><span class="p">));</span></span></span></code></pre></div></div><p>A plain<code>ServletContextHandler</code> is used, not the more obvious<code>WebAppContext</code>. The
difference is decisive here. A<code>WebAppContext</code> installs a class loader of its own
whose separation between server and application classes hides the classes from<code>org.eclipse.jetty.*</code> from the application. Vaadin, however, requires these
classes during its own initialisation, which is why a<code>WebAppContext</code> would cause
the Vaadin initialisers to fail. The<code>ServletContextHandler</code> instead shares the
class loader of the calling thread, and the isolation falls away. This very
plainness, however, buys extra effort elsewhere: static resources that a<code>WebAppContext</code> would mount of its own accord must now be provided explicitly. The
helper method responsible for this is examined further below.</p><p>In the second step, the<code>VaadinServlet</code> is registered.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="c1">// Register VaadinServlet at "/*". Async + WebSocket support is needed for @Push.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ServletHolder</span><span class="w"/><span class="n">vaadinHolder</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ServletHolder</span><span class="p">(</span><span class="s">"vaadin"</span><span class="p">,</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">VaadinServlet</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">vaadinHolder</span><span class="p">.</span><span class="na">setInitParameter</span><span class="p">(</span><span class="s">"productionMode"</span><span class="p">,</span><span class="w"/><span class="s">"true"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">vaadinHolder</span><span class="p">.</span><span class="na">setAsyncSupported</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">vaadinHolder</span><span class="p">.</span><span class="na">setInitOrder</span><span class="p">(</span><span class="n">1</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addServlet</span><span class="p">(</span><span class="n">vaadinHolder</span><span class="p">,</span><span class="w"/><span class="s">"/*"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// ServletDeployer is a context listener. With VaadinServlet already registered,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// it logs and skips creation.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addEventListener</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ServletDeployer</span><span class="p">());</span></span></span></code></pre></div></div><p>The servlet is registered under the path pattern<code>/*</code> and thus answers every
request. Three settings deserve attention. Production mode switches off the
development regime with its continuous frontend build and presupposes the
previously produced production bundle. Enabling asynchronous processing is the
precondition for<code>@Push</code> to work at all later on. The init order, finally, ensures
that the servlet comes up early in the initialisation of the context. The<code>ServletDeployer</code> registered afterwards is a context listener that would otherwise
create the servlet itself; since the servlet has already been registered by hand,
it confines itself to a log entry.</p><p>The third and most extensive step constitutes the heart of the framework-free
start-up. Since Vaadin 25, the framework no longer registers its initialisers of
its own accord; they must therefore be registered by hand, and in the correct
order.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="c1">// Vaadin SCIs in correct order. LookupServletContainerInitializer must run first.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addServletContainerInitializer</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ServletContainerInitializerHolder</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">LookupServletContainerInitializer</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">LookupInitializer</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">loadVaadinClass</span><span class="p">(</span><span class="s">"com.vaadin.flow.di.LookupInitializer$ResourceProviderImpl"</span><span class="p">),</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">loadVaadinClass</span><span class="p">(</span><span class="s">"com.vaadin.flow.di.LookupInitializer$StaticFileHandlerFactoryImpl"</span><span class="p">),</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">loadVaadinClass</span><span class="p">(</span><span class="s">"com.vaadin.flow.di.LookupInitializer$AppShellPredicateImpl"</span><span class="p">),</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">DefaultApplicationConfigurationFactory</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">DefaultRoutePathProvider</span><span class="p">.</span><span class="na">class</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addServletContainerInitializer</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ServletContainerInitializerHolder</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">RouteRegistryInitializer</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">MainView</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughView</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">MainLayout</span><span class="p">.</span><span class="na">class</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addServletContainerInitializer</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ServletContainerInitializerHolder</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">VaadinAppShellInitializer</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AppShell</span><span class="p">.</span><span class="na">class</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addServletContainerInitializer</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ServletContainerInitializerHolder</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AnnotationValidator</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AppShell</span><span class="p">.</span><span class="na">class</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addServletContainerInitializer</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ServletContainerInitializerHolder</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ErrorNavigationTargetInitializer</span><span class="p">.</span><span class="na">class</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addServletContainerInitializer</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ServletContainerInitializerHolder</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WebComponentExporterAwareValidator</span><span class="p">.</span><span class="na">class</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">context</span><span class="p">.</span><span class="na">addServletContainerInitializer</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ServletContainerInitializerHolder</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WebComponentConfigurationRegistryInitializer</span><span class="p">.</span><span class="na">class</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">));</span></span></span></code></pre></div></div><p>Each of these seven calls registers a<code>ServletContainerInitializer</code> and at the
same time names the classes for which it is responsible. These classes correspond
to the<code>@HandlesTypes</code> that a servlet container would otherwise determine by
scanning the classpath; since registration is done by hand here, they are passed
explicitly. The first initialiser is the service lookup and must run first, as all
the others build upon it. The second sets up the route registry and is given the
three navigable classes<code>MainView</code>,<code>WalkthroughView</code> and<code>MainLayout</code>; only thus
does the route registry find the two views and the shared layout. The remaining
five set up the application shell, validate annotations, determine the error
targets, and validate or register any web components.</p><p>Striking are the three classes loaded via<code>loadVaadinClass</code> in the first call.
These are internal, non-exported Vaadin classes that cannot be written directly as
a class literal. The helper method therefore loads them by name and fails with a
clear message should Vaadin&rsquo;s internal structure ever change.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">Class</span><span class="o">&lt;?&gt;</span><span class="w"/><span class="n">loadVaadinClass</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">name</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">Class</span><span class="p">.</span><span class="na">forName</span><span class="p">(</span><span class="n">name</span><span class="p">,</span><span class="w"/><span class="kc">false</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Thread</span><span class="p">.</span><span class="na">currentThread</span><span class="p">().</span><span class="na">getContextClassLoader</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">ClassNotFoundException</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">throw</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">IllegalStateException</span><span class="p">(</span><span class="s">"Vaadin runtime class not found: "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">name</span><span class="p">,</span><span class="w"/><span class="n">e</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>The fourth step completes the wiring and starts the server. Beforehand, Jakarta
WebSocket support is configured, without which the server push over the WebSocket
channel would not come about.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="c1">// Jakarta WebSocket support so @Push can use the websocket transport.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">JakartaWebSocketServletContainerInitializer</span><span class="p">.</span><span class="na">configure</span><span class="p">(</span><span class="n">context</span><span class="p">,</span><span class="w"/><span class="kc">null</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">server</span><span class="p">.</span><span class="na">setHandler</span><span class="p">(</span><span class="n">context</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Runtime</span><span class="p">.</span><span class="na">getRuntime</span><span class="p">().</span><span class="na">addShutdownHook</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">Thread</span><span class="p">(</span><span class="k">this</span><span class="p">::</span><span class="n">safeStop</span><span class="p">,</span><span class="w"/><span class="s">"ui-shutdown"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">server</span><span class="p">.</span><span class="na">start</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">logger</span><span class="p">().</span><span class="na">info</span><span class="p">(</span><span class="s">"Vaadin UI listening on http://localhost:{}/"</span><span class="p">,</span><span class="w"/><span class="n">port</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">CountDownLatch</span><span class="w"/><span class="n">latch</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">CountDownLatch</span><span class="p">(</span><span class="n">1</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Runtime</span><span class="p">.</span><span class="na">getRuntime</span><span class="p">().</span><span class="na">addShutdownHook</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">Thread</span><span class="p">(</span><span class="n">latch</span><span class="p">::</span><span class="n">countDown</span><span class="p">,</span><span class="w"/><span class="s">"ui-latch"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">latch</span><span class="p">.</span><span class="na">await</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>After the context has been set as the handler, a shutdown hook is installed that
brings the server to an orderly halt on interruption. The actual start then
reports the reachable address. Since the main thread would otherwise end and the
virtual machine would terminate the application, a<code>CountDownLatch</code> keeps the
thread open until a second shutdown hook releases it. The server thus runs until
it is explicitly stopped.</p><p>There remains the helper method that provides the static resources — the very work
that a<code>WebAppContext</code> would take on of its own accord.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">Resource</span><span class="w"/><span class="nf">metaInfResources</span><span class="p">(</span><span class="n">ServletContextHandler</span><span class="w"/><span class="n">context</span><span class="p">)</span><span class="w"/><span class="kd">throws</span><span class="w"/><span class="n">Exception</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ClassLoader</span><span class="w"/><span class="n">cl</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">Thread</span><span class="p">.</span><span class="na">currentThread</span><span class="p">().</span><span class="na">getContextClassLoader</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ResourceFactory</span><span class="w"/><span class="n">factory</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">ResourceFactory</span><span class="p">.</span><span class="na">of</span><span class="p">(</span><span class="n">context</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// Servlet 3 convention: each JAR may carry static files under META-INF/resources/.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// flow-push delivers vaadinPush-min.js under META-INF/resources/VAADIN/static/push/,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// and Vaadin's prebuilt production bundle ships under</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// META-INF/resources/VAADIN/build/ in vaadin-prod-bundle.jar — both are</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="c1">// exposed by combining every classpath entry's META-INF/resources tree.</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">List</span><span class="o">&lt;</span><span class="n">Resource</span><span class="o">&gt;</span><span class="w"/><span class="n">resources</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ArrayList</span><span class="o">&lt;&gt;</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">URL</span><span class="w"/><span class="n">u</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">Collections</span><span class="p">.</span><span class="na">list</span><span class="p">(</span><span class="n">cl</span><span class="p">.</span><span class="na">getResources</span><span class="p">(</span><span class="s">"META-INF/resources"</span><span class="p">)))</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">resources</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">factory</span><span class="p">.</span><span class="na">newResource</span><span class="p">(</span><span class="n">u</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">resources</span><span class="p">.</span><span class="na">isEmpty</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">factory</span><span class="p">.</span><span class="na">newResource</span><span class="p">(</span><span class="s">"."</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">resources</span><span class="p">.</span><span class="na">size</span><span class="p">()</span><span class="w"/><span class="o">==</span><span class="w"/><span class="n">1</span><span class="w"/><span class="o">?</span><span class="w"/><span class="n">resources</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="n">0</span><span class="p">)</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">ResourceFactory</span><span class="p">.</span><span class="na">combine</span><span class="p">(</span><span class="n">resources</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>By the Servlet 3 convention, every archive on the classpath may carry static
files under<code>META-INF/resources</code>. Vaadin makes use of this in several places, for
instance for the push script and for the prebuilt production bundle. The method
gathers all such directories from the classpath and combines them into a single
virtual root, so that the server delivers them as one resource collection.</p><p>The entire sequence can be grasped at a glance.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-diagram-01.png" alt="Framework-free Vaadin start-up flow" loading="lazy" decoding="async"/><p>With that, the framework-free start-up is complete. It consists of erecting the
context, providing the static resources, registering the servlet, explicitly
registering the seven initialisers, configuring the WebSocket, and the actual
start. Each of these steps is visible and traceable in the source. What a starter
conceals as a convenience lies open here — and this very openness is the reward of
forgoing it. The price is a handful of additional lines; the return is a start-up
that can be understood entirely with the platform&rsquo;s own means, without the detour
through a further layer of abstraction. It is on this foundation that the next
chapter builds, turning to the construction of the main view itself.</p><h2 id="3-conducting-two-requests-side-by-side">3. Conducting Two Requests Side by Side</h2><p>The persuasive force of the juxtaposition, of which the introductory chapter
spoke, rests on two conditions. The first is simultaneity: the viewer should see
both answers come into being at the very same moment. The second is equality of
conditions: both answers must be given to the same question and under the same
premises, so that the visible difference is attributable solely to the attachment
of the server and not to some incidental unequal treatment in the construction.
Neither condition is self-evident; both are worked into the source. This chapter
shows how.</p><p>Equality of conditions begins with the layout of the view. For each of the two
columns there is a field of the same kind; what exists on the left exists on the
right in the same shape.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">TextArea</span><span class="w"/><span class="n">questionInput</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">TextArea</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">Checkbox</span><span class="w"/><span class="n">mcpToggle</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Checkbox</span><span class="p">(</span><span class="s">"Enable MCP"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">Button</span><span class="w"/><span class="n">sendButton</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Button</span><span class="p">(</span><span class="s">"Ask"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">PLACEHOLDER_PLAIN</span><span class="w"/><span class="o">=</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"_Generic answers will appear here..._"</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">PLACEHOLDER_MCP</span><span class="w"/><span class="o">=</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"_Answers using your pantry will appear here..._"</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">Markdown</span><span class="w"/><span class="n">leftBody</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Markdown</span><span class="p">(</span><span class="n">PLACEHOLDER_PLAIN</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">Markdown</span><span class="w"/><span class="n">rightBody</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Markdown</span><span class="p">(</span><span class="n">PLACEHOLDER_MCP</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">StringBuilder</span><span class="w"/><span class="n">leftBuffer</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">StringBuilder</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">StringBuilder</span><span class="w"/><span class="n">rightBuffer</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">StringBuilder</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">ChatProgressView</span><span class="w"/><span class="n">leftProgress</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgressView</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">ChatProgressView</span><span class="w"/><span class="n">rightProgress</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgressView</span><span class="p">();</span></span></span></code></pre></div></div><p>Each column is assigned its own<code>Markdown</code> component for the finished answer, its
own buffer for the incoming text, and its own progress view. This paired layout is
the structural basis of the symmetry; it ensures that the left and right columns
are cut from the same cloth.</p><p>The symmetry shows itself more emphatically still in the fact that both columns
issue from a single method. A boolean parameter decides whether the plain or the
MCP-enabled variant arises.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="n">Div</span><span class="w"/><span class="nf">buildAnswerColumn</span><span class="p">(</span><span class="kt">boolean</span><span class="w"/><span class="n">mcp</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">col</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">col</span><span class="p">.</span><span class="na">addClassNames</span><span class="p">(</span><span class="s">"recipe-answer"</span><span class="p">,</span><span class="w"/><span class="n">mcp</span><span class="w"/><span class="o">?</span><span class="w"/><span class="s">"recipe-answer--mcp"</span><span class="w"/><span class="p">:</span><span class="w"/><span class="s">"recipe-answer--plain"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">chip</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">chip</span><span class="p">.</span><span class="na">addClassNames</span><span class="p">(</span><span class="s">"recipe-chip"</span><span class="p">,</span><span class="w"/><span class="n">mcp</span><span class="w"/><span class="o">?</span><span class="w"/><span class="s">"recipe-chip--mcp"</span><span class="w"/><span class="p">:</span><span class="w"/><span class="s">"recipe-chip--plain"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">mcp</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">chip</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">Icon</span><span class="p">(</span><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">MAGIC</span><span class="p">),</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="s">"With MCP"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">else</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">chip</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">Icon</span><span class="p">(</span><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">COMMENT_O</span><span class="p">),</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="s">"Without MCP"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">header</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">(</span><span class="n">chip</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">header</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-answer__header"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">subtitle</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="n">mcp</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">?</span><span class="w"/><span class="s">"Has access to your pantry &amp; profile"</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">:</span><span class="w"/><span class="s">"Generic recipe knowledge only"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">subtitle</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-card__subtitle"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ChatProgressView</span><span class="w"/><span class="n">progress</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">mcp</span><span class="w"/><span class="o">?</span><span class="w"/><span class="n">rightProgress</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">leftProgress</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Markdown</span><span class="w"/><span class="n">body</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">mcp</span><span class="w"/><span class="o">?</span><span class="w"/><span class="n">rightBody</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">leftBody</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">body</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-answer__body"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">col</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">header</span><span class="p">,</span><span class="w"/><span class="n">subtitle</span><span class="p">,</span><span class="w"/><span class="n">progress</span><span class="p">,</span><span class="w"/><span class="n">body</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">col</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>The two columns are produced in one place by two calls with the values<code>false</code> and<code>true</code>; the left, accordingly, is the plain one, the right the MCP-enabled one.
Apart from the labelling, the colouring and the choice of the associated field
pair, the construction is identical. Anyone searching the code for a difference
between the columns finds it solely in that one boolean value — and this is
precisely the assurance, visible in the source, that the juxtaposition is honest.</p><p>Simultaneity, in turn, arises on triggering via the button. The method<code>handleSend</code> prepares the interface and then sets the concurrent processing in
motion.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">handleSend</span><span class="p">()</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">question</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">questionInput</span><span class="p">.</span><span class="na">getValue</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">question</span><span class="w"/><span class="o">==</span><span class="w"/><span class="kc">null</span><span class="w"/><span class="o">||</span><span class="w"/><span class="n">question</span><span class="p">.</span><span class="na">isBlank</span><span class="p">())</span><span class="w"/><span class="k">return</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">leftBuffer</span><span class="p">.</span><span class="na">setLength</span><span class="p">(</span><span class="n">0</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">rightBuffer</span><span class="p">.</span><span class="na">setLength</span><span class="p">(</span><span class="n">0</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">leftBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="s">""</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">rightBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="s">""</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">leftProgress</span><span class="p">.</span><span class="na">clear</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">clear</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">sendButton</span><span class="p">.</span><span class="na">setEnabled</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">UI</span><span class="w"/><span class="n">ui</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">UI</span><span class="p">.</span><span class="na">getCurrent</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kt">boolean</span><span class="w"/><span class="n">withMcp</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">mcpToggle</span><span class="p">.</span><span class="na">getValue</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">Boolean</span><span class="o">&gt;</span><span class="w"/><span class="n">leftDone</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;&gt;</span><span class="p">(</span><span class="n">Boolean</span><span class="p">.</span><span class="na">FALSE</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">Boolean</span><span class="o">&gt;</span><span class="w"/><span class="n">rightDone</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;&gt;</span><span class="p">(</span><span class="o">!</span><span class="n">withMcp</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Thread</span><span class="p">.</span><span class="na">startVirtualThread</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">runPlain</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span><span class="w"/><span class="n">question</span><span class="p">,</span><span class="w"/><span class="n">leftDone</span><span class="p">,</span><span class="w"/><span class="n">rightDone</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">withMcp</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Thread</span><span class="p">.</span><span class="na">startVirtualThread</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">runWithMcp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span><span class="w"/><span class="n">question</span><span class="p">,</span><span class="w"/><span class="n">leftDone</span><span class="p">,</span><span class="w"/><span class="n">rightDone</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">else</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="s">"_(MCP disabled - check the box to enable.)_"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>After resetting both panels and disabling the button, two mutually independent
virtual threads are started — one for the plain request, one for the MCP-enabled
one. Virtual threads are the fitting means here: they are so lightweight that one
may be created for each request without hesitation, and they may block while
waiting for the answer to arrive without tying up a platform resource. The plain
request is always made; the MCP-enabled one only when the toggle is active.
Otherwise the right column receives a plain note.</p><p>The division of labour behind the two threads deserves a remark of its own. The
plain request drives the method<code>runPlain</code>, the MCP-enabled one the method<code>runWithMcp</code>; the latter makes use of the same<code>WalkthroughEngine</code> that also drives
the step-by-step view of the second part — here merely as an automatic loop with
no pauses between the phases. The actual protocol logic thus resides in exactly one
place, and only the drive differs. The bodies of both methods contain the
streaming callback that conveys the incoming text fragments into the interface; the
following chapter is devoted to it, which is why it is omitted here.</p><p>There remains the joining of the two strands. Each thread notes at its end that it
has finished, and then calls a plain coordination method.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">maybeReenable</span><span class="p">(</span><span class="n">UI</span><span class="w"/><span class="n">ui</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">Boolean</span><span class="o">&gt;</span><span class="w"/><span class="n">leftDone</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">Boolean</span><span class="o">&gt;</span><span class="w"/><span class="n">rightDone</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">leftDone</span><span class="p">.</span><span class="na">get</span><span class="p">()</span><span class="w"/><span class="o">&amp;&amp;</span><span class="w"/><span class="n">rightDone</span><span class="p">.</span><span class="na">get</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">sendButton</span><span class="p">.</span><span class="na">setEnabled</span><span class="p">(</span><span class="kc">true</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>Only once both flags are set is the button re-enabled. Since the two flags are set
and read in separate threads, they are realised as<code>AtomicReference</code>, whose value
is safely visible across thread boundaries. Here the initial assignment mentioned
earlier pays off: the right-hand flag is pre-set with<code>!withMcp</code> and therefore
counts as done at once when the toggle is inactive. Thus, in plain operation, the
single thread of the left column suffices to re-enable the button, whereas in MCP
operation both threads are awaited. No separate synchronisation is needed; the
logic carries itself.</p><p>The whole sequence can be read as a fork and a join.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-diagram-02.png" alt="Fork-and-join of the two model requests" loading="lazy" decoding="async"/><p>Whereas the diagram shows the structure of the fork, the following figure makes the
temporal side visible: both strands genuinely run within the same span of time, and
the MCP-enabled lane lasts longer because it calls tools along the way.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-ch03.png" alt="Two requests on one timeline: both strands run concurrently, the MCP lane lasts longer" loading="lazy" decoding="async"/><p>With that, the two conditions on which the juxtaposition rests are met. The
symmetry of construction ensures that both answers arise under equal premises; the
concurrent conduct over two virtual threads ensures that they do so at the same
moment. What has remained open so far is the question of how the text fragments
arriving in the background threads find their way into the server-side interface at
all. It is to this very question that the following chapter turns.</p><h2 id="4-channelling-a-character-stream-into-the-component-tree">4. Channelling a Character Stream into the Component Tree</h2><p>The preceding chapter left a question open: how the text fragments arriving in the
two background threads find their way into the interface at all. Three obstacles
stand in the way. First, Vaadin keeps the component tree on the server; the model&rsquo;s
answer, however, does not arise there but arrives over the network. Second, the
component tree may not be altered from an arbitrary thread, but only within the
locked context of the associated session. Third, the browser does not poll for
updates of its own accord; without further intervention the picture would stand
still until the user reloads the page. This chapter shows how these three obstacles
are overcome together — along the path that a single fragment travels.</p><p>At the beginning of this path stands the stream itself. The<code>OllamaClient</code> requests
the model&rsquo;s answer with streaming enabled and reads it as Server-Sent Events. The
plain request of the left column runs through the method<code>chatPlain</code>, which joins
the system prompt and the question into a message list and then calls the inner
streaming method. The third parameter is noteworthy: a<code>Consumer&lt;String&gt;</code> to which
every incoming text fragment is handed.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">chatPlain</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">systemPrompt</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">userMessage</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Consumer</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="w"/><span class="n">textChunk</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Consumer</span><span class="o">&lt;</span><span class="n">ChatProgress</span><span class="o">&gt;</span><span class="w"/><span class="n">progress</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Instant</span><span class="w"/><span class="n">start</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">progress</span><span class="p">.</span><span class="na">accept</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">info</span><span class="p">(</span><span class="s">"Sending request to Ollama"</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"model: "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">model</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">List</span><span class="o">&lt;</span><span class="n">ObjectNode</span><span class="o">&gt;</span><span class="w"/><span class="n">messages</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ArrayList</span><span class="o">&lt;&gt;</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">messages</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">message</span><span class="p">(</span><span class="s">"system"</span><span class="p">,</span><span class="w"/><span class="n">systemPrompt</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">messages</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">message</span><span class="p">(</span><span class="s">"user"</span><span class="p">,</span><span class="w"/><span class="n">userMessage</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">progress</span><span class="p">.</span><span class="na">accept</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">stream</span><span class="p">(</span><span class="s">"Streaming reply..."</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">StreamResult</span><span class="w"/><span class="n">outcome</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">streamOnce</span><span class="p">(</span><span class="n">messages</span><span class="p">,</span><span class="w"/><span class="kc">null</span><span class="p">,</span><span class="w"/><span class="n">textChunk</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">progress</span><span class="p">.</span><span class="na">accept</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">success</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"Reply complete ("</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">outcome</span><span class="p">.</span><span class="na">assistantText</span><span class="p">.</span><span class="na">length</span><span class="p">()</span><span class="w"/><span class="o">+</span><span class="w"/><span class="s">" chars)"</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Duration</span><span class="p">.</span><span class="na">between</span><span class="p">(</span><span class="n">start</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">())));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">RuntimeException</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">progress</span><span class="p">.</span><span class="na">accept</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">error</span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="na">getMessage</span><span class="p">()));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">throw</span><span class="w"/><span class="n">e</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>The method is given two callbacks: one for the text fragments, the other for
structured progress events. The actual stream<code>chatPlain</code> leaves to the inner
method<code>streamOnce</code>, to which it passes the text callback unchanged; the<code>null</code>
supplied in place of the tool list marks the plain request.</p><p>The actual decomposition of the stream takes place in this inner method. It reads
the answer line by line; empty lines and lines not belonging to the stream are
skipped, and the sentinel line<code>[DONE]</code> ends the loop.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">(</span><span class="n">BufferedReader</span><span class="w"/><span class="n">reader</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">BufferedReader</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">new</span><span class="w"/><span class="n">InputStreamReader</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="na">body</span><span class="p">(),</span><span class="w"/><span class="n">StandardCharsets</span><span class="p">.</span><span class="na">UTF_8</span><span class="p">)))</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">line</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">while</span><span class="w"/><span class="p">((</span><span class="n">line</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">reader</span><span class="p">.</span><span class="na">readLine</span><span class="p">())</span><span class="w"/><span class="o">!=</span><span class="w"/><span class="kc">null</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">line</span><span class="p">.</span><span class="na">isEmpty</span><span class="p">())</span><span class="w"/><span class="k">continue</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="o">!</span><span class="n">line</span><span class="p">.</span><span class="na">startsWith</span><span class="p">(</span><span class="s">"data:"</span><span class="p">))</span><span class="w"/><span class="k">continue</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">data</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">line</span><span class="p">.</span><span class="na">substring</span><span class="p">(</span><span class="n">5</span><span class="p">).</span><span class="na">trim</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="s">"[DONE]"</span><span class="p">.</span><span class="na">equals</span><span class="p">(</span><span class="n">data</span><span class="p">))</span><span class="w"/><span class="k">break</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">JsonNode</span><span class="w"/><span class="n">event</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">event</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">MAPPER</span><span class="p">.</span><span class="na">readTree</span><span class="p">(</span><span class="n">data</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">Exception</span><span class="w"/><span class="n">parseError</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">logger</span><span class="p">().</span><span class="na">warn</span><span class="p">(</span><span class="s">"Skipping malformed SSE chunk: {}"</span><span class="p">,</span><span class="w"/><span class="n">data</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">continue</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">JsonNode</span><span class="w"/><span class="n">choice</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">event</span><span class="p">.</span><span class="na">path</span><span class="p">(</span><span class="s">"choices"</span><span class="p">).</span><span class="na">path</span><span class="p">(</span><span class="n">0</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">JsonNode</span><span class="w"/><span class="n">delta</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">choice</span><span class="p">.</span><span class="na">path</span><span class="p">(</span><span class="s">"delta"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">delta</span><span class="p">.</span><span class="na">hasNonNull</span><span class="p">(</span><span class="s">"content"</span><span class="p">))</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">piece</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">delta</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="s">"content"</span><span class="p">).</span><span class="na">asText</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="o">!</span><span class="n">piece</span><span class="p">.</span><span class="na">isEmpty</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">assistantText</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">piece</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">textChunk</span><span class="w"/><span class="o">!=</span><span class="w"/><span class="kc">null</span><span class="p">)</span><span class="w"/><span class="n">textChunk</span><span class="p">.</span><span class="na">accept</span><span class="p">(</span><span class="n">piece</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">delta</span><span class="p">.</span><span class="na">hasNonNull</span><span class="p">(</span><span class="s">"tool_calls"</span><span class="p">))</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">JsonNode</span><span class="w"/><span class="n">tc</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">delta</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="s">"tool_calls"</span><span class="p">))</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kt">int</span><span class="w"/><span class="n">idx</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">tc</span><span class="p">.</span><span class="na">path</span><span class="p">(</span><span class="s">"index"</span><span class="p">).</span><span class="na">asInt</span><span class="p">(</span><span class="n">0</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ToolCallAccumulator</span><span class="w"/><span class="n">acc</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">calls</span><span class="p">.</span><span class="na">computeIfAbsent</span><span class="p">(</span><span class="n">idx</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">k</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ToolCallAccumulator</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">tc</span><span class="p">.</span><span class="na">hasNonNull</span><span class="p">(</span><span class="s">"id"</span><span class="p">))</span><span class="w"/><span class="n">acc</span><span class="p">.</span><span class="na">id</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">tc</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="s">"id"</span><span class="p">).</span><span class="na">asText</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">JsonNode</span><span class="w"/><span class="n">fn</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">tc</span><span class="p">.</span><span class="na">path</span><span class="p">(</span><span class="s">"function"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">fn</span><span class="p">.</span><span class="na">hasNonNull</span><span class="p">(</span><span class="s">"name"</span><span class="p">))</span><span class="w"/><span class="n">acc</span><span class="p">.</span><span class="na">name</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">fn</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="s">"name"</span><span class="p">).</span><span class="na">asText</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">fn</span><span class="p">.</span><span class="na">hasNonNull</span><span class="p">(</span><span class="s">"arguments"</span><span class="p">))</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">acc</span><span class="p">.</span><span class="na">argumentsBuffer</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">fn</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="s">"arguments"</span><span class="p">).</span><span class="na">asText</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">finish</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">choice</span><span class="p">.</span><span class="na">path</span><span class="p">(</span><span class="s">"finish_reason"</span><span class="p">).</span><span class="na">asText</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">finish</span><span class="w"/><span class="o">!=</span><span class="w"/><span class="kc">null</span><span class="w"/><span class="o">&amp;&amp;</span><span class="w"/><span class="o">!</span><span class="n">finish</span><span class="p">.</span><span class="na">isBlank</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">break</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>For the text path, only the middle section matters: if the delta carries content,
that portion is appended to the gathered answer text and at the same time handed to
the callback. In this way every fragment leaves the method at the very moment it
arrives. The section that follows gathers the components of any tool calls; it is
left undiscussed here and is the subject of the sixth chapter. The appearance of a
finish reason ends the loop.</p><p>With that, the source is clarified: the<code>Consumer&lt;String&gt;</code> receives the fragment.
Before it can appear in the browser, however, the first of the named obstacles must
be taken — the browser must be allowed to be supplied unsolicited. This is what the
annotation<code>@Push</code> does, which hangs not on an individual view but on the central<code>AppShell</code> and therefore applies to the whole application.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kn">package</span><span class="w"/><span class="nn">com.svenruppert.mcp.ui</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"/><span class="nn">com.vaadin.flow.component.page.AppShellConfigurator</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"/><span class="nn">com.vaadin.flow.component.page.Push</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"/><span class="nn">com.vaadin.flow.theme.Theme</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"/><span class="nn">com.vaadin.flow.theme.lumo.Lumo</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="nd">@Push</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="nd">@Theme</span><span class="p">(</span><span class="n">value</span><span class="w"/><span class="o">=</span><span class="w"/><span class="s">"recipe"</span><span class="p">,</span><span class="w"/><span class="n">variant</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">Lumo</span><span class="p">.</span><span class="na">LIGHT</span><span class="p">)</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="kd">public</span><span class="w"/><span class="kd">class</span><span class="nc">AppShell</span><span class="w"/><span class="kd">implements</span><span class="w"/><span class="n">AppShellConfigurator</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>The annotation permits the server to send changes to the browser of its own
accord — over the WebSocket channel that was configured expressly in the second
chapter. Because it sits on the<code>AppShell</code>, it applies to both views alike; the
second annotation merely determines the appearance.</p><p>There remains the second obstacle: the change in the correct context. The receiving
end in the main view is the method<code>runPlain</code>. It passes<code>chatPlain</code> a callback that
processes every fragment by way of<code>UI.access</code>.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">runPlain</span><span class="p">(</span><span class="n">UI</span><span class="w"/><span class="n">ui</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">question</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">Boolean</span><span class="o">&gt;</span><span class="w"/><span class="n">leftDone</span><span class="p">,</span><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">Boolean</span><span class="o">&gt;</span><span class="w"/><span class="n">rightDone</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">services</span><span class="p">.</span><span class="na">ollama</span><span class="p">().</span><span class="na">chatPlain</span><span class="p">(</span><span class="n">Prompts</span><span class="p">.</span><span class="na">SYSTEM_PLAIN</span><span class="p">,</span><span class="w"/><span class="n">question</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">chunk</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">leftBuffer</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">chunk</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">leftBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="n">leftBuffer</span><span class="p">.</span><span class="na">toString</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}),</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ev</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">leftProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">ev</span><span class="p">)));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">Exception</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">logger</span><span class="p">().</span><span class="na">warn</span><span class="p">(</span><span class="s">"Plain chat failed"</span><span class="p">,</span><span class="w"/><span class="n">e</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">leftBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="s">"**Error:** "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">e</span><span class="p">.</span><span class="na">getMessage</span><span class="p">()));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">finally</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">leftDone</span><span class="p">.</span><span class="na">set</span><span class="p">(</span><span class="n">Boolean</span><span class="p">.</span><span class="na">TRUE</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">maybeReenable</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span><span class="w"/><span class="n">leftDone</span><span class="p">,</span><span class="w"/><span class="n">rightDone</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>Here the threads come together. The reference to the<code>UI</code> was captured earlier in
the triggering thread and handed along into the background thread. Every incoming
fragment is not processed directly but wrapped in a<code>UI.access</code> call. This is
precisely the key: altering the component tree from a foreign thread would be
impermissible, since it would bypass the session&rsquo;s lock and lead to race
conditions.<code>UI.access</code> instead enqueues the change into the locked context of the
session and, since<code>@Push</code> is active, at the same time sets its transmission to the
browser in motion. Thus a single call overcomes the second and the third obstacle
in one.</p><p>Within the callback, finally, the buffer pattern shows itself. Every fragment is
appended to a<code>StringBuilder</code>, and the entire accumulated content is set as the new
content of the<code>Markdown</code> component. That the whole text is set anew on every
fragment rather than appended to the existing content is deliberate: the approach
is plain, self-contained, and matches the interface of the component, which takes a
complete text. The progress callback proceeds in the same way. At the end the
method notes in the<code>finally</code> block that the left column is finished and calls the
coordination method considered in the previous chapter; the error path sets an
error message, likewise by way of<code>UI.access</code>.</p><p>The whole path of a fragment can be read as a sequence of a few steps.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-diagram-03.png" alt="The path of a streamed text fragment into the component tree" loading="lazy" decoding="async"/><p>With that, the three obstacles named at the outset are taken together: the stream
is decomposed in the client,<code>@Push</code> permits the unsolicited supply of the browser,
and<code>UI.access</code> runs every fragment in the correct context and brings about its
transmission. One detail remains conspicuous: on every fragment the entire
accumulated text is set anew on the<code>Markdown</code> component, so that the component
displays an ever-growing, still-incomplete document. What the viewer perceives while
the document is incomplete — and whether this is the right thing — is the question
to which the following chapter turns.</p><h2 id="5-rendering-the-stream-as-markdown">5. Rendering the Stream as Markdown</h2><p>The fourth chapter traced the path of a fragment into the component tree and, at
the end, singled out one detail: on every fragment the entire accumulated text is
set on the<code>Markdown</code> component. What stood there as an incidental observation is
the actual subject of this chapter. Where the previous chapter asked how a fragment
reaches the interface, this one asks what the interface visibly does with it. The
heart of the callback reads:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="n">chunk</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">leftBuffer</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">chunk</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">leftBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="n">leftBuffer</span><span class="p">.</span><span class="na">toString</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}),</span></span></span></code></pre></div></div><p>The field<code>leftBody</code> is a<code>com.vaadin.flow.component.markdown.Markdown</code>, not a plain
text component. Its<code>setContent</code> call replaces the previous content entirely; the
component discards its existing rendering and re-renders the whole accumulated text.
Since this happens on every fragment, the answer appears to the viewer not as
growing raw text but as a continuously rendered Markdown document.</p><p>For the viewer, this means that the answer builds itself up as a structured
document. A heading appears as a heading as soon as its line is complete; a list
appears as a list as soon as its entries arrive; an emphasis takes effect as soon
as it is closed. The document thus comes into being not as a draft to be put into
shape at the end, but already in its final form, step by step.</p><p>This approach has a price. As long as a piece of markup is still incomplete — an
emphasis opened but not yet closed, a list begun but not yet finished, a code block
that has arrived only halfway — the component renders that intermediate state just
as it currently stands. If the missing part arrives shortly afterwards, the
affected section rearranges itself. The viewer perceives this as a brief
reordering that resolves itself as the remainder arrives.</p><p>The main view accepts this occasional reordering deliberately, and the reason lies
in its purpose. It is the comparison view, and its entire persuasive force rests,
as the introductory chapter set out, on the immediacy of the juxtaposition. The
viewer should see both answers come into being vividly and at the very same moment;
the progressive rendering serves precisely this vividness. The answers are,
moreover, manageable recipe texts in which the reordering is rare and slight. The
small price thus stands in a reasonable proportion to the impression of simultaneity
that is gained.</p><p>It should not be passed over that the same matter can be solved differently — and is
solved differently in the same project. The step-by-step view takes the opposite
path: it shows the stream first as raw text and renders it as Markdown only after it
has completed.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">query</span><span class="p">.</span><span class="na">toolCalls</span><span class="p">().</span><span class="na">isEmpty</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">result</span><span class="p">.</span><span class="na">remove</span><span class="p">(</span><span class="n">streamDiv</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Markdown</span><span class="w"/><span class="n">rendered</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Markdown</span><span class="p">(</span><span class="n">streamedText</span><span class="p">.</span><span class="na">isBlank</span><span class="p">()</span><span class="w"/><span class="o">?</span><span class="w"/><span class="s">"_(no text)_"</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">streamedText</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">rendered</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-walkthrough__rendered"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">result</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">rendered</span><span class="p">);</span></span></span></code></pre></div></div><p>During the stream the text grows there in a plain area; only after completion is
that area removed and replaced by a freshly created<code>Markdown</code> component. The
corresponding phase description states it expressly: &ldquo;Streaming is complete and the
text is rendered as Markdown.&rdquo; The reason for this different choice lies in the
different purpose: a teaching view prefers a calm, stable picture of each phase that
the viewer can study at his own pace, without the rendered output rearranging itself
beneath his gaze. The closer examination of this counter-design belongs in the
second part; here the note suffices that these are two well-founded answers to the
same question.</p><p>The two strategies can be set side by side in brief.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-diagram-04.png" alt="Two rendering strategies — progressive vs deferred Markdown" loading="lazy" decoding="async"/><p>Both paths follow the same principle, that an interface should honestly reflect the
state of its content — they merely draw different conclusions from it. The
comparison view shows the document as it forms, because it cares about the
simultaneity of its emergence. The teaching view shows each phase as it stands once
complete, because it cares about the calm of study. What neither view has so far
disclosed is the traffic that makes the right-hand answer possible in the first
place — the calls to the tools and their results. It is to this very traffic, and to
making it visible, that the following chapter turns.</p><h2 id="6-making-the-protocol-visible">6. Making the Protocol Visible</h2><p>The previous chapter closed with an observation: what neither view had so far
disclosed is the traffic that makes the right-hand answer possible in the first
place. This traffic is precisely what MCP achieves — and at the same time what
remains hidden in normal operation. The fetching of the tool list, the individual
tool calls, their results and the repeated model runs all happen behind the scenes.
Without making them visible, the right column would be a mere black box that
delivers a better answer but does not show why. A comparison view meant to instruct
cannot leave it at that. The main view therefore makes the traffic legible — by way
of a vertical timeline that runs alongside each answer.</p><p>At the beginning stands a plain event model. Every step of the processing is
recorded as a<code>ChatProgress</code>, a<code>record</code> with a small, closed enumeration of event
kinds and a factory method for each.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"/><span class="kd">record</span><span class="nc">ChatProgress</span><span class="p">(</span><span class="n">Kind</span><span class="w"/><span class="n">kind</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">detail</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="w"/><span class="n">at</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">enum</span><span class="w"/><span class="n">Kind</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">INFO</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">STREAM</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">TOOL_CALL</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">TOOL_RESULT</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">SUCCESS</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ERROR</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">ChatProgress</span><span class="w"/><span class="nf">info</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">label</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgress</span><span class="p">(</span><span class="n">Kind</span><span class="p">.</span><span class="na">INFO</span><span class="p">,</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="kc">null</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">ChatProgress</span><span class="w"/><span class="nf">info</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">detail</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgress</span><span class="p">(</span><span class="n">Kind</span><span class="p">.</span><span class="na">INFO</span><span class="p">,</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">detail</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">ChatProgress</span><span class="w"/><span class="nf">stream</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">label</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgress</span><span class="p">(</span><span class="n">Kind</span><span class="p">.</span><span class="na">STREAM</span><span class="p">,</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="kc">null</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">ChatProgress</span><span class="w"/><span class="nf">toolCall</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">detail</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgress</span><span class="p">(</span><span class="n">Kind</span><span class="p">.</span><span class="na">TOOL_CALL</span><span class="p">,</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">detail</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">ChatProgress</span><span class="w"/><span class="nf">toolResult</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">detail</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgress</span><span class="p">(</span><span class="n">Kind</span><span class="p">.</span><span class="na">TOOL_RESULT</span><span class="p">,</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">detail</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">ChatProgress</span><span class="w"/><span class="nf">success</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">Duration</span><span class="w"/><span class="n">elapsed</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">d</span><span class="w"/><span class="o">=</span><span class="w"/><span class="s">"%.1fs"</span><span class="p">.</span><span class="na">formatted</span><span class="p">(</span><span class="n">elapsed</span><span class="p">.</span><span class="na">toMillis</span><span class="p">()</span><span class="w"/><span class="o">/</span><span class="w"/><span class="n">1000</span><span class="p">.</span><span class="na">0</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgress</span><span class="p">(</span><span class="n">Kind</span><span class="p">.</span><span class="na">SUCCESS</span><span class="p">,</span><span class="w"/><span class="n">label</span><span class="p">,</span><span class="w"/><span class="n">d</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">ChatProgress</span><span class="w"/><span class="nf">error</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">message</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ChatProgress</span><span class="p">(</span><span class="n">Kind</span><span class="p">.</span><span class="na">ERROR</span><span class="p">,</span><span class="w"/><span class="s">"Error"</span><span class="p">,</span><span class="w"/><span class="n">message</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>Six event kinds suffice to describe the whole course of an MCP run: a general
notice, the beginning of a stream, a tool call, a tool result, the success and the
error. Every event carries a concise label, an optional detail text and the moment
of its creation. The detail text is the part decisive for the protocol, for it
holds the arguments of a call and the returned result.</p><p>The display of these events is taken on by the<code>ChatProgressView</code>, a vertical
timeline. Its central method appends a row for each event.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">append</span><span class="p">(</span><span class="n">ChatProgress</span><span class="w"/><span class="n">event</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">row</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">row</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-progress__row"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">row</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-progress__row--"</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">event</span><span class="p">.</span><span class="na">kind</span><span class="p">().</span><span class="na">name</span><span class="p">().</span><span class="na">toLowerCase</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Icon</span><span class="w"/><span class="n">icon</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Icon</span><span class="p">(</span><span class="n">iconFor</span><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="na">kind</span><span class="p">()));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">icon</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-progress__icon"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">time</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="n">TIME_FMT</span><span class="p">.</span><span class="na">format</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">LocalTime</span><span class="p">.</span><span class="na">ofInstant</span><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="na">at</span><span class="p">(),</span><span class="w"/><span class="n">ZoneId</span><span class="p">.</span><span class="na">systemDefault</span><span class="p">())));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">time</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-progress__time"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Span</span><span class="w"/><span class="n">label</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Span</span><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="na">label</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">label</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-progress__label"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Div</span><span class="w"/><span class="n">head</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Div</span><span class="p">(</span><span class="n">icon</span><span class="p">,</span><span class="w"/><span class="n">time</span><span class="p">,</span><span class="w"/><span class="n">label</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">head</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-progress__head"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">row</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">head</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="na">detail</span><span class="p">()</span><span class="w"/><span class="o">!=</span><span class="w"/><span class="kc">null</span><span class="w"/><span class="o">&amp;&amp;</span><span class="w"/><span class="o">!</span><span class="n">event</span><span class="p">.</span><span class="na">detail</span><span class="p">().</span><span class="na">isBlank</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Pre</span><span class="w"/><span class="n">detail</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Pre</span><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="na">detail</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">detail</span><span class="p">.</span><span class="na">addClassName</span><span class="p">(</span><span class="s">"recipe-progress__detail"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">row</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">detail</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">add</span><span class="p">(</span><span class="n">row</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">getElement</span><span class="p">().</span><span class="na">executeJs</span><span class="p">(</span><span class="s">"this.scrollTop = this.scrollHeight"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">VaadinIcon</span><span class="w"/><span class="nf">iconFor</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">Kind</span><span class="w"/><span class="n">k</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="k">switch</span><span class="w"/><span class="p">(</span><span class="n">k</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">case</span><span class="w"/><span class="n">INFO</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">INFO_CIRCLE_O</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">case</span><span class="w"/><span class="n">STREAM</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">PAPERPLANE</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">case</span><span class="w"/><span class="n">TOOL_CALL</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">COG</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">case</span><span class="w"/><span class="n">TOOL_RESULT</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">CHECK_CIRCLE_O</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">case</span><span class="w"/><span class="n">SUCCESS</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">CHECK_CIRCLE</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">case</span><span class="w"/><span class="n">ERROR</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">VaadinIcon</span><span class="p">.</span><span class="na">WARNING</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">};</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>Each row consists of a head — an icon suited to the event kind, the time and the
label — and, if a detail text is present, a<code>Pre</code> block set beneath it. This very<code>Pre</code> block carries the content in a monospaced and thus legibly arranged form:
here the arguments of a call and the result JSON appear in a shape that the viewer
can follow character by character. The event kind also determines a style class of
its own, so that calls, results and errors are distinguished by colour as well.
Finally, the method scrolls to the most recent entry by way of a small JavaScript
call, so that the bar always shows the current happening.</p><p>The events are created where the traffic actually takes place — in the method<code>runWithMcp</code>, which drives the MCP-enabled request.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">runWithMcp</span><span class="p">(</span><span class="n">UI</span><span class="w"/><span class="n">ui</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">question</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">Boolean</span><span class="o">&gt;</span><span class="w"/><span class="n">leftDone</span><span class="p">,</span><span class="w"/><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">Boolean</span><span class="o">&gt;</span><span class="w"/><span class="n">rightDone</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughEngine</span><span class="w"/><span class="n">engine</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">services</span><span class="p">.</span><span class="na">engine</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Instant</span><span class="w"/><span class="n">start</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughState</span><span class="w"/><span class="n">state</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">engine</span><span class="p">.</span><span class="na">startRun</span><span class="p">(</span><span class="n">Prompts</span><span class="p">.</span><span class="na">SYSTEM_MCP</span><span class="p">,</span><span class="w"/><span class="n">question</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ChatProgress</span><span class="p">.</span><span class="na">info</span><span class="p">(</span><span class="s">"Fetching tool list from MCP server"</span><span class="p">)));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ToolCatalog</span><span class="w"/><span class="n">catalog</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">engine</span><span class="p">.</span><span class="na">fetchToolCatalogue</span><span class="p">(</span><span class="n">state</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ChatProgress</span><span class="p">.</span><span class="na">info</span><span class="p">(</span><span class="s">"Loaded "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">catalog</span><span class="p">.</span><span class="na">toolNames</span><span class="p">().</span><span class="na">size</span><span class="p">()</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">+</span><span class="w"/><span class="s">" tools from MCP server"</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="p">.</span><span class="na">join</span><span class="p">(</span><span class="s">", "</span><span class="p">,</span><span class="w"/><span class="n">catalog</span><span class="p">.</span><span class="na">toolNames</span><span class="p">()))));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="kt">int</span><span class="w"/><span class="n">safety</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">0</span><span class="p">;</span><span class="w"/><span class="n">safety</span><span class="w"/><span class="o">&lt;</span><span class="w"/><span class="n">WalkthroughEngine</span><span class="p">.</span><span class="na">MAX_ITERATIONS</span><span class="p">;</span><span class="w"/><span class="n">safety</span><span class="o">++</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">final</span><span class="w"/><span class="kt">int</span><span class="w"/><span class="n">iterNumber</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">state</span><span class="p">.</span><span class="na">iteration</span><span class="p">()</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">1</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">stream</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"Iteration "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">iterNumber</span><span class="w"/><span class="o">+</span><span class="w"/><span class="s">": streaming reply..."</span><span class="p">)));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ModelQuery</span><span class="w"/><span class="n">query</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">engine</span><span class="p">.</span><span class="na">runModelQuery</span><span class="p">(</span><span class="n">state</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">chunk</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">rightBuffer</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">chunk</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">rightBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="n">rightBuffer</span><span class="p">.</span><span class="na">toString</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">query</span><span class="p">.</span><span class="na">toolCalls</span><span class="p">().</span><span class="na">isEmpty</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Duration</span><span class="w"/><span class="n">elapsed</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">Duration</span><span class="p">.</span><span class="na">between</span><span class="p">(</span><span class="n">start</span><span class="p">,</span><span class="w"/><span class="n">Instant</span><span class="p">.</span><span class="na">now</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kt">int</span><span class="w"/><span class="n">it</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">state</span><span class="p">.</span><span class="na">iteration</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">success</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"Reply complete after "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">it</span><span class="w"/><span class="o">+</span><span class="w"/><span class="s">" iteration"</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">+</span><span class="w"/><span class="p">(</span><span class="n">it</span><span class="w"/><span class="o">==</span><span class="w"/><span class="n">1</span><span class="w"/><span class="o">?</span><span class="w"/><span class="s">""</span><span class="w"/><span class="p">:</span><span class="w"/><span class="s">"s"</span><span class="p">),</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">elapsed</span><span class="p">)));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">PendingToolCall</span><span class="w"/><span class="n">pc</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">query</span><span class="p">.</span><span class="na">toolCalls</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">toolCall</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"Call "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">pc</span><span class="p">.</span><span class="na">name</span><span class="p">()</span><span class="w"/><span class="o">+</span><span class="w"/><span class="s">"(...)"</span><span class="p">,</span><span class="w"/><span class="n">pc</span><span class="p">.</span><span class="na">argumentsJson</span><span class="p">())));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ToolExecution</span><span class="w"/><span class="n">exec</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">engine</span><span class="p">.</span><span class="na">executeTools</span><span class="p">(</span><span class="n">state</span><span class="p">,</span><span class="w"/><span class="n">query</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">for</span><span class="w"/><span class="p">(</span><span class="n">WalkthroughPhase</span><span class="p">.</span><span class="na">ToolOutcome</span><span class="w"/><span class="n">oc</span><span class="w"/><span class="p">:</span><span class="w"/><span class="n">exec</span><span class="p">.</span><span class="na">outcomes</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ChatProgress</span><span class="w"/><span class="n">event</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">oc</span><span class="p">.</span><span class="na">error</span><span class="p">()</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">?</span><span class="w"/><span class="n">ChatProgress</span><span class="p">.</span><span class="na">error</span><span class="p">(</span><span class="n">oc</span><span class="p">.</span><span class="na">name</span><span class="p">()</span><span class="w"/><span class="o">+</span><span class="w"/><span class="s">": "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">oc</span><span class="p">.</span><span class="na">resultText</span><span class="p">())</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">:</span><span class="w"/><span class="n">ChatProgress</span><span class="p">.</span><span class="na">toolResult</span><span class="p">(</span><span class="s">"Result from "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">oc</span><span class="p">.</span><span class="na">name</span><span class="p">(),</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">oc</span><span class="p">.</span><span class="na">resultText</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">event</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">info</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"Reached max tool-call iteration limit. Stopping."</span><span class="p">)));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightBuffer</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="s">"\n\n[Reached the maximum tool-call iteration limit. Stopping here.]"</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="n">rightBuffer</span><span class="p">.</span><span class="na">toString</span><span class="p">()));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">Exception</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">logger</span><span class="p">().</span><span class="na">warn</span><span class="p">(</span><span class="s">"MCP chat failed"</span><span class="p">,</span><span class="w"/><span class="n">e</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightProgress</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">ChatProgress</span><span class="p">.</span><span class="na">error</span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="na">getMessage</span><span class="p">())));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(()</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">rightBody</span><span class="p">.</span><span class="na">setContent</span><span class="p">(</span><span class="s">"**Error:** "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">e</span><span class="p">.</span><span class="na">getMessage</span><span class="p">()));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">finally</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">rightDone</span><span class="p">.</span><span class="na">set</span><span class="p">(</span><span class="n">Boolean</span><span class="p">.</span><span class="na">TRUE</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">maybeReenable</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span><span class="w"/><span class="n">leftDone</span><span class="p">,</span><span class="w"/><span class="n">rightDone</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(</span><span class="n">sidebar</span><span class="p">::</span><span class="n">refreshGrid</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ui</span><span class="p">.</span><span class="na">access</span><span class="p">(</span><span class="n">sidebar</span><span class="p">::</span><span class="n">refreshProfileForm</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>The method drives a loop whose inner steps — the fetching of the catalogue, the
model run, the tool execution — are performed by the<code>WalkthroughEngine</code> and are
examined more closely only in the second part. For this chapter, all that matters is
what happens between those steps: at every significant point a<code>ChatProgress</code> event
is created and appended to the timeline by way of<code>UI.access</code>. First an event
reports the fetching of the tool list, another the number and the names of the
loaded tools. In each run an event announces the beginning of the stream. If the
model requests no further tools, a success event reports the completion together
with the elapsed time. Otherwise the method creates, for each requested call, a call
event whose detail text carries the arguments, and after the execution, for each
result, a result event with the returned text — or, in the error case, an error
event. Thus, alongside the answer, a running, timestamped record of the whole
traffic comes into being.</p><p>The sequence of these events can be read as the timeline of a run.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-diagram-05.png" alt="Timeline of an MCP-enabled run (info / stream / tool calls / results / success)" loading="lazy" decoding="async"/><p>This is how the timeline looks during a run:</p><figure><img src="/images/2026/06/recipe-assistant-part-1-ch06.png" alt="A reproduction of the ChatProgressView timeline with an icon, time, label and monospaced detail block for each event" loading="lazy" decoding="async"/><p>With that, the traffic is no longer hidden. Alongside the right-hand answer the
viewer sees which tools the model requests, with which arguments it calls them and
which results flow back — and he sees it at the very moment it happens. It should be
noted that the step-by-step view presents the same traffic more elaborately still:
with one card per phase and with concurrently executed calls as side-by-side lanes
under a parallel marker. This richer, paused presentation is the subject of the
second part; the main view contents itself with the compact, running timeline.</p><p>One detail of the method already points ahead: in the<code>finally</code> block the sidebar is
refreshed after every run. Behind this lies the question of how the display of the
pantry and the profile agrees with the actual state that the tools may have changed.
It is to this very question of state consistency that the following chapter turns.</p><h2 id="7-keeping-the-state-consistent">7. Keeping the State Consistent</h2><p>The previous chapter ended with a detail that pointed ahead: in the<code>finally</code> block
of<code>runWithMcp</code> the sidebar is refreshed after every run. Behind this lies a
consistency problem to which this chapter is devoted. For two writers act upon the
same business state — the pantry and the dietary profile. The operator changes it by
hand through the maintenance sidebar; the model changes it indirectly through the
MCP tools, for instance by striking out a consumed ingredient or adjusting the
profile. A display that is to do justice to both writers must therefore be conducted
with care.</p><p>The first decision concerns the path the sidebar takes when writing. It writes
directly against the REST server and deliberately bypasses the MCP server. The
reason lies in its role: the sidebar is the operator&rsquo;s instrument, not part of the
model dialogue. The REST server holds the authoritative state; the MCP server
accesses this very state only by reading and writing through its tools. It would be
mistaken to route an operator&rsquo;s action through the model&rsquo;s tool channel. The pantry
write shows this.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">saveItem</span><span class="p">()</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">name</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">nameField</span><span class="p">.</span><span class="na">getValue</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">name</span><span class="w"/><span class="o">==</span><span class="w"/><span class="kc">null</span><span class="w"/><span class="o">||</span><span class="w"/><span class="n">name</span><span class="p">.</span><span class="na">isBlank</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">show</span><span class="p">(</span><span class="s">"Name is required"</span><span class="p">,</span><span class="w"/><span class="n">2500</span><span class="p">,</span><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">Position</span><span class="p">.</span><span class="na">MIDDLE</span><span class="p">)</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">addThemeVariants</span><span class="p">(</span><span class="n">NotificationVariant</span><span class="p">.</span><span class="na">LUMO_ERROR</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Double</span><span class="w"/><span class="n">qty</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">quantityField</span><span class="p">.</span><span class="na">getValue</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">qty</span><span class="w"/><span class="o">==</span><span class="w"/><span class="kc">null</span><span class="p">)</span><span class="w"/><span class="n">qty</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">0</span><span class="p">.</span><span class="na">0</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">unit</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">blankToDefault</span><span class="p">(</span><span class="n">unitField</span><span class="p">.</span><span class="na">getValue</span><span class="p">(),</span><span class="w"/><span class="s">"piece"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">category</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">blankToDefault</span><span class="p">(</span><span class="n">categoryField</span><span class="p">.</span><span class="na">getValue</span><span class="p">(),</span><span class="w"/><span class="s">"Pantry"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Optional</span><span class="o">&lt;</span><span class="n">LocalDate</span><span class="o">&gt;</span><span class="w"/><span class="n">bestBefore</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">Optional</span><span class="p">.</span><span class="na">ofNullable</span><span class="p">(</span><span class="n">bestBeforeField</span><span class="p">.</span><span class="na">getValue</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">PantryItem</span><span class="w"/><span class="n">item</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">PantryItem</span><span class="p">(</span><span class="n">name</span><span class="p">,</span><span class="w"/><span class="n">qty</span><span class="p">,</span><span class="w"/><span class="n">unit</span><span class="p">,</span><span class="w"/><span class="n">bestBefore</span><span class="p">,</span><span class="w"/><span class="n">category</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">nameField</span><span class="p">.</span><span class="na">isReadOnly</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">pantryClient</span><span class="p">.</span><span class="na">replace</span><span class="p">(</span><span class="n">item</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">show</span><span class="p">(</span><span class="s">"Updated "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">name</span><span class="p">,</span><span class="w"/><span class="n">1500</span><span class="p">,</span><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">Position</span><span class="p">.</span><span class="na">BOTTOM_START</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">else</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">pantryClient</span><span class="p">.</span><span class="na">create</span><span class="p">(</span><span class="n">item</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">show</span><span class="p">(</span><span class="s">"Added "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">name</span><span class="p">,</span><span class="w"/><span class="n">1500</span><span class="p">,</span><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">Position</span><span class="p">.</span><span class="na">BOTTOM_START</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">clearForm</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">refreshGrid</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">Exception</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">show</span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="na">getMessage</span><span class="p">(),</span><span class="w"/><span class="n">3000</span><span class="p">,</span><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">Position</span><span class="p">.</span><span class="na">MIDDLE</span><span class="p">)</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">addThemeVariants</span><span class="p">(</span><span class="n">NotificationVariant</span><span class="p">.</span><span class="na">LUMO_ERROR</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>After a brief check of the name, a<code>PantryItem</code> is assembled from the fields.
Whether it is created or replaced is decided by the state of the name field: if it
is read-only, an existing entry has been loaded for editing, and the call<code>replace</code>
replaces it; otherwise<code>create</code> adds a new one. What is decisive is the turn at the
end of the successful branch: the form is cleared and the grid is reloaded. This
very turn recurs on deletion.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">deleteSelected</span><span class="p">()</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Optional</span><span class="o">&lt;</span><span class="n">PantryItem</span><span class="o">&gt;</span><span class="w"/><span class="n">sel</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">grid</span><span class="p">.</span><span class="na">getSelectionModel</span><span class="p">().</span><span class="na">getFirstSelectedItem</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">sel</span><span class="p">.</span><span class="na">isEmpty</span><span class="p">())</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">show</span><span class="p">(</span><span class="s">"Select an item first"</span><span class="p">,</span><span class="w"/><span class="n">1500</span><span class="p">,</span><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">Position</span><span class="p">.</span><span class="na">BOTTOM_START</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">pantryClient</span><span class="p">.</span><span class="na">delete</span><span class="p">(</span><span class="n">sel</span><span class="p">.</span><span class="na">get</span><span class="p">().</span><span class="na">name</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">show</span><span class="p">(</span><span class="s">"Removed "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">sel</span><span class="p">.</span><span class="na">get</span><span class="p">().</span><span class="na">name</span><span class="p">(),</span><span class="w"/><span class="n">1500</span><span class="p">,</span><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">Position</span><span class="p">.</span><span class="na">BOTTOM_START</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">clearForm</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">refreshGrid</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">Exception</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">show</span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="na">getMessage</span><span class="p">(),</span><span class="w"/><span class="n">3000</span><span class="p">,</span><span class="w"/><span class="n">Notification</span><span class="p">.</span><span class="na">Position</span><span class="p">.</span><span class="na">MIDDLE</span><span class="p">)</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">addThemeVariants</span><span class="p">(</span><span class="n">NotificationVariant</span><span class="p">.</span><span class="na">LUMO_ERROR</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>Here too the grid is reloaded after the write. With that, the actual consistency
principle shows itself, which resides in the method<code>refreshGrid</code>.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">refreshGrid</span><span class="p">()</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">grid</span><span class="p">.</span><span class="na">setItems</span><span class="p">(</span><span class="n">pantryClient</span><span class="p">.</span><span class="na">list</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">Exception</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">logger</span><span class="p">().</span><span class="na">warn</span><span class="p">(</span><span class="s">"Could not refresh pantry grid: {}"</span><span class="p">,</span><span class="w"/><span class="n">e</span><span class="p">.</span><span class="na">getMessage</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>The grid is not updated locally — the entry just created is not, say, added to the
existing display — but refilled entirely from the server. After every change the
display thus reflects the authoritative state and never a locally maintained,
possibly diverging view. This principle is plain but consequential: as long as every
change closes with a reload, the display cannot drift apart.</p><p>The writes themselves are carried out by a thin REST client using the platform&rsquo;s own
means. The replacing method may stand for them.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="n">PantryItem</span><span class="w"/><span class="nf">replace</span><span class="p">(</span><span class="n">PantryItem</span><span class="w"/><span class="n">item</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">try</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">body</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">MAPPER</span><span class="p">.</span><span class="na">writeValueAsString</span><span class="p">(</span><span class="n">item</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">HttpResponse</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="w"/><span class="n">resp</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">http</span><span class="p">.</span><span class="na">send</span><span class="p">(</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">HttpRequest</span><span class="p">.</span><span class="na">newBuilder</span><span class="p">(</span><span class="n">URI</span><span class="p">.</span><span class="na">create</span><span class="p">(</span><span class="n">baseUrl</span><span class="w"/><span class="o">+</span><span class="w"/><span class="s">"/pantry/"</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">encode</span><span class="p">(</span><span class="n">item</span><span class="p">.</span><span class="na">name</span><span class="p">())))</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">timeout</span><span class="p">(</span><span class="n">Duration</span><span class="p">.</span><span class="na">ofSeconds</span><span class="p">(</span><span class="n">5</span><span class="p">))</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">header</span><span class="p">(</span><span class="s">"Content-Type"</span><span class="p">,</span><span class="w"/><span class="s">"application/json"</span><span class="p">)</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">PUT</span><span class="p">(</span><span class="n">HttpRequest</span><span class="p">.</span><span class="na">BodyPublishers</span><span class="p">.</span><span class="na">ofString</span><span class="p">(</span><span class="n">body</span><span class="p">,</span><span class="w"/><span class="n">StandardCharsets</span><span class="p">.</span><span class="na">UTF_8</span><span class="p">))</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">build</span><span class="p">(),</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">HttpResponse</span><span class="p">.</span><span class="na">BodyHandlers</span><span class="p">.</span><span class="na">ofString</span><span class="p">(</span><span class="n">StandardCharsets</span><span class="p">.</span><span class="na">UTF_8</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ensureSuccess</span><span class="p">(</span><span class="n">resp</span><span class="p">,</span><span class="w"/><span class="s">"replace pantry item"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">MAPPER</span><span class="p">.</span><span class="na">readValue</span><span class="p">(</span><span class="n">resp</span><span class="p">.</span><span class="na">body</span><span class="p">(),</span><span class="w"/><span class="n">PantryItem</span><span class="p">.</span><span class="na">class</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">RuntimeException</span><span class="w"/><span class="n">re</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">throw</span><span class="w"/><span class="n">re</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">Exception</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">throw</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">RuntimeException</span><span class="p">(</span><span class="s">"Failed to replace pantry item "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">item</span><span class="p">.</span><span class="na">name</span><span class="p">(),</span><span class="w"/><span class="n">e</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span></span></span></code></pre></div></div><p>An<code>HttpRequest</code> with the method<code>PUT</code> carries the entry, cast as JSON, to the
address of the REST server; a check of the status code ensures success. Creating and
deleting proceed with<code>POST</code> and<code>DELETE</code> respectively, after the same pattern. No
additional dependency is needed for this; the platform&rsquo;s<code>HttpClient</code> suffices.</p><p>The profile section of the sidebar is built after the same pattern. Its write
transmits the changed profile through the associated REST client, and its refresh
likewise reloads the profile from the server into the form. What holds for the
pantry therefore holds for the dietary profile as well: write, then reload.</p><p>There remains the opposite direction. The writes considered so far proceed from the
operator. The second writer is the model, which changes the same state through the
MCP tools. So that its changes too become visible, the main view refreshes the
sidebar after every MCP run — those very two calls in the<code>finally</code> block that the
previous chapter announced: the reloading of the grid and the reloading of the
profile form. Thus the circle closes in both directions. If the operator changes the
data, the next MCP-enabled answer reflects the change, because the model re-fetches
the state through the tools on every request. If the model changes the data, the
sidebar reflects it, because it reloads after the run.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-diagram-06.png" alt="Two writers, one source of truth — operator and model write through to the REST server" loading="lazy" decoding="async"/><figure><img src="/images/2026/06/recipe-assistant-part-1-ch07.png" alt="Two writers, one source of truth: operator and model change the same state, while the display reloads after every change" loading="lazy" decoding="async"/><p>This very closed circle is the actual demonstration value of the sidebar. The
operator can show, without a restart, how a change of data affects the next answer —
for instance by switching the diet, adding an intolerance or striking out an
ingredient — and he can at the same time observe how a change carried out by the
model appears at once in the sidebar. The rigour with which everything is reloaded
after every change is the condition for this demonstration to succeed reliably. With
that, the constituents of the main view have been considered in full; the following
and final chapter of this part draws the sum.</p><h2 id="8-conclusion">8. Conclusion</h2><p>The introduction to this part presented the main view as a proof: the single image
of two answers set side by side replaces the assertion about the value of MCP with
immediate perception. The guiding observation was that the persuasive force of this
juxtaposition rests less on the logic of the answers than on the discipline of their
presentation. The tour through the concrete hurdles has made good on that
observation.</p><p>Six hurdles had to be taken. Running without a starter laid the foundation by wiring
the start-up by hand and thereby disclosing what a starter otherwise conceals.
Conducting two requests concurrently created the simultaneity and, through the
symmetric construction of both columns, the equality of conditions. Channelling a
character-by-character stream into the component tree overcame the separation between
the background thread and the server-side interface, by having<code>@Push</code> and<code>UI.access</code> work together. Rendering the stream as Markdown let the answer come into
being as a structured document before the viewer&rsquo;s eyes. Making the protocol visible
turned the hidden tool traffic into a legible, running timeline. And keeping the
state consistent ensured that, after every change, the display reflects the
authoritative state, whether the operator or the model has changed it.</p><p>However different these hurdles are, a common thought connects them. An interface
for model-driven tool use must not merely display results; it must keep the process
of their emergence comprehensible. Each of the decisions discussed serves precisely
this comprehensibility: the simultaneity, so that the difference between the answers
becomes undeniable; the progressive rendering, so that the answer comes into being
vividly; the timeline, so that the traffic does not remain hidden; the reloading, so
that the display does not deceive. It is this very comprehensibility that turns a
mere display into a proof.</p><p>It should not be passed over that, for all its disclosure, the main view conceals
something. It sets the result of two requests side by side, yet it lets the
mechanism that brings forth the right-hand answer vanish into the automatic run. The
running timeline hints at it but does not halt it. It is precisely here that the
second part begins. The step-by-step view reverses the relationship: it halts the
pipeline, makes each phase individually triggerable, and turns the otherwise
fleeting course into a legible, temporally ordered object. Where this part showed
that the attached server makes a difference, the next will show how that difference
comes about — the passage from the what to the how.</p><figure><img src="/images/2026/06/recipe-assistant-part-1-ch08.png" alt="A map of the six hurdles: from the question through the six stations to an answer you can follow" loading="lazy" decoding="async"/><p>With that, the first part is concluded. The main view is no mere accessory to the
MCP server, but the means by which its value becomes perceptible — and it is so
precisely because it makes the discipline of its presentation its business.</p>
]]></content:encoded><category>Java</category><category>Vaadin</category><category>MCP</category><media:content url="https://svenruppert.com/images/2026/06/recipe-assistant-vaadin-ui-part-1-hero.jpg" medium="image"/><media:thumbnail url="https://svenruppert.com/images/2026/06/recipe-assistant-vaadin-ui-part-1-hero.jpg"/><enclosure url="https://svenruppert.com/images/2026/06/recipe-assistant-vaadin-ui-part-1-hero.jpg" type="image/jpeg" length="0"/></item></channel></rss>