<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Python on Posit Open Source</title>
    <link>https://opensource.posit.co/languages/python/</link>
    <description>Recent content in Python on Posit Open Source</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Fri, 30 Oct 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://opensource.posit.co/languages/python/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>All Together Now: Concurrent Async in Shiny for Python</title>
      <link>https://opensource.posit.co/blog/2026-10-30_shiny-python-concurrent-async/</link>
      <pubDate>Fri, 30 Oct 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-10-30_shiny-python-concurrent-async/</guid>
      <dc:creator>Josh Taillon</dc:creator>
      <dc:creator>Barret Schloerke</dc:creator><description><![CDATA[<!-- TODO before publishing: confirm the version number, the date, the authors, and the PR links. -->
<p><a href="https://pypi.org/project/shiny/" target="_blank" rel="noopener">Shiny for Python 1.9</a> overhauls how the reactive system runs async code, and now uses a concurrency model similar to Shiny for R. This post explains what changed, and shows how much faster an app gets when it awaits slow work. The key changes:</p>
<ul>
<li><a href="#one-slow-user-no-longer-blocks-everyone">A slow <code>await</code> in one session no longer blocks other sessions</a>, so individual users of an app no longer wait for each other.</li>
<li><a href="#async-outputs-run-at-the-same-time">Async outputs in the same session now run concurrently</a>, so a page&rsquo;s slow outputs run in parallel rather than serially.</li>
<li><a href="#reactivelock-is-deprecated"><code>reactive.lock()</code> is deprecated</a>, and the new <a href="#new-run_once_when_idle"><code>session.run_once_when_idle()</code></a> provides a way to change reactive state from a background task.</li>
</ul>
<p>These changes only affect <code>async</code> code. Your app still runs on one thread with a single <code>asyncio</code> event loop, and synchronous
code still blocks other sessions (see <a href="#concurrency-needs-await">Concurrency needs <code>await</code></a>). The difference is that an async
task now waits for slow work without blocking other tasks, in its own session or in others.</p>
<p>Before 1.9, one lock for the whole process serialized all reactive updates. The lock made sure that an input could not
change while an output that read it was still computing. In 1.9, the lock is gone, and instead each session sends its
outputs before it applies that user&rsquo;s next input, the same model as Shiny for R.
Within that rule, the reactive system starts each effect as its own task and moves on. While one session awaits a query, the
event loop is free to run other tasks, making the app more responsive for everyone.</p>
<h2 id="one-slow-user-no-longer-blocks-everyone">One slow user no longer blocks everyone
</h2>
<p>Until now, Shiny for Python processed reactivity behind one lock for the whole process, <code>reactive.lock()</code>. When a user&rsquo;s action invalidated an effect or an output, Shiny took the lock, ran everything that was invalidated, and released the lock only when all of it had finished. This model meant that any other reactive effects were blocked for as long as an operation kept the lock. During that time, no other session in the process (<em>e.g.</em>, a separate tab or another user) could update anything, not even an output that had nothing to do with the query.</p>
<p>The recording below shows an example of how the new system improves things drastically. It simulates four people using the same app each in a separate Shiny session. Alice, Bob and Carol each click &ldquo;Run report&rdquo; at the same time. A report has two async outputs, &ldquo;Sales&rdquo; and &ldquo;Forecast&rdquo;, and each one takes two seconds to finish query. Dan only watches a chart that the server redraws every 0.2 seconds. The plot at the bottom displays the <a href="https://shiny.posit.co/py/docs/opentelemetry.html" target="_blank" rel="noopener">OpenTelemetry spans</a> for the app to visualize the timeline of server processing.</p>
<p>Each user&rsquo;s card shows the status of their two outputs: a hand while the output waits its turn, a spinner while it runs, and a check when it is done. Under the button, the time since the click is split into three parts that add up to the total: how long the report <em>waited</em> before its first output started, how long its outputs <em>computed</em>, and how long the session was <em>blocked</em> afterwards, before its chart could update again.</p>
<h3 id="before-19">Before 1.9
</h3>
<figure>
<img src="https://opensource.posit.co/blog/2026-10-30_shiny-python-concurrent-async/before.gif" data-fig-alt="Shiny for Python 1.8: three users click Run report at the same time. Alice&#39;s report computes for 4 seconds and is then blocked for 8 seconds. Bob waits for 4 seconds, computes for 4, and is blocked for 4. Carol waits for 8 seconds and computes for 4. Dan&#39;s chart is blocked for 12 seconds. In the timeline, the six output spans form a staircase, one after another." alt="Shiny for Python 1.8. Because of the reactive.lock(), the reports run one after another and finish after 4, 8 and 12 seconds. Even within one report, Forecast waits for Sales to finish. Every live chart stops updating until the last report is done. In the OpenTelemetry timeline, the six output spans form a staircase, one after another." />
<figcaption aria-hidden="true"><strong>Shiny for Python 1.8.</strong> Because of the <code>reactive.lock()</code>, the reports run one after another and finish after 4, 8 and 12 seconds. Even within one report, Forecast waits for Sales to finish. Every live chart stops updating until the last report is done. In the OpenTelemetry timeline, the six output spans form a staircase, one after another.</figcaption>
</figure>
<h3 id="after-19">After 1.9
</h3>
<figure>
<img src="https://opensource.posit.co/blog/2026-10-30_shiny-python-concurrent-async/after.gif" data-fig-alt="Shiny for Python 1.9: three users click Run report at the same time. Every report waits 0 seconds, computes for 2 seconds, and is blocked for 0 seconds. Dan&#39;s chart keeps moving. In the OpenTelemetry timeline, all six output spans start and end together." alt="Shiny for Python 1.9. Because reactive effects are now allowed to run concurrently, both outputs of every report run at the same time, all three reports finish after 2 seconds, and Dan’s chart keeps updating during the slow operations." />
<figcaption aria-hidden="true"><strong>Shiny for Python 1.9.</strong> Because reactive effects are now allowed to run concurrently, both outputs of every report run at the same time, all three reports finish after 2 seconds, and Dan’s chart keeps updating during the slow operations.</figcaption>
</figure>
<p>In 1.8, the OpenTelemetry data shows the six output spans forming a staircase. Alice&rsquo;s &ldquo;Forecast&rdquo; waits for her own &ldquo;Sales&rdquo;, and Bob&rsquo;s report can&rsquo;t start until Alice&rsquo;s ends. No session redraws its chart (the green ticks) until the last report is done. In 1.9, all six outputs run at the same time, and Dan&rsquo;s chart never stops updating.</p>
<p>In 1.9, a reactive flush starts each invalidated effect as an asynchronous task and the reactive system doesn&rsquo;t wait for it to finish to continue processing events. While Alice&rsquo;s report awaits its query, the event loop is free to serve Bob, Carol, Dan, and everyone else.</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-10-30_shiny-python-concurrent-async/how-it-works.svg" data-fig-alt="Diagram in two panels. The top panel, Shiny 1.8, shows Alice holding one lock for the whole app while Bob and Carol wait for it. Behind the lock, in a box labeled Global queue, six outputs run in a single line: Alice&#39;s Sales, Alice&#39;s Forecast, Bob&#39;s Sales, Bob&#39;s Forecast, Carol&#39;s Sales, and Carol&#39;s Forecast. The bottom panel, Shiny 1.9, shows three separate sessions for Alice, Bob and Carol side by side. In each one, the user&#39;s click starts Sales and Forecast at the same time, inside a box labeled Per-session tasks, both feed into one step that sends both outputs together, and then the user&#39;s next input is applied. A note says that all six outputs run at the same time, and that each session applies its next input only after it sends its outputs, so inputs never change halfway through." alt="How a report runs in each version. In 1.8, every user waits for one lock, so the six outputs run one after another. In 1.9, each session has its own tasks, which start at the same time. The session sends their outputs together, and only then applies that user’s next input." />
<figcaption aria-hidden="true">How a report runs in each version. In 1.8, every user waits for one lock, so the six outputs run one after another. In 1.9, each session has its own tasks, which start at the same time. The session sends their outputs together, and only then applies that user’s next input.</figcaption>
</figure>
<p>The more users that click at the same time, the larger the difference:</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-10-30_shiny-python-concurrent-async/sessions.png" data-fig-alt="Line chart. With Shiny for Python 1.8, the wait for the last user grows by 4 seconds for each user, up to 32 seconds for 8 users. With 1.9, it stays at 2 seconds for any number of users." alt="The time until the last user sees a result, when several users click at the same time. Each report has two async outputs that await a two-second query." />
<figcaption aria-hidden="true">The time until the last user sees a result, when several users click at the same time. Each report has two async outputs that await a two-second query.</figcaption>
</figure>
<p>Put in terms of scaling: when <em>n</em> users each start <em>k</em> async outputs that each wait <em>t</em> seconds, the last user in 1.8 waits about <em>n</em> × <em>k</em> × <em>t</em> seconds, because every output runs after the one before it. In 1.9, every user waits about <em>t</em> seconds, for any number of users and outputs. The wait goes from $O(n \cdot k)$ to $O(1)$. In the chart, <em>k</em> is 2 and <em>t</em> is 2 seconds, so for 8 users the wait goes from 32 seconds to 2.</p>
<p><em>Caveat:</em> this speedup applies when the slow part is waiting on something outside Python that can serve many requests at the same time, such as a database or a web API. Work that keeps Python itself busy will still add up (see <a href="#concurrency-needs-await">Concurrency needs <code>await</code></a> below).</p>
<h2 id="async-outputs-run-at-the-same-time">Async outputs run at the same time
</h2>
<p>Apart from the cross-session benefits, the updated reactive system also improves things for a single user, as Alice&rsquo;s two outputs show in the recording above. Before, a flush ran the outputs of a session one after another, so a page with several slow <code>async</code> outputs added up their times. Now they all start together and the page updates once all the updates are ready.</p>
<p>In this example, there are three outputs and each one waits for a one-second query. The app then shows how long after the click it was ready:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">asyncio</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">time</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny</span> <span class="kn">import</span> <span class="n">reactive</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny.express</span> <span class="kn">import</span> <span class="nb">input</span><span class="p">,</span> <span class="n">render</span><span class="p">,</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ui</span><span class="o">.</span><span class="n">input_action_button</span><span class="p">(</span><span class="s2">&#34;refresh&#34;</span><span class="p">,</span> <span class="s2">&#34;Refresh dashboard&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@reactive.calc</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">refreshed_at</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="nb">input</span><span class="o">.</span><span class="n">refresh</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">time</span><span class="o">.</span><span class="n">perf_counter</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">query</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">start</span> <span class="o">=</span> <span class="n">refreshed_at</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>  <span class="c1"># Stands in for a one-second database query.</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s2"> ready after </span><span class="si">{</span><span class="n">time</span><span class="o">.</span><span class="n">perf_counter</span><span class="p">()</span> <span class="o">-</span> <span class="n">start</span><span class="si">:</span><span class="s2">.1f</span><span class="si">}</span><span class="s2">s&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@render.text</span>
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">revenue</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">await</span> <span class="n">query</span><span class="p">(</span><span class="s2">&#34;Revenue&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@render.text</span>
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">signups</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">await</span> <span class="n">query</span><span class="p">(</span><span class="s2">&#34;Signups&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@render.text</span>
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">churn</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">await</span> <span class="n">query</span><span class="p">(</span><span class="s2">&#34;Churn&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p><a href="https://shinylive.io/py/editor/#code=NobwRAdghgtgpmAXAAjFADugdOgnmAGlQGMB7CAFzkqVQEsZ1SAnC5KAZ1wmLtIB0IDJq2QUGcQYIBmzUjGQcAFnQi5kwlm2ZwoxcQDdJEWfMUq1WOAA90Ojhw2MtGiOgCuFIjogATOMxE7nRSEMFYqh4UAPp64uTRAEaeFOQAFPxgOrJwyplEmQBKcDnKyL6cSomkUMy&#43;mQCUoYIAAjpxdEZYxFAANsSC-tLI2fZKcL6xFGkNiILIC65RWKO5SjPzizoU7swQYhI4AdLRZO6UARsQoZzcxOUlyACO7gG4adDwKBwUzA3IAFoAHyKX5zfaLH61NgAXhGJTGEymV0W7AA7lA6Gxbjw&#43;FgOL04HB0GkAIz-ZAAYmQAGUKFA-I5VMhpCx2MhyHAARw4GQ-OUoPTEpw4M9XsxcFhNgttrt9tJMiBPnAAL7wqC&#43;dRQaRUZjIEDieBHZgnM4XZgzQGg6GILCk6QqjiZZoQNrUfzMLBUawUQQ4&#43;5DeFGMJwGbg1GyvbozFsF5vDJgYrB16NF1uvwBL02X3QLg8B7DDh0ADmYXQHDD0vhOyjUAxWLF8cyNJLZadYCa1y76Y9WZ9frzAcexCUcsrEJlcBr&#43;zrMcbEoTAGFR3tUxBCKgKLh0AgUGBvRQwCqALpAA" target="_blank" rel="noopener">Open this app in Shinylive</a> to run it in your browser and edit it.</p>
<p>In 1.8, the outputs are ready one at a time, after 1.0, 2.0 and 3.0 seconds, and none of them render until the last one is done at 3.0s. In 1.9, all three are ready after 1.0 second.</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-10-30_shiny-python-concurrent-async/outputs.png" data-fig-alt="Line chart. With Shiny for Python 1.8, the time grows by one second for each output, up to 12 seconds for 12 outputs. With 1.9, it stays at 1 second for any number of outputs." alt="The time until every output is rendered, for a page of async outputs that each take one second." />
<figcaption aria-hidden="true">The time until every output is rendered, for a page of async outputs that each take one second.</figcaption>
</figure>
<h2 id="concurrency-needs-await">Concurrency needs <code>await</code>
</h2>
<div class="callout callout-important" role="note" aria-label="Important">
<div class="callout-header">
<span class="callout-title">Blocking code still blocks every session</span>
</div>
<div class="callout-body">
<p>Shiny runs on a single <a href="https://docs.python.org/3/howto/a-conceptual-overview-of-asyncio.html#event-loop" target="_blank" rel="noopener"><code>asyncio</code> event loop</a>. Code can only run concurrently while other code awaits. A synchronous call such as <code>time.sleep()</code>, a blocking database driver, or a long calculation still stops every session until it returns, in both 1.8 and in 1.9.</p>
</div>
</div>
<p>The recommended way to run slow work is an <a href="https://shiny.posit.co/py/docs/nonblocking.html" target="_blank" rel="noopener">extended task</a>. It runs in the background, outside the session&rsquo;s reactive cycle, so the user can keep using the app while it runs. It also gives you the task&rsquo;s status, a way to cancel it, and a button that shows when the task is busy. Inside the task, call an async library, or run a blocking call in a thread with <a href="https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread" target="_blank" rel="noopener"><code>asyncio.to_thread()</code></a>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nd">@ui.bind_task_button</span><span class="p">(</span><span class="n">button_id</span><span class="o">=</span><span class="s2">&#34;go&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nd">@reactive.extended_task</span>
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">run_query</span><span class="p">(</span><span class="n">filters</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">to_thread</span><span class="p">(</span><span class="n">run_slow_query</span><span class="p">,</span> <span class="n">filters</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@reactive.effect</span>
</span></span><span class="line"><span class="cl"><span class="nd">@reactive.event</span><span class="p">(</span><span class="nb">input</span><span class="o">.</span><span class="n">go</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">_</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">run_query</span><span class="p">(</span><span class="nb">input</span><span class="o">.</span><span class="n">filters</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@render.data_frame</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">results</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">run_query</span><span class="o">.</span><span class="n">result</span><span class="p">()</span></span></span></code></pre></div></div>
<p>For lower-level control, you can await <code>asyncio.to_thread()</code> directly in an output. This keeps the event loop free, so other sessions keep going, but it doesn&rsquo;t have the extended task&rsquo;s features: there is no status, no way to cancel, and the output is part of the session&rsquo;s cycle, so the session sends its other outputs and applies its next input only after the call returns.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nd">@render.data_frame</span>
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">results</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">to_thread</span><span class="p">(</span><span class="n">run_slow_query</span><span class="p">,</span> <span class="nb">input</span><span class="o">.</span><span class="n">filters</span><span class="p">())</span></span></span></code></pre></div></div>
<h2 id="what-stays-the-same">What stays the same
</h2>
<p>Within one session, Shiny keeps the guarantees that make reactive code easy to reason about. These match how Shiny works in R as well:</p>
<ul>
<li><strong>Inputs don&rsquo;t change in the middle of a cycle.</strong> If a user changes an input while the session&rsquo;s effects are still running, Shiny applies the change only when all of them have finished. An effect that pauses at an <code>await</code> sees the same input values when it resumes. Timers from <code>reactive.invalidate_later()</code> wait the same way.</li>
<li><strong>Outputs update together.</strong> A session sends all of its new output values at once, when its effects have finished, so the page never shows a mix of old and new results.</li>
<li><strong>An effect never overlaps itself.</strong> If an effect is invalidated while it runs, the next run starts when the current run ends.</li>
</ul>
<h2 id="changes-you-might-notice">Changes you might notice
</h2>
<div class="callout callout-warning" role="note" aria-label="Warning">
<div class="callout-header">
<span class="callout-title">Draft: review needed</span>
</div>
<div class="callout-body">
<p>@schloerke, please review this section before we publish. It may be more detail than is needed, and I&rsquo;m not 100% certain on the explanations and could use another set of eyes.</p>
</div>
</div>
<p>Most apps won&rsquo;t need any changes, but here are some differences that you might see:</p>
<ul>
<li><strong>Async effects interleave.</strong> Effects in the same session now run in turns at each <code>await</code>, instead of one after another. <code>priority</code> sets the order in which effects <em>start</em>, not the order in which they finish. If one effect depends on another effect&rsquo;s side effect, use a <code>reactive.calc</code> or a <code>reactive.value</code> to make the dependency explicit. (<a href="https://github.com/posit-dev/py-shiny/issues/837" target="_blank" rel="noopener">#837</a>)</li>
<li><strong>Destroying an effect cancels it.</strong> Because Shiny no longer waits for async effects to finish, a session can end, or a module can be destroyed, while one of its effects is paused at an <code>await</code>. So <code>effect.destroy()</code>, the end of a session, and <code>session.destroy()</code> for a module now cancel a run that is still in progress, by raising <code>asyncio.CancelledError</code> at its current <code>await</code>. This stops the effect before it can read values that were destroyed with its module. If an effect needs to clean up, catch <code>asyncio.CancelledError</code> (or use <code>try</code>/<code>finally</code>) and then let the error continue.</li>
<li><strong>Message handlers run in their own tasks.</strong> In 1.8, Shiny ran a <code>session.set_message_handler()</code> handler before it read the next message from the client, and it kept the global lock while the handler ran. So a slow handler delayed the client&rsquo;s other messages, and every other session as well. Now each handler runs in a task of its own, and the session keeps handling messages while it runs.</li>
<li><strong>Setting a value schedules a flush.</strong> Setting a <code>reactive.value</code> now schedules a flush on its own (<a href="https://github.com/posit-dev/py-shiny/issues/21" target="_blank" rel="noopener">#21</a>). So a value set from a download handler updates the outputs that read it right away, even while a streamed download is still sending (<a href="https://github.com/posit-dev/py-shiny/issues/1785" target="_blank" rel="noopener">#1785</a>). The reactive graph is still not thread-safe, so from another thread, use <code>loop.call_soon_threadsafe(value.set, new_value)</code>.</li>
<li><strong>Messages from <code>session.on_flushed()</code> callbacks reach the browser.</strong> A callback that sends a message, for example <code>ui.update_text()</code>, now has its message sent to the client (<a href="https://github.com/posit-dev/py-shiny/issues/1889" target="_blank" rel="noopener">#1889</a>).</li>
<li><strong>OpenTelemetry spans follow the session.</strong> There is now one <code>reactive_update</code> span for each session&rsquo;s cycle, as in Shiny for R. It has the session&rsquo;s <code>session.id</code>, and the session&rsquo;s calc, effect and output spans are its children.</li>
</ul>
<h2 id="reactivelock-is-deprecated"><code>reactive.lock()</code> is deprecated
</h2>
<p>Shiny itself no longer uses <code>reactive.lock()</code>, and in 1.9 it no longer does anything. Calling it emits a <code>ShinyDeprecationWarning</code> and returns a lock that never blocks. The <code>reactive.lock()</code> API will be completely removed in a future version of Shiny for Python.</p>
<p>The usual reason to use it was to change reactive state from a background task, followed by a flush. You can now just set the value directly, and Shiny will schedule the flush for you:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Before</span>
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">with</span> <span class="n">reactive</span><span class="o">.</span><span class="n">lock</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">latest</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">new_value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="n">reactive</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># After</span>
</span></span><span class="line"><span class="cl"><span class="n">latest</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">new_value</span><span class="p">)</span></span></span></code></pre></div></div>
<p>If your code used <code>reactive.lock()</code> to keep two pieces of your own code from running at the same time, create your own <a href="https://docs.python.org/3/library/asyncio-sync.html#asyncio.Lock" target="_blank" rel="noopener"><code>asyncio.Lock</code></a> for that.</p>
<h2 id="new-run_once_when_idle">New: <code>run_once_when_idle()</code>
</h2>
<div class="callout callout-warning" role="note" aria-label="Warning">
<div class="callout-header">
<span class="callout-title">Draft: doc link needs updating</span>
</div>
<div class="callout-body">
<p>This will need a link to the correct <code>run_once_when_idle()</code> docs once those are published.</p>
</div>
</div>
<p>Sometimes a change should wait until a session&rsquo;s running effects have finished, the same way input changes from the browser do. <a href="https://shiny.posit.co/py/api/core/Session.html" target="_blank" rel="noopener"><code>session.run_once_when_idle()</code></a> runs a function once, at the start of the session&rsquo;s next cycle:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">session</span><span class="o">.</span><span class="n">run_once_when_idle</span><span class="p">(</span><span class="k">lambda</span><span class="p">:</span> <span class="n">latest</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">new_value</span><span class="p">))</span></span></span></code></pre></div></div>
<p>The function runs when all of the session&rsquo;s effects have finished and its outputs have been sent. If the session is idle already, it runs on the next pass of the event loop.</p>
<h2 id="try-it-yourself">Try it yourself
</h2>
<div class="callout callout-warning" role="note" aria-label="Warning">
<div class="callout-header">
<span class="callout-title">Draft: demo sources need links</span>
</div>
<div class="callout-body">
</div>
</div>
<p>The demo apps in this post, with the live OpenTelemetry timeline, are in <!-- TODO: link to where the demo source is published -->. Run them with Shiny for Python 1.8 and 1.9 to see the difference on your own machine. To view the spans of your own app, see <a href="https://shiny.posit.co/py/docs/opentelemetry.html" target="_blank" rel="noopener">Shiny&rsquo;s OpenTelemetry docs</a>. <!-- TODO: confirm this URL --></p>
<h2 id="in-closing">In closing
</h2>
<p>This is one of the largest changes to Shiny for Python&rsquo;s internals since its first release, and it brings Python and R to the same reactive model. The <a href="#">Shiny for Python 1.9 release post</a> <!-- TODO: link to the 1.9 release post --> covers the rest of the release. If an app behaves differently after you upgrade, please let us know! As always, if you have questions or feedback, <a href="https://discord.gg/yMGCamUMnS" target="_blank" rel="noopener">join us on Discord</a> or open an issue on <a href="https://github.com/posit-dev/py-shiny/issues/new" target="_blank" rel="noopener">posit-dev/py-shiny</a>. Happy Shiny-ing!</p>
<!-- Halloween easter egg: once the "In closing" heading scrolls into the top quarter
     of the screen (or the page ends first), a moonlit graveyard rises from the bottom
     of the screen. A skeleton climbs up behind the headstone of reactive.lock() and
     waves, jack-o'-lanterns pop out of the ground, and bats cross the moon. It plays
     once per page view, is skipped for readers who prefer reduced motion, and a click
     the close button, or the Escape key dismisses it. -->
<script>
(function () {
  if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;

  const DURATION = 8500;
  const BONE = "#ECE5D3";
  const BONE_EDGE = "#6B6352";
  const NIGHT = "#0E0A19";
  const STONE = "#1B1429";
  const RIM = "rgba(244, 236, 210, 0.16)";

  // The scene is one SVG in a 1600x420 coordinate system, anchored to the bottom
  // center of the viewport, so narrow screens crop the edges and keep the
  // headstone and the jack-o'-lanterns around it.
  const strokes = (paths, width, color) => paths
    .map((d) => `<path d="${d}" fill="none" stroke="${color}" stroke-width="${width}" stroke-linecap="round"/>`)
    .join("");
  const bones = (paths, width) => strokes(paths, width + 3, BONE_EDGE) + strokes(paths, width, BONE);

  // Skeleton, in its own coordinates with the center of the skull at (0, 32).
  let spine = "";
  for (let y = 76; y < 240; y += 10) {
    spine += `<rect x="-4.5" y="${y}" width="9" height="7" rx="2.5" fill="${BONE}" stroke="${BONE_EDGE}"/>`;
  }
  const ribs = [];
  for (let i = 0; i < 6; i++) {
    const y = 104 + i * 15, w = 38 - Math.abs(i - 1.5) * 3;
    ribs.push(`M-4 ${y} C${-w} ${y - 4} ${-w - 2} ${y + 12} -6 ${y + 10}`);
    ribs.push(`M4 ${y} C${w} ${y - 4} ${w + 2} ${y + 12} 6 ${y + 10}`);
  }
  const skeleton = `
    <svg x="732" y="136" width="161.5" height="240" viewBox="-80 -14 190 282" overflow="visible">
      ${spine}
      ${bones(["M-2 92 L-50 99", "M2 92 L50 99", "M-50 101 L-60 168", "M50 101 L80 62", ...ribs], 4.5)}
      <g class="spk-forearm">
        ${bones(["M80 62 L92 8"], 4.5)}
        ${bones(["M88 4 L85 -6", "M92 1 L91 -10", "M96 2 L98 -8", "M98 7 L104 1"], 2.4)}
        <circle cx="92" cy="5" r="5.5" fill="${BONE}" stroke="${BONE_EDGE}" stroke-width="1.5"/>
      </g>
      <circle cx="0" cy="32" r="30" fill="${BONE}" stroke="${BONE_EDGE}" stroke-width="1.5"/>
      <path d="M-26 40 C-24 18 24 18 26 40" fill="none" stroke="rgba(107, 99, 82, 0.25)" stroke-width="6"/>
      <rect x="-17" y="50" width="34" height="22" rx="7" fill="${BONE}" stroke="${BONE_EDGE}" stroke-width="1.5"/>
      <rect x="-15" y="48" width="30" height="6" fill="${BONE}"/>
      <ellipse cx="-11" cy="33" rx="8" ry="9" fill="#120C1E"/>
      <ellipse cx="11" cy="33" rx="8" ry="9" fill="#120C1E"/>
      <circle class="spk-eye" cx="-11" cy="34" r="2.4" fill="#FFB347"/>
      <circle class="spk-eye" cx="11" cy="34" r="2.4" fill="#FFB347"/>
      <path d="M0 43 L-4 51 L4 51 Z" fill="#120C1E"/>
      <g stroke="#120C1E" stroke-width="1.3">
        <line x1="-12" x2="12" y1="62" y2="62"/>
        <line x1="-7" x2="-7" y1="58" y2="66"/>
        <line x1="0" x2="0" y1="58" y2="66"/>
        <line x1="7" x2="7" y1="58" y2="66"/>
      </g>
    </svg>`;

  // Four finger bones hooked over the curved top of the headstone, with the tips
  // hanging down its face.
  const stoneTop = (x) => 270 - 50 * Math.sqrt(1 - ((x - 800) / 65) ** 2);
  const grip = [752, 760, 768, 776].map((x, i) => {
    const y = stoneTop(x), drop = 9 + (i === 0 || i === 3 ? 0 : 3);
    return `M${x - 1} ${y - 5} Q${x + 2} ${y - 2} ${x + 1} ${y + drop * 0.5} L${x} ${y + drop}`;
  });

  // Headstone lettering is cut into the stone: a dark edge above each stroke and
  // a pale face below it.
  const epitaph = (dy, fill) => `
    <g transform="translate(0 ${dy})" fill="${fill}" text-anchor="middle" font-family="spk-epitaph, Georgia, serif" font-weight="600">
      <text x="800" y="266" font-size="11" letter-spacing="3.5">HERE LIES</text>
      <text x="800" y="298" font-size="12.5" letter-spacing="0.5">reactive.lock()</text>
    </g>`;
  const headstone = `
    <path d="M735 350 V270 A65 50 0 0 1 865 270 V350 Z" fill="${STONE}" stroke="${RIM}" stroke-width="2"/>
    <path d="M776 277 H824 M800 273.5 l3 3.5 l-3 3.5 l-3 -3.5 Z" stroke="rgba(220, 212, 190, 0.45)" stroke-width="1" fill="rgba(220, 212, 190, 0.45)"/>
    ${epitaph(-0.9, "rgba(0, 0, 0, 0.75)")}${epitaph(0, "rgba(220, 212, 190, 0.68)")}`;

  const graves = `
    <g fill="${STONE}" stroke="${RIM}" stroke-width="1.5">
      <path d="M402 356 V300 A30 26 0 0 1 462 300 V356 Z" transform="rotate(-7 432 356)"/>
      <path d="M1124 360 V316 H1104 V302 H1124 V282 H1138 V302 H1158 V316 H1138 V360 Z" transform="rotate(5 1131 360)"/>
      <path d="M1196 362 V322 A22 18 0 0 1 1240 322 V362 Z"/>
    </g>`;

  let fence = `<path d="M50 316 H330 M50 340 H330" stroke="${NIGHT}" stroke-width="4"/>`;
  for (let x = 54; x <= 326; x += 17) {
    fence += `<path d="M${x} 356 V306 M${x - 4} 308 L${x} 298 L${x + 4} 308 Z" stroke="${NIGHT}" stroke-width="3" fill="${NIGHT}"/>`;
  }

  const tree = strokes(["M1392 340 C1386 296 1398 252 1380 206"], 18, "#140E22")
    + strokes(["M1383 222 C1352 196 1330 190 1300 162", "M1385 212 C1412 182 1442 174 1472 142", "M1381 206 C1378 176 1392 154 1386 118"], 8, "#140E22")
    + strokes(["M1300 162 C1290 150 1286 140 1290 126", "M1328 188 C1316 176 1312 166 1298 160", "M1472 142 C1482 130 1480 120 1494 110", "M1440 172 C1452 168 1462 172 1474 168", "M1386 118 C1380 106 1386 96 1378 86"], 3.5, "#140E22");

  // Jack-o'-lanterns: [x, ground y, scale, delay in ms]. Each one rises out of a
  // clip at its ground line, so it appears to push up out of the earth.
  const lanterns = [[600, 372, 1, 700], [684, 382, 0.7, 860], [936, 378, 1.15, 1020], [1032, 386, 0.72, 1180], [258, 382, 0.85, 1340], [1300, 384, 0.9, 1500]];
  const face = `
    <path d="M-25 -37 L-9 -37 L-17 -50 Z M9 -37 L25 -37 L17 -50 Z M-4 -29 L4 -29 L0 -35 Z
             M-27 -22 Q0 -6 27 -22 Q24 -9 15 -7 L12 -13 L6 -6 L0 -12 L-6 -6 L-12 -13 L-15 -7 Q-24 -9 -27 -22 Z"/>`;
  const pumpkins = lanterns.map(([x, y, s, delay], i) => `
    <clipPath id="spk-ground-${i}"><rect x="${x - 80}" y="${y - 120}" width="160" height="122"/></clipPath>
    <ellipse class="spk-halo" cx="${x}" cy="${y}" rx="${70 * s}" ry="${16 * s}" fill="url(#spk-halo)" style="animation-delay:${delay + 300}ms"/>
    <g clip-path="url(#spk-ground-${i})">
      <g class="spk-pop" style="animation-delay:${delay}ms">
        <g transform="translate(${x} ${y}) scale(${s})">
          <path d="M-3 -58 C-4 -66 0 -72 8 -75 L10 -71 C4 -68 3 -63 4 -58 Z" fill="#3D4A1F"/>
          <g fill="url(#spk-pumpkin)" stroke="rgba(60, 18, 0, 0.6)" stroke-width="1.2">
            <ellipse cx="-19" cy="-30" rx="24" ry="29"/>
            <ellipse cx="19" cy="-30" rx="24" ry="29"/>
            <ellipse cx="0" cy="-31" rx="21" ry="31"/>
          </g>
          <g class="spk-face" fill="#FFD27A" filter="url(#spk-glow)" style="animation-delay:${-i * 370}ms">${face}</g>
        </g>
      </g>
    </g>`).join("");

  let embers = "";
  lanterns.forEach(([x, y, s, delay], i) => {
    for (let j = 0; j < 3; j++) {
      const ex = x + (j - 1) * 18 * s, dur = 2600 + ((i * 3 + j) * 431) % 1400;
      embers += `<circle class="spk-ember" cx="${ex}" cy="${y - 40 * s}" r="${1.4 + (j % 2)}" fill="#FFB347"
        style="animation-duration:${dur}ms; animation-delay:${delay + 600 + j * 520}ms; --drift:${(j - 1) * 14}px"/>`;
    }
  });

  const scene = `
    <svg class="spk-scene" viewBox="0 0 1600 420" preserveAspectRatio="xMidYMax slice">
      <defs>
        <radialGradient id="spk-pumpkin" cx="0.42" cy="0.38" r="0.75">
          <stop offset="0" stop-color="#E9832F"/><stop offset="0.55" stop-color="#A9440F"/><stop offset="1" stop-color="#4A1704"/>
        </radialGradient>
        <radialGradient id="spk-halo">
          <stop offset="0" stop-color="#FF9A3C" stop-opacity="0.55"/><stop offset="1" stop-color="#FF7A1A" stop-opacity="0"/>
        </radialGradient>
        <linearGradient id="spk-far" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stop-color="#2B2145"/><stop offset="1" stop-color="#171026"/>
        </linearGradient>
        <filter id="spk-ink" x="-5%" y="-5%" width="110%" height="110%">
          <feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="2" seed="7" result="grain"/>
          <feDisplacementMap in="SourceGraphic" in2="grain" scale="2.5"/>
        </filter>
        <filter id="spk-glow" x="-60%" y="-60%" width="220%" height="220%">
          <feGaussianBlur stdDeviation="3.5" result="blur"/>
          <feMerge><feMergeNode in="blur"/><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
      </defs>
      <path d="M0 300 C200 252 400 274 600 244 C800 214 1000 262 1200 232 C1350 210 1500 242 1600 226 V420 H0 Z" fill="url(#spk-far)"/>
      ${tree}
      <g class="spk-climb">${skeleton}</g>
      ${headstone}
      <g class="spk-grip">${bones(grip, 3.4)}</g>
      <path d="M0 352 C250 322 500 348 800 334 C1100 318 1350 348 1600 332 V420 H0 Z" fill="${NIGHT}"/>
      ${graves}${fence}${pumpkins}${embers}
    </svg>`;

  const bat = `
    <svg viewBox="0 0 100 40"><g class="spk-wings">
      <path d="M50 15 C48 11 46 10 44 11 C40 3 26 -1 2 9 C14 11 20 15 22 23 C26 19 32 19 35 25 C38 21 43 22 45 27 L50 33 L55 27 C57 22 62 21 65 25 C68 19 74 19 78 23 C80 15 86 11 98 9 C74 -1 60 3 56 11 C54 10 52 11 50 15 Z M46 12 L45 6 L49 11 Z M54 12 L55 6 L51 11 Z"/>
    </g></svg>`;
  // [top in vh, width in px, crossing time, delay, bob time]; smaller bats are farther away.
  const bats = [[14, 90, 4200, 1100, 900], [24, 62, 5200, 1500, 1100], [9, 44, 6000, 1900, 1300], [30, 120, 3600, 2500, 800], [18, 36, 6400, 2900, 1500]]
    .map(([top, width, cross, delay, bob]) => `
      <div class="spk-bat" style="top:${top}vh; width:clamp(${Math.round(width * 0.55)}px, ${width / 13}vw, ${width}px);
        animation-duration:${cross}ms; animation-delay:${delay}ms; opacity:${0.55 + width / 270}">
        <div class="spk-bob" style="animation-duration:${bob}ms">${bat}</div>
      </div>`).join("");

  const boo = [..."Boo!"]
    .map((ch, i) => `<span style="--tilt:${[-4, 3, -2, 6][i]}deg; animation-delay:${1700 + i * 160}ms">${ch}</span>`).join("");

  // Cinzel and Cinzel Decorative by Natanael Gama and The Cinzel Project Authors,
  // SIL Open Font License 1.1 (https://openfontlicense.org), subset to the letters
  // used here and renamed as the license requires for modified versions.
  const font = (family, weight, data) => `
    @font-face { font-family: ${family}; font-weight: ${weight}; src: url(data:font/woff2;base64,${data}) format("woff2"); }`;
  const css = `${font("spk-epitaph", 600, "d09GMgABAAAAAB0gABAAAAAATJgAABzAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGnwbmiochRYGYD9TVEFUHACBUBEICus011kBNgIkA4I4C4EeAAQgBYNGByAMBxsHQVUHYtg4AGD5jEDy/6cDboiI9EHttgtFUXoyZTUlBiy3jHI7nrqlV0/dPpjHzWvXRjhOcIzfl3lRNxZwOV7hOHx073P+MBxSMZ/Cb/b79cUvdm9Ex8R8igJKXG9HH+qBkjI9U7bzqUu6pMS88CcjJJnlefrlu/O2ZfPzJrP/s6G0JkstTsViDIPQSAZhERqLMAz6LupKSZYdx3HCx/hEPLc8Ak1Lt6lHNGdzd5DkNoZHgAZKEqBogllCTCFKoTRGUKvb/9f+HapiLyL0xaE8T97e35h2m6VxUtYsCDwwjutwzia03Gyrx/BfAkxuYJ2FVhzK4isNgN6n0TClhZTcT0k9RwH8H7lZO5FmhlwVeBGtVIuKJXFyQox3YVu2PXP/9sT/XLPEboQpoAMGvxvVqUTt6zp1THd/X8pc/e9tpu2+XenQtJJDIDuIvZnLEPS0+nt7q9U/abU6EgZYI8tARwbRjFcYAu6MhFWAKo+rEBQNTa/y0mXSVWmxiuvCdRNbVsuGkehNKpIbWb97v59ZYHsqKW45w8GIE5zFERsS37v/+w+jABWARgIJSFKQ5kIeCwUiKJGHiipQTR1qakEdXWjCFDRjBpo1i8zZgLbtQHsqYY5oYM4YYBBY6JpHQ220A2t6+zqAhQEJBMrZ0NMBNKAABSMaMf7BjDEwuWY3xyJtJvIIarNog3teTrwIJuKy7kSmgN/e0sbe4T4NHb2exCjFrD5jgIY+obPPF5dDG7X6AJa7Vn9v44VccpNmMdexcZBclx0gKwhyev18D5DjNXYD0q/ShnqPYE1n19FOcf3DbzQCWnm6s6t74eRpd/MMJvUG1jycX3gth9vu9R1i9pIejAUXF6Tlli7yaep/7Z2fHT1MQSDI0R+PNAJ7IqkdqKMjxYbVxEqC3n55NMSdB3GheKeIDdGT0TQQnlH476VTEdnPjLG8KHvcmHRDBSFB+MZ7Aeyh2S3Jaq+ojqeQtkNR7DKCQk9V12b+w1BPcV0u7B4zYkyEtAD5uwsiFpJ+F28acwbNboCOpaVPZjNRg93/zOElp/Yg20zv2z1+HA/qSHijw5gQvLvp4w1P7yK8F5kkAUcjicrSRKsb5c++4h06L1aIJrqKiiv78mtakM44hbAgQrIE9KASokzLS27eScd6HlF4bPQSL6OmB/jx7gxLuEMolobdCxaxYY3fR4Y5aFxvKCBQmTRAkctm4pXtZz+SJR88r8vyJB98y4NGnJurFAehHDWsqO4iHXLzYdndwfBE3+4JhZ5D6g0m1fWYCZ6W6XwNtEPUuT06yHLZbNQZSQh0aZmIdMqYu+3q/2VHFbfRLChskWMxJ4RwWAGlI2Qz3ShkoGH158KauGrMAEWovZlDLaMaBqugNO44Bj9fKHPmTLv14ZcexWtpp50lsp2CjZpHt9ZTJRK3U1VdIAD56BaGtKsR2jAdn7KjMHYvwxqBa3S6pmSQhV4LgJiIOn3jZSymdw35//XzYuTGlU+MUiACy5qgtQuhD1f4mfaNySo2UpBHJhIxU4ILBewG6h7+efB2c/oXgM78tF7wTwHy7i7n82kSanLRahnu5gYDeaLuByxo+f3zaZJ2HMmbJrgdgXAR1s0NO/pr5RuKiwZyMvoAgaMtVAITJGKKxDtyQOUGMB7wjlRae1U1evnEhrIgt/LqMVzhqLA/DBUGVa7EKgJGUEHZPD6zWCYSEDLuwOrcSv6HNUSWr+p0yNqYP17ymmG+kqPh5HMqnPIuxYTS+mtuKYaP4j3UTMlWkYCXv5OyGJ18YYZvmXN7nq3UGiu+w30OJDdA5cY+fSthsdL7MYw2SHyK7HgjjGAs3eQmZkP3acCQR1eVUv+HCMdOo5olgVovKuJAXeCkvq4JM3o2bBjbsi/NIQOJKyZFELpClZEGABzjGwmhnoaEMaGgyCpTphDSF2hLCdNaqmbU1IF9ADDKYUNqZo+LjUVc4ISmPGOSx1MCGox3Z/TkLKPbH9YKlRgJTULhLPKfB5hTlVcAZsq9ThGy+oA3DkhHqMCuwCXKuHosiRkpyJaa/uxi8jhSDI7QJttEVQLM9mq7oRf5RNUqt6dWG1gDJWN5g5KTSwSmdm9bH4iPtuZGSJ+d/jaQXr3ZEqukltr3XAPFEDuCBgPGczxThgV1oNElDSXjl5j01XanPT1pbgEDSj6HKQCAdy3ZLB36pNSESVOmzdiwlU9Su6AU1InSAv5UlAKAy/si77DMaClD+RDjFOUAM0Vgry5po0FK555QCW2y475CASXuVQJm0PIhKT26JixZsY4cq0DFM4Mqq07hNBfF9Mw7lti12vAo4dQJOdDo2/ZkbTtFEciUNfVNmkWEeYQtItoy4lpFfGtIALEDOEuyEkV1XXPIlAVk2hKybQXZsY4csWdw1gwuf7ohFDSPrQiev5KygTSTVJ0F+g8OegnnOexvvf/hwSMvfXp9L69+wGEEeekdUwWbu7GpD8PzO2idM7FZvuw/cFYTM1pGpu1RUNOxsnOp4eUX1KxLjz6Dpv6Bw4b6p1bRMqjmrNygSatioLcnyf7DvW4nfyWxW81SsZDPZTPplDOmfqzOOKHAJASKa/AN5Q+ra+zcxi3cAPSQx/DIRFM0B5EkojHF0fSz5/dARlxd4QxcjtRcrJNNqWy5PDunOyyM3Zjwi10RsHqh0/4LDQdtVtAOZcxFujIGI9hHa8r+gcpAF3GNJ1ka72rSblJF0QH9xhf9PIxWmGcRNkHAhl3DqBQG8ZCgylomSa3UqizaT64H3kNwRKtsltXw1iuDHymZ8CULKvfV464H2kUw9eae+T3rgfdzVUEe66Os8QdZTSRvSHpJhMofZJoUd43HUVjWulJhrkhT+sw4dQsXtp7t5oKAY05ZnxptJnx1ws5hMly5byNiCIIEWbltHke74jjQcCDCFCikukIIEpgCJLlPvHcmbQ/gApSQKM9xD5o7E3OQqUtQ0vShP3dNgRhDyDAFLB0fr8xuw8WVLkII9g4PirQIMx5DZqssgj56R0ysdlEQAARmsIAiRgAa1MXeJajKIUESw29PrMAxiD6s3Ts/EHMJcO2DOSElAaRIRgVQAuGMRwJ2/mSsLuMcQ1ddvwfK2WXsF5ApeZEJMXRzn1FzQARSIPeUJLWAS2Am2YZikiOlsIjcoC/feFbQgwfRKKoCoKOXzpI5f8H/ECbxOJo5Pf8ugdGQwysNYvgoSYFYmV/413g1BFmAHJde0jEBkOQJ9AvM3/icex2g4aenNQISQ9MvtCUymTOT5rafCxhKUFxII9pK8TZZCIXjRO+J/5sTFUftmVAcqqlhKFGlhmzgVyWj12obrGXM0s7a5UwRQxe/I+Ws9kurds+DfB/oqA7gj9ppl90TTzrvZXFbJqelqqNAXMJR6VziU8jxc2bZPVD9bDlCjcjsbgAX4j1jGrsbQOkHEWwf+otZAbxhGr4DcF0kRk1/no3ajAURypQ1oRfmKaEbNmVLd7W1QbPmH5FxHeWOIr2+DO3gAgzdn0kR3eRxcWoUk6kGzcniK8BC9ae6C5ChvkctD3ozFL4Vqzh0lwK1V1tSRnX7pAJZNLcx4VU6Be0cRsOVfwNGmnWEiCU1lK8yGgH1jjqGPpEgbigP1PlwMuYdMDm1rUAdiUX67zGCtZCnTexSLMgXpoS/aoBBb4RUCsIY3d+iM2TVP7p5OYZJJCiDqQs/H5x5CYYAU3Nx3RoHYeGgwrx5JtJjqPbrbmWs6goQ2dYRnFVICg3VXrISjnGNLEvRyBokAekyfECJ+PWCICxr6f7cRXFmT0tPg0ECnDJ3HI/GcKNg+A4nVR70Eh6ouomXu+ZbIpJox+7YXTA7wsmAHJAzdUKzL0OQ4EKsk1AEYZf+TbWeu9T1N7YlAmirMNO0hRvq2enx83QzLIevggC4UN5ExW6/B0JDskZQPJNwUpBIjRGRZsco7NQPKkH0EfuFxa6W6aREi5mY7hBuHrv5lvZWD9zt82vYLalXYULEKFso15p3AHddEB5Jhzm2gUaUwjfbsSdn0nd173ywRAVjD67smgfWILEdWHraDExyf2mZu2xMwYULzTm0B+o/rNF9+PbKeKQPqa2rbkK+fbQe4w+9X7usQK3gGj/H4Re+R6GM5GvBzD9axWEwKKVaVq0yvGYQwyWfM2+flNE0b2Sf+3k3ddgHrWq4KWj3STcF/Ku2OmoP7rC4QU4akwDJWUIgKh101xIszftVxpLPQElC7Jn2WBpShOwfA7E5QCdnzSe9sfJENkGTboTBtIT0DzOt5gQY6epFXqnPPDueJnikAKTry+JM0GsL07gT+e37yeecz+3iN6aK9EWdTK8TpMpkpyek4wlVI9QHZ84pTJ0pK9Rbu2Sj9PelUR1iv7dV/2tYMmYStsP1YQKstx8DdcRRN9VRmyHoo7ml0iHWmq6P1HD/ux2goWNe9sSFHa+B+e0YkTVhfmV3FCWvdC8P/c/Jte/hiJ4vF5f3f0j9SEyz6WzHGvr5f3BD+UtX8PvE2dH3ysrX7hH6VANnZnysRe3rmnc+w5/buRECDJ8tmXrQe5YbKIFzSHVylDaIRM4KZ5bysscYPx820ptZoAuCyE4cGivnw9abZ/mcN08cSd527tn4p0/tmy8fPiyMR3N3CpetRTHXnj1zvbLWuDpm1dfsjqfT730BLsJq7jxJNlo/q4g+/xi7+9nTcufZxZ2BvhxUbHS+uUOU9jEDy2NJlE8rAXK+qpYTdC7Lk95+pmYVNEkWd8EXpn6JF4IEOMeg4J3H9x8/zuccefbo2bPYyXSHM+8zrXum2GHLGLcE9DWOwv/N0IEMEvux8vmefrnQtzDH8USyT8BhRo8ShyNi+OkuRb7/Tsm1tU+Tt33Z+/YsBokeIiCBw3heXBDS9LdXiVkj71IPkzMfiTPEqv0rn1sO0AAJEBl5H+6cwbYGDeVtQl0v7HN5NnZ3Tt64zz3VPH38Nup6kls/y+moHjDLVvr95at7rEbM0CFV9zN98Kd1etrpeWsp8clfKcVfi6MsydHJVaOSM6fyc3RCifJ/71InogveFODVg04b/umbAlT4rAmxxCNduDcvfiqKCX199hydFQET+EoSTyVIJR+YgKzgeXUWy7fyVa+1i9yLmMLbIqI2u7fnPIWBmeE+rOApr8/nRnGvuE1kIvR60jBGri/1XLLz9xnDbWQ9hry9jNrfMXgP/AKG3mHGpInff4pU9IcrhmqF376fLdu2CfyBNE9vROA1ChmSuDRfj+0fzRD5wdWHvTA6pm2DEd4huknVa4G59VN9CGVa081mrKDW88IgIpyCxpmz6bpOhLEhdmXZPQzHw1rC2GRZEW8linfKemS+G0V5GVuSuO0Xa28szPnPdkc1J8Y0JxbYGOpsLaQWlZHs8IlhOA7dN1v8qK/FR2FqbAgiMF4Or7NMJhYByKZpm98xBao6zflIDTNZemGNHMiOGaYz2+0CT0oSmlO+K3FyPJ97AUdu9O52T9rU2cmMdfb27bokdhU6YOiwlK4KBIghuUF+Z8MPM9MtwewcBw0TWmR5Vq4LLUGw4WjI8NbSLBbIFeZyB1RWMglO4dubQhj5yzD8MLMwwxr90Xi1JF3k0BXatsNfC1+4/H6NbGUssuVao09Z3fahZZj4f1sVwq3313v3v80tYeEquSYxYaWAwBSVzHuJ3G4DdEAWoN+IzFG/1kudhXK0JcXFyhLEZR06//IykVGA185qbRcZGf+2bdwP15LS1GprpueBXp47biTy8JKPxPaMrCyzM+MHMqQpGuYdPzHtRDchbhW3jG9eyUx+4ZknN27KN8wVB7fzk/Um4XkxYK5szluzODp6KoLOcMW1NZ8kZ64LbcK8KmSN/PivUfLJwjOfRPU0Lx0T9/wIrIR+ZWnfg4wji7f/NvXgkw9YdXfYcGAjSXLafu6Y8y5V/PB7ycU1p5ywZh6XjPHt8UXIZ66LItFdBDTg1K0/vvh8KRQVvWTOnbzR3Rb1wyLPlMpplQlTtH+teO9wycwZlSxgAPoOsQT+rzvxFfhTWwILZSFgQkxTWYVIZWD6wSZNA8aug7IjoWn5xfsrV+86E05DVxDQIPrkx9gEPPhhiPr7v723/lgmm4eFhDB++ZKPT0YBDRAN0cLPwIJ/xapqbW4gvqIVhnT25T6fbfmQvr1yMHYR9XUap7ZPbzUEFbnTbfa8GQGFCtMGM0oDpBU+D7n73S3SL6mi+xzRA9EYX/RClLsnbcfW36dlCaeeTew0IzIu+lNc324y4ILoOLJgTz2dLpwuIlwSfisZL0e+pA4zAiZwxkKx93DKaAGYgMzW21HxRY9E8PEomOhhmTWYvO8MMeE+euRhTZOow3J0Vgz+HsFhFrc0cfHQsSIwYMk5UzvBPeDeHissEzb9X6FGQMc4YzG9RAyLA3RAat5tAX7AYF/OKAfLWcfdFeP+HNpDhI4VLGZrQOgYbhjdl/ITF8zwjRMdPidMLibwymMihTL5QMeQiTc+Igl04GdQjsAp42EMmPtQ2/JjOm/peVKGNhiNNcJVnjXmNqiQkpm259IVFDIkXkhoYe+7ESR5/S13vxtzO0CEcVihPUQMi4uRqNNIvBJOYXIgg8DumqW/U0V5C9LvSP47LWRf3f1eheZncYaA/blo4EyAVgNHgc2KJsfpuRR5AAELeHVFw/T/dllkiYQrpcXbysPK6wMJj9PWo+OFp2sCc/Lfst1QJkUV1XGBDaihSUKRlJAszwfgaagIJdsqKJTTZXoEYcD5P6YllBEN4YD0ZY0pzQO/tDzPIeVQzfxpvMoCZMv074KlZQYE4RDD2C/zf76XMD9gnJ02qUrao9UWxjKtYBwp66CuVwaT3K8onGF3FE4nQU5HKJjusBfMmAXwNBQ0NkyQGth8qd2eESSYJHQ3bKzZlJ7OoKnk5TIIYEbjpnSYZAweVsDoGGo/0s/ceu0hTfBBqCfdGL6VmS4SqEf+WJz0VOgbF1M0lhGDKvN1kanmloYpuak/zU4gsexPPT+43kxOZmF1vOlh5/eHgkSK3ANunNthbHP4Zzcn+CkGpwMyNleixEcDGge/3THocBGiIlFhYj2dgS+TdZQjRh3WmMUVKpJu7g3hK6AV918+IVuWxED10p6Cw5EtJCgYgvoV566mvR7i/3D01bc/R9z+fZh/ULB7TwQVXUZAg6iTv0xd8svO2CfClnRG1bxXVqXcvNHNdB/bD/v2UUlO8F0058IO0dVHt9e8svl3WPkKhYype+PFxIqWcBo6iYAK/DPLfDAH3hqKtQawJN+CQ85T6mDUx7VmA30QMYzqLyef2MLZKI8CBqAniBk7+siJLcG/ej+OgTgI6BCdmP9jhQZh9JiV9XWtHTeLylfyYxX82JxYfi7tRlL8eXbxnLBiaCybddD+6LGqXTQMXqwpeWfllCH3V167qi9pCWhBJ1ygavZr/of4R64fLdMyoxv0oBUu2WH2LE/cQ18MMWUnNIFXPKxsh9Q9nBHOB8A9AF6bVm7TjfH/NinDp2+dLS4SO5/KBEGiSlShya6VLPUJJI2l9QZl7bg68scFobP1HatTOgdNTwT2CZbFxggJSOXzKN60V+sCC6U231Beh7d4vsVaurDd+2buJGk9q4p/n7qeFJgsQVGBrCnTaE/u19Rr1y52TgLPWpttI9aT2BCXBzw5eVQpHhMfsCQfTF6TeO4nVf64nFhKVEG1moXkxKdKT43GEX+qVovF1Z5diz144bheAejYeA0OZIYZ01wa9uNU1tIgVTVhYEA7Xs1VBWomxH3iepHOgjCE8e0kaMFbJHDvropLTGUsc9NUqeDFtARfGQuXUAlt5NmJPEtpzxEJhbP0DYY+cbOD34WvWpcxgZK9O2x2K9tLkZOyIVTrIDQYCjiGs9MHsfMCBOBhVhfcTjHjEWNSy/ufIhX6fMVLtbnf3syWHVsc0edH1cTP73a6KXfzO7otpqocV+wIojTTuoVT3OX6eFO+lnD1ErVW8miGrI5j/wMNf1quIvrhx/T7T79sym9ndYa1i5qzPZI1XzUnjSAcDVK7r73TVnBGtjeN35ezWjCdGgzfRnzsZNaWYLXgc4ZoyKVeg+is9ao6n3AIVzdeXhwDWaadSrdFKdLEKmmhpMaWnXhXazTU/p0n6wrOK7ZgKN7CshEOjiO7yuD4YnuWbOy1B38sXECE9Txcn0jzxhThcYKNoscpUJDu7TjeMTj8ELDc4z1lTyAK3yMPxAvGaVMo70+ms2g7ThFeDV4dYqNA56Z4SIC1i/zZPd9i2+dpiSEstovs6pvU+ph1DFKqwpnYUXAgwqeyt6/IEqYiKEeHyEpOF7u2WGFt+vRs5t4QctJq0+iqylhFfnZewEivTG2XmVzJXqQJn8NpNmnmsJ2lc213gPEo5eVmYlK6VUWYZ2BfHVen2yP+LdrRTNRNsFRGljZiDHmlMdcVOVb+TGNDkWseQqRIGJISx3pCnra1N+FrG+5IxEIRr/+GUHj5cogz3ezOUxK9HbHeYr07tUdpL1vS5Xs7d1LdO7ndvvLFNmvR/BaPluvuyPJE9xZ36Yfn2R2OudW61xvkCb0V7rqsC3JDapqMZeiDv6nd09bhDp0VMXk2NEggBZ+JrNbrf71h2yUSWxyTq7t2qJKVdCApxTy/v/Cy4upHPF0YXuOzmskvKce2zs1FvE9G/B6cF2hkudgwV2tvKFPbjk47OtBG8FSxDGS5mwi8FPzCz/XwgwWiOcrg6S+ejAIJ9Vp9S5pa05Ils0YG+M2zFrVQ+3I8nuLKQmtqmgqvSq9+vnvaSLI9LTPT5Mwsixiv3rqWMgsR+uLiQmVxHPbLrXR4/beQT744XyNhW/cvENKNQoUppaDol5E6MMbDGBiyPo8j4b0rVhcoDPm+2Efo0sTsCv0E8y+sl1saWvh289uf1jSkNUk0bbvDxQdSVG2KNHPYTdKjSng1XZGo2PVLF8mqY9WukoznzBmf5Plttv4sMwU9rJPKTWnuG2cq1k38Wr69psK/vNUZvl9lJGD/p6gjIY9RH2PBSSytGWf+OCX6RXVqVZ00AKw0T+FEtdaZko1kGom8uiA4oY0b4OjWP+2XssdKs4stdZUle4g6iYLBi5fNFAoMbipKLPeufS0w+ramlkugG+CsjoI3jvMBgTpV4DJQHqytuI+nqwhfR4MvbBnylzf95t7zbZxU0l0lafuFLutzrX4ZAUTDj8c/pCa79L8miG8Bv+vruYC/f31HGeu6wqEQ94FBAwr/Jm3qTzpSLrPuAQrZu1fY78x764XaEzaQrZzsmXKu5zPVx4RzMDvYjYiQNC+FDviFx5j8BLkuI/LDdy8PBLgsxIMMzJRq5In4SbpaPngVYxMHs0CjBB32jganrVgtvSVMa4XRLApZVz87s+8P3IH7kHrLecOWU+dfoPnCb15+RQQuY9YpWCvwX4X0AgyxUA174QiRLW0mgRl2E292coerXE8x1Q+ETb45dTrtasrZRoafUKFbmPMCqU74bkjvvXe2v00xl5cE/PPpYwLDK3hNxk55asev1WJhGwilI4Hv4PLQYj/DhzY2/ZK3ZfJOHNrby3UYFJ8fCwMz/x+KwfkLmzocaildlgOdukzVo1mjJn0SzJv9tDY9DqGh/QPNOkwjoS2d4FaPTi2w38S6p9/2rUmnHr0SDKGsfbq4ZLEZMz5p1vr0+/lkeqdTuyzXlHRsFCycVKplOdOketDVt06tpkpwpGsjeO3YNKnWoFG/Nl49nBoQuJt16vCDnjYbMgvjbw9K7EfOwNWn2QALxgcehHP7efUSgIpvk+CnLl1KL3dpIC6boM4u30h1+DMvFv7glwAAAA==")}${font("spk-title", 900, "d09GMgABAAAAAANUAA8AAAAABhAAAAL/AAEAgwAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbgRQcMAZgAEQRCAqCEIICATYCJAMQCwoABCAFgzwHIAwHG/QEQK4GY2Ml9pspIpEKhDpnv9fYYf6/IKK6KrK6V70HYtbgia+bPbwB/GR8xbmY28QamndPMstioWme/cR+YlASpZMyDR6+na/qgkgiyQaeDjaiMY6G0n5XJsPf6/9C+9pFCbzu5oeyhI6Qef48sS5y4MiatyTxQGphEFIgkKXb1EcvYWBp1aJtENANAChCaIQAwxTetBkZn07/zCrq6N96eyN9OsACAXJVtDaiAAFCp4BiGEN0owPR5+xe2r6JMowOxjFRNNCPKzCAUBSNiauQYmWbOIKqVAJtLe/TFmMF9ahsGOJGVVlskgnAACNWhIwjK5GjgaZy838XtE/aYRCAetVRYBQFFNBAPxoB/eicgS1bCowWigLxe5aXggAAEBLoCUAHYAusQTKGegwA/QCAoqhmO6K3vW1eupGZa/90ye0Fd8y35pd0aQuGDpyfYbXyuGFId3PGMeZvt48z25vvzU++myv3T0fqDx7IgvuGmHcNXVdnvneP+f+u+4sePDM/WHR/yd2Fr2tNiyNNi/un/kKvxIXHhse5xzKf1qqU4Q4rDs/M626kDu5uuO9Mit/hF/IdN120Cm8pWL+4OG/rtxEq+EVZWQ7mSsgoiAu0639eO8spFxzOKNvcsuvZycVPT2Y+PVm55UDllkl5NJyB5ZbUV3WMMfFZK1WXESAW/c7PKSvq6f/N1l77CHCVyO0A7/9k/X/3b24m7RZgSwEQ/JK36n29OxMIzKD7jQwc8hfL2mLGEKArJAFQQAgIxgAANnLxpNoBexlmKAbZi0oP56XWkbqilen+r7I2IMVzQRIvGY/DVQA7mFFcOXPhapREuoIupkMhEEUX0YFJasRs5Chm8eRMpIIyyLJQhkTGTEaZxlNwgVQohzIVZAGRSKy48/4RBCYK44m9odBQmgQhgReQzlMIoSvkKIV0mSeUyXlIDLhw5MyZKz/JboAz5zqvRgrmDqqcO/Lgye+1IYCrSraFeh7DRnKuXLhx5cFPsGM8ACzDFeKDrKX9+lIA")}
    #spooky-egg { position: fixed; inset: 0; z-index: 9999; pointer-events: none; overflow: hidden; transition: opacity 0.8s ease; }
    #spooky-egg.spk-leaving { opacity: 0; }
    .spk-art { position: absolute; inset: 0; }
    .spk-close {
      position: absolute; top: 16px; right: 16px; width: 44px; height: 44px; padding: 0;
      display: grid; place-items: center; border-radius: 50%; cursor: pointer; pointer-events: auto;
      border: 2px solid #EDE4CC; background: rgba(14, 10, 25, 0.7); color: #EDE4CC;
      animation: spk-fade 0.6s 0.4s ease both; transition: background-color 0.15s, color 0.15s, border-color 0.15s;
    }
    .spk-close svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 2.5; stroke-linecap: round; }
    .spk-close:hover { background: #D2601A; border-color: #D2601A; color: #FFF8EA; }
    .spk-close:focus-visible { outline: 3px solid #D2601A; outline-offset: 3px; }
    #spooky-egg.spk-leaving .spk-scene { transform: translateY(12%); transition: transform 0.8s ease-in; }
    .spk-veil {
      position: absolute; inset: 0;
      background: radial-gradient(ellipse 120% 90% at 50% 30%, rgba(24, 17, 48, 0.8), rgba(11, 7, 25, 0.92) 60%, rgba(6, 4, 16, 0.97));
      -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
      animation: spk-fade 0.9s ease both;
    }
    .spk-moon {
      position: absolute; top: clamp(40px, 9vh, 100px); right: clamp(24px, 11vw, 220px);
      width: clamp(68px, 10vw, 140px); aspect-ratio: 1; border-radius: 50%;
      background:
        radial-gradient(circle at 63% 30%, rgba(160, 140, 100, 0.32) 0 8%, transparent 9%),
        radial-gradient(circle at 34% 60%, rgba(160, 140, 100, 0.28) 0 12%, transparent 13%),
        radial-gradient(circle at 70% 70%, rgba(160, 140, 100, 0.22) 0 6%, transparent 7%),
        radial-gradient(circle at 38% 34%, #FFFAEE, #EFE2BD 55%, #CDB98A);
      box-shadow: 0 0 40px 8px rgba(255, 240, 200, 0.32), 0 0 160px 50px rgba(255, 214, 150, 0.16);
      animation: spk-moon 1.8s 0.3s cubic-bezier(0.2, 0.8, 0.2, 1) both;
    }
    .spk-scene {
      position: absolute; left: 0; bottom: 0; width: 100%; height: clamp(220px, 44vh, 440px);
      animation: spk-rise 1.1s cubic-bezier(0.16, 0.84, 0.24, 1) backwards;
    }
    .spk-fog {
      position: absolute; left: -25%; width: 150%; bottom: 0; height: 30vh; filter: blur(8px);
      background:
        radial-gradient(ellipse 22% 40% at 18% 72%, rgba(196, 186, 232, 0.2), transparent 70%),
        radial-gradient(ellipse 28% 36% at 52% 82%, rgba(196, 186, 232, 0.17), transparent 70%),
        radial-gradient(ellipse 20% 34% at 84% 76%, rgba(196, 186, 232, 0.15), transparent 70%);
      animation: spk-drift 12s linear both, spk-fade 2s 0.5s ease both;
    }
    .spk-fog.spk-back { bottom: 8vh; animation-direction: reverse, normal; opacity: 0.7; }
    .spk-climb { animation: spk-climb 1.3s 1s cubic-bezier(0.3, 1.25, 0.5, 1) backwards; }
    .spk-forearm { transform-origin: 80px 62px; animation: spk-wave 0.55s ease-in-out 2.2s 10 alternate; }
    .spk-grip { animation: spk-fade 0.4s 2s ease both; }
    .spk-eye { animation: spk-candle 1.4s ease-in-out infinite alternate; }
    .spk-pop { animation: spk-pop 0.75s cubic-bezier(0.3, 1.5, 0.5, 1) backwards; }
    .spk-halo { animation: spk-fade 0.6s ease both, spk-candle 1.7s ease-in-out infinite alternate; }
    .spk-face { animation: spk-candle 1.3s ease-in-out infinite alternate; }
    .spk-ember { opacity: 0; animation: spk-ember ease-out infinite; }
    .spk-bat { position: absolute; left: 0; color: #120C1E; animation: spk-fly linear both; }
    .spk-bat svg { display: block; width: 100%; height: auto; fill: currentColor; }
    .spk-bob { animation: spk-bob ease-in-out infinite alternate; }
    .spk-wings { transform-origin: 50px 18px; animation: spk-flap 0.15s ease-in-out infinite alternate; }
    .spk-title {
      position: absolute; left: 50%; top: clamp(60px, 14vh, 160px); transform: translateX(-50%);
      width: min(92vw, 720px); text-align: center;
    }
    .spk-boo {
      display: block; font-family: spk-title, Georgia, serif; font-weight: 900; filter: url(#spk-ink);
      font-size: clamp(72px, 12vw, 150px); line-height: 1; color: #EDE4CC;
      text-shadow: 0.04em 0.035em 0 #D2601A;
    }
    .spk-boo span {
      display: inline-block; transform: rotate(var(--tilt));
      animation: spk-stamp 0.42s cubic-bezier(0.5, 0, 0.75, 0) backwards;
    }
    .spk-sub {
      display: flex; align-items: center; justify-content: center; gap: 0.8em; margin-top: 0.7em;
      font-family: spk-epitaph, Georgia, serif; font-weight: 600; font-size: clamp(14px, 1.9vw, 21px);
      letter-spacing: 0.14em; color: #EDE4CC; animation: spk-fade 0.8s 2.5s ease both;
    }
    .spk-sub::before, .spk-sub::after {
      content: ""; flex: 0 1 64px; height: 5px;
      border-top: 1.5px solid #D2601A; border-bottom: 1.5px solid #D2601A;
    }
    @media (max-width: 600px) {
      .spk-sub::before, .spk-sub::after { display: none; }
      .spk-sub { text-wrap: balance; }
      .spk-moon { top: 20px; right: 76px; width: 56px; }
      .spk-title { top: 17vh; }
    }
    @keyframes spk-fade { from { opacity: 0; } }
    @keyframes spk-rise { from { transform: translateY(100%); } }
    @keyframes spk-moon { from { opacity: 0; transform: translateY(40px) scale(0.92); } }
    @keyframes spk-climb { from { transform: translateY(200px); } }
    @keyframes spk-pop { from { transform: translateY(95px); } }
    @keyframes spk-wave { from { transform: rotate(-20deg); } to { transform: rotate(14deg); } }
    @keyframes spk-candle { 0% { opacity: 1; } 35% { opacity: 0.78; } 55% { opacity: 0.95; } 80% { opacity: 0.7; } 100% { opacity: 0.92; } }
    @keyframes spk-ember {
      0% { opacity: 0; transform: translate(0, 0); } 15% { opacity: 0.9; }
      100% { opacity: 0; transform: translate(var(--drift), -150px); }
    }
    @keyframes spk-drift { to { transform: translateX(8%); } }
    @keyframes spk-fly { from { transform: translateX(-15vw); } to { transform: translateX(115vw); } }
    @keyframes spk-bob { from { transform: translateY(-2.5vh) rotate(-4deg); } to { transform: translateY(2.5vh) rotate(5deg); } }
    @keyframes spk-flap { to { transform: scaleY(0.3); } }
    @keyframes spk-stamp { from { opacity: 0; transform: rotate(calc(var(--tilt) * -2)) scale(1.6); } }`;

  function play() {
    const style = document.createElement("style");
    style.textContent = css;
    const egg = document.createElement("div");
    egg.id = "spooky-egg";
    egg.innerHTML = `
      <div class="spk-art" aria-hidden="true">
      <div class="spk-veil"></div>
      <div class="spk-moon"></div>
      ${bats}
      <div class="spk-fog spk-back"></div>
      ${scene}
      <div class="spk-fog"></div>
      <div class="spk-title">
        <span class="spk-boo">${boo}</span>
        <span class="spk-sub">Happy Halloween from the Shiny team!</span>
      </div>
      </div>
      <button type="button" class="spk-close" aria-label="Close the Halloween animation">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 7 L17 17 M17 7 L7 17"/></svg>
      </button>`;
    document.head.appendChild(style);
    document.body.appendChild(egg);

    let done = false;
    function finish() {
      if (done) return;
      done = true;
      egg.classList.add("spk-leaving");
      setTimeout(() => { egg.remove(); style.remove(); }, 800);
      document.removeEventListener("pointerdown", finish);
      document.removeEventListener("keydown", onKey);
    }
    function onKey(event) {
      if (event.key === "Escape") finish();
    }
    egg.querySelector(".spk-close").addEventListener("click", finish);
    document.addEventListener("pointerdown", finish);
    document.addEventListener("keydown", onKey);
    setTimeout(finish, DURATION);
  }

  function onScroll() {
    const heading = document.getElementById("in-closing");
    const pageEnd = window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2;
    if ((heading && heading.getBoundingClientRect().top < window.innerHeight * 0.25) || pageEnd) {
      window.removeEventListener("scroll", onScroll);
      play();
    }
  }
  window.addEventListener("scroll", onScroll, { passive: true });
})();
</script>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-10-30_shiny-python-concurrent-async/feature.gif" length="193148" type="image/gif" />
    </item>
    <item>
      <title>Positron October Release Highlights</title>
      <link>https://opensource.posit.co/blog/2026-10-13_positron-2026-10-release/</link>
      <pubDate>Tue, 13 Oct 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-10-13_positron-2026-10-release/</guid>
      <dc:creator>Julia Silge</dc:creator><description><![CDATA[<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Note</span>
</div>
<div class="callout-body">
<p><a href="https://positron.posit.co/?utm_source=opensource-posit-co&amp;utm_medium=referral&amp;utm_campaign=positron-2026-10-release-blog&amp;utm_content=intro-note" target="_blank" rel="noopener">Positron</a> is Posit&rsquo;s new, next-generation IDE for data science. Positron is designed to be an extensible, polyglot tool for exploring data and reproducible authoring in Python, R, and more.</p>
</div>
</div>
<p>Welcome back to another edition of our monthly Positron updates! Each month we share highlights from our <a href="https://positron.posit.co/release-notes?utm_source=opensource-posit-co&amp;utm_medium=referral&amp;utm_campaign=positron-2026-10-release-blog&amp;utm_content=release-notes-link" target="_blank" rel="noopener">latest release</a> and useful resources. <a href="https://opensource.posit.co/blog/2026-09-09_positron-2026-09-release">Last release</a> we told you about a redesigned welcome page, a first version of Import Data, new Posit Assistant features, expanded Data Connections, package vulnerability scanning, and a more responsive Console. This milestone brings a new ability for Posit Assistant to drive Positron itself, a built-in MCP server so you can bring your own coding agent, Data Connections on by default, faster language features for Quarto, and smoother interpreter and project setup.</p>
<h2 id="posit-assistant-can-drive-positron">Posit Assistant can drive Positron
</h2>
<p><a href="https://assistant.posit.co/?utm_source=opensource-posit-co&amp;utm_medium=referral&amp;utm_campaign=positron-2026-10-release-blog&amp;utm_content=assistant-section" target="_blank" rel="noopener">Posit Assistant</a> can now operate Positron itself, not just the R or Python session inside it. You can ask it to run a Shiny app, open a Parquet file in the Data Explorer, set up a Python environment, browse your data connections, or deploy to Posit Connect. Posit Assistant uses the same source of truth as the Positron UI, so it works with your real interpreters, packages, and connections rather than guessing at them.</p>
<img src="https://opensource.posit.co/blog/2026-10-13_positron-2026-10-release/assistant-data-explorer.png" data-fig-align="center" data-fig-alt="Positron with the Posit Assistant chat beside the Data Explorer, which shows pokemon.xlsx that Posit Assistant opened." />
<p>In practice, this means Posit Assistant can run and preview Dash, FastAPI, Flask, Gradio, marimo, Streamlit, and Shiny apps with the same app commands you use, see which apps are running along with their status and URL, and stop them. It can show HTML files and URLs in the Viewer pane, and it can read, take screenshots of, and interact with data apps and other content there, which makes it much more useful for checking its own work on an app. It can read your configured and detected Data Connections, browse a connection&rsquo;s schema, and give you the code to open it. It can also tell which interpreter a project uses, explain why an installed interpreter isn&rsquo;t showing up, and make that interpreter available, and it knows the commands for deploying to Connect through Posit Publisher.</p>
<p>To try this preview feature, enable the <a href="positron://settings/assistant.previewFeatures"><code>assistant.previewFeatures</code></a> setting, and tell us what you think in the <a href="https://github.com/posit-dev/positron/discussions/16280" target="_blank" rel="noopener">GitHub discussion</a>.</p>
<h2 id="bring-your-own-coding-agent">Bring your own coding agent
</h2>
<p>If you already have a favorite coding agent other than Posit Assistant, this release lets it work alongside you in Positron. A new built-in Model Context Protocol (MCP) server lets coding agents such as Claude Code, Codex, and Gemini CLI work in your live Python and R sessions. Our goal is to give these agents a set of tools comparable to what Posit Assistant has: agents can list your sessions, run code in the Console, inspect objects without cluttering your history, read recent session history, interrupt a long-running computation, look at the current plot, and search for and run Positron commands. These other agents may not have the data science expertise that Posit Assistant has, but they can still help you write code and explore data.</p>
<p>You stay in the loop on what an agent does. Code that an agent runs shows up in the Console with the agent&rsquo;s name next to it, so you can always tell agent code from your own. Console tabs also now show a dot when code runs in a console you aren&rsquo;t looking at, whether that code came from Posit Assistant or an external agent.</p>
<img src="https://opensource.posit.co/blog/2026-10-13_positron-2026-10-release/claude-code-in-positron.png" data-fig-align="center" data-fig-alt="Claude Code in Positron&#39;s editor area, asked whether to visualize country-to-country twinning relationships as a heatmap or a network graph. It calls the Positron execute_code and get_plot tools, then recommends the heatmap and explains it. In the Console, the R code it ran is labeled Claude Code, and the Plots pane shows the resulting heatmap of twinning links between the top countries." />
<p>To try this experimental feature, enable the <a href="positron://settings/ai.mcp.enabled"><code>ai.mcp.enabled</code></a> setting. Positron configures Claude Code for you automatically, and the <em>Positron MCP: Add to Coding Agent</em> command sets up the Codex and Gemini CLIs. For any other MCP client, <em>Copy Connection Details</em> gives you a URL and token to connect with.</p>
<h2 id="data-connections-on-by-default">Data Connections on by default
</h2>
<p><a href="https://positron.posit.co/data-connections?utm_source=opensource-posit-co&amp;utm_medium=referral&amp;utm_campaign=positron-2026-10-release-blog&amp;utm_content=data-connections-section" target="_blank" rel="noopener">Data Connections</a> is now the default way to work with databases and data warehouses in Positron. Data Connections connects to databases directly, without needing a Python or R session, and it replaces both the older Connections pane and the Catalog Explorer. If you need the older Connections pane, set <a href="positron://settings/dataConnections.enabled"><code>dataConnections.enabled</code></a> to <code>false</code> and reload the window.</p>
<p>This release also extends what you can connect to and how you sign in. Amazon Redshift connections can sign in with AWS Identity and Access Management (IAM), for both Redshift Serverless and provisioned clusters, using credentials from the standard AWS credential provider chain. On Posit Workbench, Snowflake and Databricks connections can use the session&rsquo;s managed credentials with no credential input at all. Snowflake users get the most new features this month; the Data Connections pane shows Snowflake semantic views with their dimensions, facts, metrics, and relationships, you can browse Snowflake stages as folders and files, and connections in <code>connections.toml</code> show up automatically as detected connections.</p>
<p>Smaller improvements round out the experience. When you open a DuckDB or SQLite file from the Explorer, Positron offers to create a data connection for it. <strong>Connect With</strong> can now generate ggsql code for SQLite, DuckDB, ODBC, PostgreSQL, and Redshift connections.</p>
<img src="https://opensource.posit.co/blog/2026-10-13_positron-2026-10-release/connections-ggsql-bikeshare.gif" data-fig-align="center" data-fig-alt="The Data Connections pane in Positron showing a Bikeshare demo SQLite connection. Choosing ggsql under Connect With opens a dialog with the generated connection code. After clicking Connect, the code runs in a ggsql console, followed by a SQL query that counts bikes by bike type and returns a results table." />
<p>The <strong>Add Data Connection</strong> dialog lists each driver with a short description, and Positron now tells you when a connection fails to open or expand. For slow tables and views, the Data Explorer now shows a progress indicator and placeholders while data arrives, and it loads column summaries in two passes so that missing-value counts appear quickly and sparklines follow later.</p>
<h2 id="faster-language-features-for-quarto">Faster language features for Quarto
</h2>
<p>Positron now provides completions, hover, diagnostics, outline, formatting, and other language features natively for R and Python cells in Quarto and R Markdown documents. Before, the Quarto extension wrote temporary virtual documents to disk to power these features. Positron now uses a virtual notebook in memory instead, which is faster (especially in remote sessions) and solves a whole category of bugs, such as Python imports that did not resolve correctly. It also means Go to Definition, Find References, and Rename work across R cells, finding names that earlier cells define.</p>
<p>The document outline for Quarto and R Markdown documents with many code chunks now updates quickly, and it no longer shows duplicate groups or symbols. Inline output, including HTML tables and the inline Data Explorer, now scales with your editor font size. If you run into problems with the new language features and need to go back to the old behavior, set <a href="positron://settings/quarto.embeddedLanguageFeatures.native"><code>quarto.embeddedLanguageFeatures.native</code></a> to <code>false</code>.</p>
<h2 id="interpreter-project-setup">Interpreter project setup
</h2>
<p>Positron is better at setting up the right Python environment for your project. When a workspace has a <code>uv.lock</code> or <code>pixi.lock</code> file but no environment yet, Positron offers to create it with <code>uv sync</code> or <code>pixi install</code>, which is especially handy right after you clone a project. If uv is missing, the New Folder flow can install it for you, and package installs and the Packages pane work with uv right away. When Positron finds a new Python environment in a workspace, it now offers to start a console session in it, and system Python installations now show last in the interpreter picker, below uv, venv, conda, and pyenv environments.</p>
<img src="https://opensource.posit.co/blog/2026-10-13_positron-2026-10-release/uv-lock-create-env.gif" data-fig-align="center" data-fig-alt="A Shiny dashboard Quarto document open in Positron. A notification says the project has a uv.lock file but no virtual environment and offers to run uv sync. After clicking Yes, Positron creates the environment and then offers to start a console session with it." />
<p>For more control over interpreters in <em>both</em> Python and R, interpreter path settings now support <code>${workspaceFolder}</code>, so a project can point to an interpreter that lives inside it. The new <a href="positron://settings/interpreters.definitions"><code>interpreters.definitions</code></a> setting lets you define additional R and Python interpreters, each with its own label, environment variables, and startup script. This is useful for workflows like loading environment modules or configuring rJava before a session starts. Set <a href="positron://settings/interpreters.discovery"><code>interpreters.discovery</code></a> to <code>definitionsOnly</code> to show only the interpreters you have defined for a language.</p>
<h2 id="whats-coming-next">What&rsquo;s coming next
</h2>
<ul>
<li>Meet Posit at <a href="https://posit.co/events/snowflake-world-tour-2026-nyc?utm_source=opensource-posit-co&amp;utm_medium=referral&amp;utm_campaign=positron-2026-10-release-blog&amp;utm_term=snowflake&amp;utm_content=events-section" target="_blank" rel="noopener">Snowflake World Tour NYC</a> on October 15. Stop by our booth, schedule time with a Posit expert, or catch our theater session on AI-powered data science where your data lives, inside Snowflake.</li>
<li>Join us at <a href="https://opensource.posit.co/events/data-outpost-2026/" target="_blank" rel="noopener">Data Outpost</a> on November 4-5 in San Francisco. I&rsquo;ll be speaking on &ldquo;The SQL Agent Harness We Didn&rsquo;t Mean to Build,&rdquo; about how the SQL tools we built in Positron for people turned out to already be a harness for AI assistants.</li>
</ul>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Tip</span>
</div>
<div class="callout-body">
<p><a href="https://positron.posit.co/download?utm_source=opensource-posit-co&amp;utm_medium=referral&amp;utm_campaign=positron-2026-10-release-blog&amp;utm_content=download-cta" target="_blank" rel="noopener">Download Positron</a> to try out the new features and improvements in this release!</p>
</div>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-10-13_positron-2026-10-release/featured.svg" length="91754" type="image/svg&#43;xml" />
    </item>
    <item>
      <title>Correct, transparent, and reproducible data agents</title>
      <link>https://opensource.posit.co/blog/2026-10-09_ai-newsletter/</link>
      <pubDate>Fri, 09 Oct 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-10-09_ai-newsletter/</guid>
      <dc:creator>Sara Altman</dc:creator>
      <dc:creator>Simon Couch</dc:creator><description><![CDATA[<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title"><strong>Subscribe to the AI Newsletter!</strong></span>
</div>
<div class="callout-body">
<p>The AI newsletter is published as an RSS feed. Follow it in your favorite reader:</p>
<p><a href="https://opensource.posit.co/tags/ai-newsletter/index.xml" target="_blank" rel="noopener noreferrer" class="btn-shortcode inline-flex mb-5 mr-5 items-center px-4 py-3 text-sm leading-5 gap-2 rounded-lg bg-blue-400 !text-white font-semibold align-middle hover:bg-blue-500 transition no-underline">Subscribe via RSS</a></p>
<p><strong>Want the newsletter as an email?</strong> Paste the feed URL, <a href="https://opensource.posit.co/tags/ai-newsletter/index.xml" target="_blank" rel="noopener">https://opensource.posit.co/tags/ai-newsletter/index.xml</a>, into a free RSS-to-email service such as <a href="https://blogtrottr.com/" target="_blank" rel="noopener">Blogtrottr</a>, <a href="https://feedrabbit.com/" target="_blank" rel="noopener">Feedrabbit</a>, or <a href="https://follow.it/" target="_blank" rel="noopener">Follow.it</a>, and each new issue will arrive in your inbox.</p>
</div>
</div>
<style>
.blog-toc-content ul ul {
  display: none;
}
.prose img[src*="images/slides/"],
.prose video.column-page {
  border-radius: 0.25rem;
  box-shadow:
    0 1rem 2.5rem rgba(28, 45, 70, 0.14),
    0 0.2rem 0.6rem rgba(28, 45, 70, 0.1);
}
.prose img[src*="images/slides/"] {
  transition:
    box-shadow 160ms ease,
    transform 160ms ease;
}
.prose img[src*="images/slides/"]:hover,
.prose img[src*="images/slides/"]:focus-visible {
  box-shadow:
    0 1.2rem 3rem rgba(28, 45, 70, 0.2),
    0 0.25rem 0.75rem rgba(28, 45, 70, 0.14);
  transform: translateY(-2px);
}
</style>
<p>Last month, we gave a posit::conf(2026) keynote on building correct, transparent, and reproducible data agents. The talk brings together many themes from this newsletter, so this week we&rsquo;re sharing an annotated walkthrough. You can also see the full slide deck <a href="https://pos.it/keynote" target="_blank" rel="noopener">here</a>.</p>
<h2 id="correctness-vsconvenience">Correctness vs. convenience
</h2>
<p><strong>(Simon)</strong></p>
<p>This morning, your VP pulled up a coding agent and asked a reasonable question:</p>
<blockquote>
<p>How is traffic trending for our site?</p>
</blockquote>
<p>The agent looked around the workspace, found a table that seemed relevant,
wrote a SQL query against it, and returned a polished chart. Daily visits, it
said, were down 64% over the last 90 days.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/vp-site-traffic-reminder.webp" class="column-page" loading="lazy" data-fig-alt="A chat interface shows the question “how is traffic trending for our site?” The agent answers that daily visits are down 64 percent and displays a sharply declining line chart." width="1280" height="720" />
<p>The variation looks a little tight, and the bend is a little sharp. Is there
any possible reason we might see that bend other than a genuine change in site
traffic?</p>
<p>It turns out that a version of Chrome released 45 days earlier fired the
page-view event differently, so it was not aggregated correctly in the
database. As people updated Chrome, it appeared that site visits were trending
down. This was not even the table the data science team used to track site
traffic.</p>
<p>But you are not there! Your VP sees this chart and takes it to the rest of the
leadership team. They decide to restructure the organization and spend millions
of dollars to reverse the trend.</p>
<p>A month later, somebody asks, &ldquo;Could you recreate that chart? Let&rsquo;s see how
things are trending now.&rdquo; The agent cannot find it. Many coding agents delete
conversation histories after 30 days, and they often do data analysis in
temporary files so they do not clutter the project directory. The source code
for the analysis is long gone.</p>
<p>The coding agent offers to recreate the chart. It looks around the workspace,
finds a table, and runs some SQL. The number was going up the whole time.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/site-traffic-recreated.webp" class="column-page" loading="lazy" data-fig-alt="A chat interface cannot find the earlier conversation or its files. After recreating the analysis with a different warehouse table, it reports that daily visits increased 22 percent and shows an upward-trending chart." width="1280" height="720" />
<p>This is a fictional story, but we&rsquo;e seen situations like this internally
at Posit and at many of the organizations we work with. It is becoming normal
to <em>vibe-analyze</em> data: ask an agent a question, receive a plausible answer,
and move on without understanding how it was produced.</p>
<h3 id="the-tension-between-correctness-and-convenience-is-not-new">The tension between correctness and convenience is not new
</h3>
<p>Posit CTO Joe Cheng summarized the current moment this way:</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/convenience-versus-correctness.webp" class="column-page" loading="lazy" data-fig-alt="A cream-colored slide displays the quotation “In the battle between convenience and correctness, convenience is winning,” attributed to Joe Cheng, Posit CTO." width="1280" height="720" />
<p>AI has broadened the group of people who can convincingly do data analysis, but
many may have no idea what data scientists go through to produce a trustworthy
analysis.</p>
<p>Joe said this a month before the keynote, but he could have said it seven years
ago. In 2019, the beverage company Conviviality disclosed that <a href="https://www.accountingweb.co.uk/business/management-accounting/convivialitys-spreadsheet-hell-sinks-the-business" target="_blank" rel="noopener">a spreadsheet
arithmetic error had contributed £5.2 million to a £14 million forecasting
mistake</a>.
When that &ldquo;innocent spreadsheeting mistake&rdquo; came to light, the business sank.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/conviviality-spreadsheet-error.webp" class="column-page" loading="lazy" data-fig-alt="A news article about Conviviality reporting a spreadsheet arithmetic error that contributed 5.2 million pounds to a 14 million pound forecasting error." width="1280" height="720" />
<p>Or Joe could have said it 20 years ago, when <a href="https://www.science.org/doi/10.1126/science.314.5807.1856" target="_blank" rel="noopener">a sign error in closed-source
protein-modeling
software</a> sent a
research field down the wrong path. A lab published landmark study after
landmark study while other labs struggled to reproduce its findings. Only
after another lab found a protein structure that was nearly a mirror image of
one that the first lab had published did the first lab audit its source code.
They found a negative sign where it did not belong and retracted five papers.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/protein-modeling-software-error.webp" class="column-page" loading="lazy" data-fig-alt="A Science article titled &#39;Error prompts retractions&#39; describing a sign error in protein-modeling software." width="1280" height="720" />
<p>There were threats to correctness in data analysis long before AI. Posit&rsquo;s
stance has never been that people must sacrifice convenience for correctness.
While preparing the talk, I found this quote in a blog post old enough to
refer to itself as a &ldquo;weblog&rdquo;:</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/trustworthy-and-intuitive.webp" class="column-page" loading="lazy" data-fig-alt="A cream-colored slide quotes JJ Allaire: “Our goal is to develop a powerful tool that supports trustworthy, high quality analysis. At the same time, we want RStudio to be as straightforward and intuitive as possible.”" width="1280" height="720" />
<p>JJ was saying that a tool could be both correct and convenient. (By
convenient, we do not mean 7-Eleven. We mean straightforward and intuitive.😜)</p>
<p>Can we take the same stance toward AI? Is it possible to make data analysis
agents that are both correct and convenient for end users?</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/correct-and-convenient.webp" class="column-page" loading="lazy" data-fig-alt="Two overlapping circles, one blue and one orange, form a Venn diagram above the words &#39;Correct and convenient.&#39;" width="1280" height="720" />
<h2 id="why-build-ai-tools-at-all">Why build AI tools at all?
</h2>
<p><strong>(Sara)</strong></p>
<p>This pull between correctness and convenience has been on our minds throughout
the new, exciting age of LLMs. In 2025 and early 2026, AI started to feel
really, really convenient for data analysis. That made us worry about
correctness.</p>
<p>In August 2025, when we released Databot, our state-of-the-art exploratory data
analysis agent, we did so under what was essentially a warning label. The
release post was one of the first things I worked on after joining the AI team,
and it gave me the chance to do something I think I am particularly good at:
be pessimistic.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/databot-flotation-device.webp" class="column-page" loading="lazy" data-fig-alt="A split slide dated August 2025 shows an illustration of the Databot mascot wearing a bright orange flotation device." width="1280" height="720" />
<p>We were all really excited about Databot. It felt like flying through your
data, gathering insights faster than you thought would ever be possible. It was
precisely that convenience that worried us. People might trust it because they
were having so much fun, even when it was wrong. So we wrote an entire article
detailing basically every way we thought Databot might fail.</p>
<p>If you were at posit::conf(2025), you might remember Joe Cheng describing it as
both the most exciting and the most dangerous software he had worked on in his
30-year career.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/exciting-and-dangerous.webp" class="column-page" loading="lazy" data-fig-alt="A quotation from Joe Cheng says that Databot is both the most exciting software and the most dangerous software he has worked on in his 30-year career." width="1280" height="720" />
<p>That raises a fair question, one we contemplate ourselves on a weekly basis:
If AI causes all this chaos and makes all these mistakes, why are we doing
this? Why involve ourselves at all?</p>
<p>There are many answers. We&rsquo;ll focus on one.</p>
<h3 id="curiosity-and-exploration">Curiosity and exploration
</h3>
<p>Before joining the AI team, I taught data science. One thing that always
struck me was what brought people with very different backgrounds into the
field. The thing that united many of them was a sense of curiosity. Maybe they
were curious about a scientific field, computers, statistics, or how R and
Python themselves work. Data rewards that curiosity: There are datasets on
almost everything.</p>
<p>But even as an undergraduate who knew some statistics and a little R, I often
felt like I was looking at an ocean from above. The dataset was right in front of
me, and I knew there was so much going on, but I just couldn&rsquo;t see it.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/mission-ocean.webp" class="column-page" loading="lazy" data-fig-alt="An aerial photograph shows pale blue ocean water meeting a sandy shoreline, with a small dark shape visible below the surface." width="1280" height="720" />
<p>As a graduate student, I learned the tidyverse and more about data science. As
I got better at using those tools, I finally felt like I had what I needed to
ask and answer questions about my data.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/below-the-surface.webp" class="column-page" loading="lazy" data-fig-alt="A split slide pairs the words &#39;The right tools let us see below the surface&#39; with a photograph of rays swimming underwater." width="1280" height="720" />
<p>The right tools let us see below the surface. They enable that curiosity.</p>
<p>That same curiosity motivates our work now. We are curious about how these
models work, how we can build things around them to make them work better, and,
maybe most importantly, how we can turn them into tools that open new worlds of
analysis. For us, AI is a continuation of work we have always done: finding new
ways to explore data.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/ai-as-continuation.webp" class="column-page" loading="lazy" data-fig-alt="A cream-colored slide reads, &#39;AI is a continuation of our work to find new ways to explore data,&#39; with &#39;continuation&#39; and &#39;new ways to explore data&#39; highlighted." width="1280" height="720" />
<h3 id="the-models-got-better">The models got better
</h3>
<p>I also wanted to spend some time on what has changed since August 2025, when
we released that warning-label image. One major thing happened: The models got
better.</p>
<p>About a year before the keynote, we ran an experiment to understand how well
models interpret plots. We showed them plots based on a transformed diamonds
dataset and asked them to describe what they saw.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/bluffbench-counterintuitive-plot.webp" class="column-page" loading="lazy" data-fig-alt="A scatterplot shows diamond price decreasing as carat increases. An overlaid model response incorrectly says there is a strong positive relationship, followed by the question of whether models can interpret plots that contradict their expectations." width="1280" height="720" />
<p>The models gave answers like, &ldquo;There is a strong positive relationship between
carat and price.&rdquo; That sounds plausible until you look at the plot, which shows
the opposite. Behind the scenes, we had manipulated the data to reverse a
canonical relationship, so larger diamonds were, unintuitively, less expensive.</p>
<p>We wanted to know whether the models could see what was in front of them and
interpret plots that contradicted their expectations. A year ago, the answer
was largely no.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/bluffbench-november-2025.webp" class="column-page" loading="lazy" data-fig-alt="A horizontal stacked bar chart titled “Models often report what they expect to see, not what’s plotted.” GPT-5, Gemini Pro 2.5, and Claude Sonnet 4.5 are mostly marked incorrect." width="1280" height="720" />
<p>In November 2025, even the best models of the time were abysmal at this task.
They said what they expected to see instead of paying attention to what was
actually happening in the plot.</p>
<p>We tried all sorts of interventions to improve the
<a href="https://posit-dev.github.io/bluffbench/" target="_blank" rel="noopener">bluffbench</a> scores, and absolutely
nothing worked. Then we waited six months.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/bluffbench-september-2026.webp" class="column-page" loading="lazy" data-fig-alt="A horizontal stacked bar chart titled “Models got better at interpreting counterintuitive plots.” Recent thinking models have much larger correct segments than models evaluated in 2025." width="1280" height="720" />
<p>By May 2026, the major AI companies were releasing models that
were noticeably better at the task. By September, the numbers had jumped dramatically:
The models could interpret these counterintuitive plots.</p>
<p>The models got better. That does not mean data science is solved.</p>
<h3 id="models-still-make-mistakes">Models still make mistakes
</h3>
<p>After that experiment, we asked another question: Can models identify
data-quality issues in visualizations? In
<a href="https://github.com/posit-dev/bluffbench2" target="_blank" rel="noopener">bluffbench2</a>, we again showed them
plots and asked what they saw.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/bluffbench2-data-artifact.webp" class="column-page" loading="lazy" data-fig-alt="A scatterplot of sleep hours against stress score has an overall negative trend. Within the cloud, a suspicious subset of points falls exactly along a straight line." width="1280" height="720" />
<p>If you look carefully, there&rsquo;s a suspiciously straight line through the cloud
of points. If we really cared about this data, that&rsquo;s something to investigate: Did something weird happen when the data was
created? We wanted to know whether models would do the same.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/bluffbench2-results.webp" class="column-page" loading="lazy" data-fig-alt="A horizontal bar chart titled “Frontier models still struggle to notice subtle data quality issues.” Every evaluated model scores below 50 percent." width="1280" height="720" />
<p>It turns out they don&rsquo;t really. Even the leading models were not very good at
this task. They struggle to notice subtle data-quality issues.</p>
<p>But there&rsquo;s another problem. Data analysis is not just you, plus a good model,
plus your data. It also depends on context that is not necessarily in your CSV
file, warehouse table, or JSON file.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/context-outside-the-data.webp" class="column-page" loading="lazy" data-fig-alt="A cream-colored slide reads, &#39;Data analysis depends on context that is not in the data,&#39; with the word &#39;context&#39; highlighted." width="1280" height="720" />
<p>All data is constructed. It all has context. That context
might live in a data dictionary, a Markdown file, or a semantic view. Maybe it
just lives in the head of a coworker who has been at the organization for 15
years, knows absolutely everything, and has never written a single thing down.</p>
<p>That context is not inherently inaccessible to models. We could give it to
them. But it has to be in the right format, it has to be curated, and it has to
itself be correct.</p>
<p>So the models have gotten better, but data analysis with AI is not solved. The
question then becomes: How do we make AI-involved analysis correct?</p>
<h2 id="how-do-we-make-ai-for-data-analysis-trustworthy">How do we make AI for data analysis trustworthy?
</h2>
<p><strong>(Sara)</strong></p>
<p>Here&rsquo;s a common thought about AI: It&rsquo;s untrustworthy, but useful. To make it
trustworthy, we need to add in a person.</p>
<p>Here&rsquo;s a simple version of what that might look like. The agent or model does stuff, probably writes a bunch of code. A person then looks at that code and verifies, approves, or
redirects as needed. That feeds back into the agent. This intuitively can feel really appealing. The agent gets to do what it&rsquo;s
good at, the person does what they&rsquo;re good at, and there&rsquo;s an expert there to keep everything
from going off the rails.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/human-in-the-loop.webp" class="column-page" loading="lazy" data-fig-alt="A diagram presents a common division of labor in which an AI system produces work and a human verifies or supervises it." width="1280" height="720" />
<p>And so it&rsquo;s kind of tempting to apply this pattern everywhere. Any time the agent might make
a mistake, we have a person there to clean it up and make sure nothing bad
happens. We call this relatively simplistic view the <em>slap a human on it</em>
approach.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/slap-a-human-on-it.webp" class="column-page" loading="lazy" data-fig-alt="A cream-colored slide shows a roll of black repair tape, used as a metaphor for attaching a human reviewer to an AI system without designing the interaction." width="1280" height="720" />
<p>In this approach, you don&rsquo;t really think very much about what the person will actually do. How will
they apply their expertise? What is the purpose of their involvement? How will
we keep them from technically being in the loop but without actually applying
their expertise?</p>
<p>I&rsquo;m being a bit facetious, but this isn&rsquo;t that far from how we wrote risk mitigation with Databot a year earlier. We said that
Databot and LLMs were not at a point where users could abdicate responsibility.
You still needed all of your data skills to catch errors, interpret results in
context, and avoid being misled by confident but incorrect claims.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/user-responsibility.webp" class="column-page" loading="lazy" data-fig-alt="A quotation over a beach image says that people cannot abdicate responsibility or suspend skepticism when working with LLMs and that their expertise helps them catch errors and avoid confident but incorrect claims." width="1280" height="720" />
<p>And all of that is still true! But
we never said <em>how</em> this was supposed to happen. We assumed that if an expert
was there, they would catch the mistakes.</p>
<p>But just adding a person doesn&rsquo;t really guarantee a better result. Human&ndash;AI combinations do
not reliably outperform the better of the human or AI working
alone.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/human-ai-evidence.webp" class="column-page" loading="lazy" data-fig-alt="A slide reads, &#39;Adding a human does not guarantee a better result,&#39; followed by points about human-AI performance, approval fatigue, trust, timing, cognitive load, social cues, and information design." width="1280" height="720" />
<p>People also get tired of approving requests, and then they stop reading them.
Think about the last time your coding agent showed you a command to approve.
Did you carefully read it, or did you hit approve as quickly as possible and
feel annoyed that it even bothered to ask?</p>
<p>Human decision-making is not fixed. How much you trust the system, how much you
trust the people who built it, the timing of the information, your cognitive
load, and how much your coworkers trust the agent can all influence the
decision you make. Even if you are an expert who makes the right decision
outside an interaction with an agent, adding the agent might change that
decision.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/people-are-not-fixed.webp" class="column-page" loading="lazy" data-fig-alt="A cream-colored slide reads, &#39;People are not fixed safety components,&#39; with the final phrase highlighted." width="1280" height="720" />
<p>So how do we design the entire system to produce correct work? How do we build
trust and correctness into the agent itself, and design interactions with
users that actually support their expertise?</p>
<p>That maps onto two principles we returned to throughout the talk:</p>
<ol>
<li>Help the agent be correct.</li>
<li>Make it less bad when the agent is wrong.</li>
</ol>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/agent-correctness-goals.webp" class="column-page" loading="lazy" data-fig-alt="A cream-colored slide reads, &#39;Help the agent be correct&#39; and &#39;Make it less bad when the agent is wrong.&#39;" width="1280" height="720" />
<h2 id="posit-assistant">Posit Assistant
</h2>
<p><strong>(Simon)</strong></p>
<p><a href="https://pos.it/assistant" target="_blank" rel="noopener">Posit Assistant</a> is Posit&rsquo;s coding and data science
agent. It is available in RStudio, Positron, and the terminal. When it is
running inside an IDE, it works in the same R or Python session you&rsquo;re using.
You can use whatever model you want from whatever provider you have access to.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/posit-assistant.webp" class="column-page" loading="lazy" data-fig-alt="A blue slide displays the Posit Assistant wordmark and orange circular logo." width="1280" height="720" />
<p>For the demo, I returned to one of my first experiences of seeing below the
surface with data science. In an introductory statistics class, I analyzed
flight data to understand whether I could book tickets that were less likely
to be delayed. I used ggplot2 and dplyr, pushing data frames around and
visualizing them.</p>
<p>I asked Posit Assistant to find an R package with Houston flight data and
retrieve data from the previous year. It found <code>anyflights</code>, read its
documentation, and generated the correct call.</p>
<video class="column-page" loop muted playsinline controls preload="metadata" aria-label="Posit Assistant searches for an R package that provides data about flights from Houston.">
<source src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/videos/posit-assistant-web-fetch.mp4" type="video/mp4">
</video>
<p>Next, I asked it to grab data from the same time the previous year. The agent read the package documentation and so was able to write the code with the right arguments the first time.</p>
<video class="column-page" loop muted playsinline controls preload="metadata" aria-label="Posit Assistant reads the anyflights package documentation and writes code to retrieve Houston flight data.">
<source src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/videos/posit-assistant-docs.mp4" type="video/mp4">
</video>
<p>We have seen that many coding agents have a superficial relationship with data quality, and we wondered whether we could do better. Posit
Assistant proactively recommends cleaning the data and has a dedicated data-cleaning mode. It runs scratch code to look for
missing values, outliers, and similar issues. Once it finds them, it surfaces questions to the user in an interactive dialog.</p>
<video class="column-page" loop muted playsinline controls preload="metadata" aria-label="Posit Assistant enters data-cleaning mode, asks the user about missing values and outliers, and records the decisions in a persistent cleaning script.">
<source src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/videos/posit-assistant-clean-data.mp4" type="video/mp4">
</video>
<p>In the demo, the agent found missing values in a relevant column, then found some flights with departure delays of multiple days. I chose to keep both:
Those outliers represented what really happened.</p>
<p>At the end of cleaning mode, the agent wrote a persistent file in the project
directory to ingest and clean the data. When someone revisits the analysis,
they can reproduce those steps.</p>
<p>Posit Assistant also shows plots inside the conversation. It is nice to be able to see them, but we
also think it is necessary: Our evaluations show that agents cannot reliably
interpret plots. When one carrier appeared unusually delayed, it checked the sample size
and found only 32 flights, suggesting that we should not read too much into the
pattern.</p>
<video class="column-page" loop muted playsinline controls preload="metadata" aria-label="Posit Assistant plots departure delays by carrier, checks the sample size behind an apparent anomaly, and finds that the carrier has only 32 flights.">
<source src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/videos/posit-assistant-carriers.mp4" type="video/mp4">
</video>
<p>Once the analysis was complete, I asked the agent to turn it into a persistent
Quarto report.</p>
<video class="column-page" loop muted playsinline controls preload="metadata" aria-label="Posit Assistant uses a specialized skill to turn the flight-delay analysis into a persistent Quarto report.">
<source src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/videos/posit-assistant-report.mp4" type="video/mp4">
</video>
<p>I have one more &ldquo;little zoomy zoom&rdquo;: The entire conversation cost five cents.
<video class="column-page" loop muted playsinline controls preload="metadata" aria-label="The Posit Assistant conversation displays the selected GLM 5.3 Flash model, token usage, and a total cost of five cents."><source src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/videos/posit-assistant-model-cost.mp4" type="video/mp4"></video></p>
<p>Part of our mission at Posit is to provide tools to people regardless of their
economic means.</p>
<p>Posit Assistant is designed to be
token-efficient and can use capable, inexpensive open-weight models such as GLM
5.3 Flash through Posit AI Pass. We spend a lot of time
watching the open-weights model space for models on the Pareto frontier:
capable of data analysis and orders of magnitude cheaper than frontier models.</p>
<p>Returning to the two principles, part of helping the agent be correct is, quite
literally, asking nicely. We say, &ldquo;Pretty please pay attention to data
cleanliness. Pretty please be open to uncertainty when carrying out data
analysis.&rdquo;</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/posit-assistant-correctness.webp" class="column-page" loading="lazy" data-fig-alt="A slide says to help the agent be correct by encouraging sound statistics and reproducibility practices, and make errors less bad by using the user&#39;s expertise and sharing an R or Python environment." width="1280" height="720" />
<p>We also design interactions around the user&rsquo;s expertise: Cleaning mode asks
about the data-generating process, plots remain visible, and the user shares
the agent&rsquo;s R or Python session.</p>
<p>Posit Assistant assumes that the user is a data scientist, analyst, or someone
else who writes code and wants to work in an IDE, close to that code. But what
if the user is not a data scientist? How do we help them get correct answers,
too?</p>
<h2 id="introducing-commons">Introducing commons
</h2>
<p><strong>(Sara)</strong></p>
<p>Let&rsquo;s return to our vibe-analyzing VP. I think it&rsquo;s easy to say, &ldquo;They just
shouldn&rsquo;t be doing this kind of analysis.&rdquo; But let&rsquo;s be on their side for a
moment. They had a question, they wanted it answered, and so they&rsquo;re going to
reach for a tool that gives them an answer.</p>
<p>The problem is that they reached for the wrong tool and it gave them the wrong answer.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/vp-site-traffic-reminder.webp" class="column-page" loading="lazy" data-fig-alt="A chat interface shows the VP asking how traffic is trending for the site. The agent reports that daily visits are down 64 percent and displays a sharply declining line chart." width="1280" height="720" />
<p>The data team already has vetted, maintained code that computes site traffic
correctly. We call this <em>trusted code</em>. Trusted code might live in a package, Shiny app,
Quarto document, report, or dashboard.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/trusted-code.webp" class="column-page" loading="lazy" data-fig-alt="A cream-colored slide displays the words &#39;Trusted code,&#39; with &#39;Trusted&#39; highlighted in orange." width="1280" height="720" />
<p>Instead of having the model write bespoke code from scratch every time someone
asks a question, what if it could use that trusted code?</p>
<p>This is one of the core ideas behind a new open-source package that we&rsquo;re
excited to share with you all today. It&rsquo;s called commons.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/commons.webp" class="column-page" loading="lazy" data-fig-alt="A blue slide displays the orange commons bird logo and a link to pos.it/commons." width="1280" height="720" />
<p><a href="https://pos.it/commons" target="_blank" rel="noopener">commons</a> helps data scientists build trustworthy data
agents for their collaborators. It is an open-source R and Python package built
on the ellmer, chatlas, and shinychat stack.</p>
<p><strong>(Simon)</strong></p>
<p>Here is how the VP&rsquo;s analysis works with a commons agent. When the VP asks,
&ldquo;How is traffic trending for our site?&rdquo; the first thing the agent does is
search for a trusted calculation. If it finds one that answers the question,
it invokes that calculation directly.</p>
<video class="column-page" loop muted playsinline controls preload="metadata" aria-label="A commons agent answers a site-traffic question by running a trusted calculation, displaying a chart showing a 22 percent increase, and marking the answer with a green shield.">
<source src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/videos/commons-traffic-trend.mp4" type="video/mp4">
</video>
<p>The calculation returns a plot and a green shield indicating that the answer
came from a trusted calculation. commons adds that marker deterministically;
the agent itself does not control it.</p>
<p>What makes coding agents so convenient is that they are everything tools. They
will write their own R, Python, or SQL code to answer a question. Trusted
calculations cannot cover every question, so commons has a fallback path.</p>
<p>When no trusted calculation exists, the agent can use context authored by the
data team. commons verifies any citation against that context before displaying
its blue citation marker.</p>
<video class="column-page" loop muted playsinline controls preload="metadata" aria-label="A commons agent charts declining page views, uses trusted context to explain the instrumentation change behind the decline, and displays a blue citation marker.">
<source src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/videos/commons-page-views.mp4" type="video/mp4">
</video>
<p>If there is neither a trusted calculation nor supporting context to cite, the agent
can write code, but commons displays a yellow warning so the user knows to treat
the answer cautiously.</p>
<video class="column-page" loop muted playsinline controls preload="metadata" aria-label="A commons agent writes custom code to find the day with the most site visits, then marks the answer with a yellow warning because it did not use a trusted calculation or cite trusted context.">
<source src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/videos/commons-most-visits.mp4" type="video/mp4">
</video>
<p>Here&rsquo;s a diagram of the various ways the agent can come up with answers and how the trust relationships are mapped.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/commons-trust-flow.webp" class="column-page" loading="lazy" data-fig-alt="A flow diagram shows commons first searching trusted calculations. A found calculation leads to a verified marker. Otherwise the agent searches context and writes SQL, R, or Python, leading to either a citation marker or a lower-trust warning." width="1280" height="720" />
<p>So far, we&rsquo;ve been talking about the VP&rsquo;s experience with the commons agent. What is it like to work with
commons as a data scientist? There are three main loops: build, deploy, and improve.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/commons-lifecycle.webp" class="column-page" loading="lazy" data-fig-alt="Three cards describe the commons lifecycle: build by connecting the agent to trusted code, deploy it for colleagues on Connect, and improve it by reviewing behavior and covering common use cases." width="1280" height="720" />
<p>With data collection enabled, data scientists can review cases where the agent
had to write its own code. If those cases share a pattern, they can add trusted
code that moves future responses up the trust ladder, making the agent more
correct over time.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/commons-correctness.webp" class="column-page" loading="lazy" data-fig-alt="A slide says to help the agent be correct with trusted calculations and context, and make errors less bad by signaling trust and introducing feedback loops." width="1280" height="720" />
<p>Returning to our two principles, commons helps the agent be correct by letting
it directly use trusted code. It makes mistakes less bad by telling the user
when the agent did not directly use trusted code, and by moving responses up
the trust ladder over time.</p>
<h3 id="other-use-cases-for-commons">Other use-cases for commons
</h3>
<p><strong>(Sara)</strong></p>
<p>The VP example is business intelligence, but commons is not only for BI or
vice presidents. It&rsquo;s for any data person who wants to build an agent that
gives correct answers to their collaborators.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/commons-examples.webp" class="column-page" loading="lazy" data-fig-alt="A table pairs a biostatistician with a physician comparing treatment groups, a UX researcher with a product manager summarizing experiment results, and a policy analyst with a program lead simulating a policy&#39;s budget impact." width="1280" height="720" />
<p>We&rsquo;ve made a sample <a href="https://pos.it/clinical-trials-agent" target="_blank" rel="noopener">clinical trials
agent</a> to highlight this functionality.
Its data is simulated. The trusted code is a TLG catalog: code written and
trusted by data people to work on this type of data and produce correct
answers. You can ask it questions such as, &ldquo;Plot Kaplan&ndash;Meier curves.&rdquo;</p>
<video class="column-page" loop muted playsinline controls preload="metadata" aria-label="A clinical trials agent runs a trusted calculation for the safety population and produces a Kaplan–Meier plot of serious adverse-event-free probability.">
<source src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/videos/clinical-trials-agent-demo.mp4" type="video/mp4">
</video>
<h2 id="it-is-still-very-bad-to-be-wrong">It is still very bad to be wrong
</h2>
<p><strong>(Sara)</strong></p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/conclusion.webp" class="column-page" loading="lazy" data-fig-alt="A blue slide reads “It’s (still) very bad to be wrong” in large cream-colored text." width="1280" height="720" />
<p>It&rsquo;s now so easy to ask any kind of agent a question about your data and get a
wrong answer that looks completely plausible.</p>
<p>The problem is not just that the answer is wrong, and it&rsquo;s also not just that you might be unable to reproduce it or have no
idea how it arrived at the answer.</p>
<p>These are bad enough, but there&rsquo;s another problem. If we become accustomed to, say, 15% of our numbers
being wrong, we might lose trust in analysis itself and its ability to tell us things about the world, or become used to numbers just being a bit wrong all the time.</p>
<p>But correctness matters, especially in particular industries, and Posit has always
built tools to support correct analysis. We do not think that standard should
change just because AI is here.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/conclusion-infrastructure.webp" class="column-page" loading="lazy" data-fig-alt="A blue slide reads, &#39;AI has expanded who can get answers from data. That changes the infrastructure we need for correct, transparent, and reproducible analysis.&#39;" width="1280" height="720" />
<p>But AI has expanded who can get answers from data. Now anyone can ask a
question about their data. That changes how we need to think about the
infrastructure for building correct, transparent, and reproducible agents.</p>
<p>With commons, a data team establishes which code and calculations it trusts
and provides the organizational context the agent needs. The team can give that
agent to someone who might otherwise perform a vibe analysis. That person keeps
the convenience of asking questions in natural language, while the answers are
grounded in the data team&rsquo;s trusted work and can be traced and reproduced.</p>
<p>That is where data scientists and analysts come in. This infrastructure must be
built by people who understand the data, calculations, and organization.
commons helps them build it and make it available to others.</p>
<h2 id="looking-forward">Looking forward
</h2>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/canvas.webp" class="column-page" loading="lazy" data-fig-alt="A Canvas application displays a polished data-analysis workspace with an agent conversation, visual results, and interactive controls arranged across a large visual canvas." width="1280" height="720" />
<p>Looking forward, we have been experimenting with something called Canvas.</p>
<p>Canvas is a next-generation agent experience. It assumes that you might not
always want to look at the code. That does not mean the code is not there when
you need it, but you might not always want to work in an IDE.</p>
<p>This frees up real estate for expansive, new agent-collaboration interfaces.
Canvas feels a little aggressively agentic to us, in the way Databot felt a
year ago.</p>
<img src="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/slides/thank-you.webp" class="column-page" loading="lazy" data-fig-alt="A green slide says &#39;Thank you!&#39; above resource cards linking to the keynote slides, commons, and the AI Newsletter." width="1280" height="720" />
<h3 id="recent-past-newsletters">Recent past newsletters
</h3>
<ul>
<li><a href="https://opensource.posit.co/blog/2026-09-18_ai-newsletter">New releases from ellmer, shinychat, and commons</a></li>
<li><a href="https://opensource.posit.co/blog/2026-09-04_ai-newsletter">You probably don&rsquo;t want to fine-tune</a></li>
</ul>
<p><a href="https://opensource.posit.co/tags/ai-newsletter/index.xml" target="_blank" rel="noopener noreferrer" class="btn-shortcode inline-flex mb-5 mr-5 items-center px-4 py-3 text-sm leading-5 gap-2 rounded-lg bg-blue-400 !text-white font-semibold align-middle hover:bg-blue-500 transition no-underline">Subscribe via RSS</a></p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-10-09_ai-newsletter/images/hero.webp" length="15396" type="image/webp" />
    </item>
    <item>
      <title>Genomes, Prompts, and Shiny: What I Built During My Summer at Posit</title>
      <link>https://opensource.posit.co/blog/2026-10-08_genomes-prompts-and-shiny/</link>
      <pubDate>Thu, 08 Oct 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-10-08_genomes-prompts-and-shiny/</guid>
      <dc:creator>Samuel Bharti</dc:creator>
      <dc:creator>Shiny Team</dc:creator>
      <dc:creator>Barret Schloerke</dc:creator>
      <dc:creator>Carson Sievert</dc:creator>
      <dc:creator>Joe Cheng</dc:creator><description><![CDATA[<p>My name is Samuel Bharti and I am a PhD candidate at the University of Alabama at Birmingham, working on rare diseases and cancers, particularly focusing on Neurofibromatosis Type 1 and the cancers associated with it. I spent this summer as a software engineering intern on the Shiny team at Posit. In three short months, I built <strong>10 open-source applications and 5 supporting packages</strong> for computational biology, across R, Python, TypeScript, and JavaScript. Some are tools researchers can use directly. Others sit underneath those applications and solve recurring infrastructure problems.</p>
<p>I came into the internship as a computational biologist. In my PhD research, I work with single-cell sequencing, genomics, and multi-omics data, and with the public databases and identifiers that come with them.</p>
<p>Across different projects, I kept running into the same problems:</p>
<ul>
<li>Biological datasets are getting enormous.</li>
<li>The information needed to interpret them is scattered across many databases.</li>
<li>Identifiers are surprisingly inconsistent.</li>
<li>The same API and data-access logic gets rewritten again and again.</li>
<li>Even when excellent public datasets exist, there can still be a huge gap between <strong>the data being available</strong> and <strong>a researcher being able to use it comfortably</strong>.</li>
</ul>
<p>Those gaps became the theme of my summer.</p>
<blockquote>
<p>How do we make increasingly large and complicated biological datasets genuinely usable for researchers?</p>
</blockquote>
<h2 id="making-enormous-biological-datasets-explorable">Making enormous biological datasets explorable
</h2>
<p>One of the projects I am most excited about is <a href="https://posit-tahoe-explorer.share.connect.posit.cloud/" target="_blank" rel="noopener"><strong>Tahoe Explorer</strong></a> (<a href="https://github.com/samuelbharti/tahoe-explorer" target="_blank" rel="noopener">source</a>).</p>
<p><a href="https://huggingface.co/datasets/tahoebio/Tahoe-100M" target="_blank" rel="noopener">Tahoe-100M</a> contains more than 100 million single-cell perturbation profiles. That scale is scientifically exciting, but it creates a practical problem: you cannot treat 100 million observations like a normal dataframe inside an interactive application.</p>
<p>I wanted researchers to be able to answer basic questions before committing to a large analysis. Which cell lines are represented? Which drugs were tested? How much coverage exists for the combination I care about? What do we know about the genomic background of those cell lines? And once I find what I need, how do I retrieve only that part of the dataset?</p>
<p>Tahoe Explorer lets researchers move through drugs, cell lines, samples, genes, coverage, and cell-level metadata without first downloading and loading the entire resource. For the larger data, queries are pushed down to DuckDB, which returns only the rows or summaries the current view needs.</p>
<p>I integrated additional information from resources such as <a href="https://depmap.org/portal/" target="_blank" rel="noopener">DepMap</a> and <a href="https://www.cellosaurus.org/" target="_blank" rel="noopener">Cellosaurus</a>, including variant information for the cell lines. That makes it possible to think about perturbation response alongside the genomic context of each cell line.</p>
<p>The Subset builder tab then turns exploration into something reproducible. Users can define the experiment they care about and generate R or Python code for retrieving that subset later.</p>
<p>Another feature of the app is the built-in chat assistant. It can help researchers reason about Tahoe, identify useful filters, interact with supported parts of the app, and finally build those reproducible subset recipes.</p>
<p>What I like most is that the assistant lives inside the app. It can use tools over the data and help operate the workflow. To me, that is a much more interesting direction for AI in scientific software.</p>
<div class="not-prose">
<figure>
<a href="https://posit-tahoe-explorer.share.connect.posit.cloud/" target="_blank" rel="noopener"><img class="h-auto max-w-full rounded-lg" src="https://opensource.posit.co/blog/2026-10-08_genomes-prompts-and-shiny/tahoe-explorer.gif" alt="An animation of Tahoe Explorer. On the Overview tab, the tiles show 100.6M cells, 50 cell lines and 379 drugs, and clicking the Breast bar in the cell lines by organ chart filters the table to HS-578T and BT-474. The Coverage tab shows a heatmap of the cells profiled for each drug and cell line. On the Subset builder tab, the assistant panel opens and is asked to build a breast cancer subset. It replies that it selected Organ: Breast in the Subset builder, names BT-474 and HS-578T, estimates 3,094,027 cells across 1,344 samples, and writes R and Python code to pull them, while the tiles change to 3.1M cells and 2 cell lines." title="A quick tour of the Overview and Coverage tabs, then the assistant setting a filter in the Subset builder and reporting what the selection covers." loading="lazy"></a>
<figcaption class="text-sm text-center text-gray-500">A quick tour of the Overview and Coverage tabs, then the assistant setting a filter in the Subset builder and reporting what the selection covers.</figcaption>
</figure>
</div>
<p>The Tahoe app solved one type of scaling problem: accessing and understanding a dataset that is much larger than the application itself. One layer later, the same issue came back, with the <strong>visualization itself</strong> as the bottleneck.</p>
<p><a href="https://opensource.posit.co/software/plotomics/"><strong>plotomics</strong></a> and <a href="https://posit-plotomics-live.share.connect.posit.cloud/" target="_blank" rel="noopener"><strong>Plotomics Live</strong></a> (<a href="https://github.com/samuelbharti/plotomics-live" target="_blank" rel="noopener">source</a>) came out of that bottleneck.</p>
<p>plotomics is a visualization library for computational biology with a shared TypeScript rendering core and wrappers for R and Python. Its 17 components cover embeddings, heatmaps, volcano plots, networks, oncoplots, genomic views, spatial data, survival analysis, and other common biological visualizations. Of those, 15 ship in R, Python, and JavaScript; the two genome browsers are JavaScript and Python only.</p>
<p>For large visualizations, the browser can draw with WebGL or Canvas. Numeric data can also move to the frontend as binary buffers instead of being serialized into huge JSON payloads.</p>
<p>The visualization logic exists once. The R, Python, and JavaScript wrappers all talk to the same rendering engine.</p>
<p>I built <strong>Plotomics Live</strong> to explore how far that model could go inside Shiny.</p>
<p>The app contains 26 biological visualizations, built from those 17 components with several of them used more than once, across single-cell data, spatial transcriptomics, genomics, networks, perturbation data, and other common workflows. Every one of them renders two ways, and an engine toggle puts the GPU-accelerated React component beside the server-rendered ggplot2 image so you can compare them directly.</p>
<p>One page, for example, interactively renders more than half a million real single-cell observations in the browser.</p>
<p>Plotomics Live is where I worked out what Shiny can look like when R remains the analytical engine and the browser does more of the work it is good at.</p>
<div class="not-prose">
<figure>
<a href="https://posit-plotomics-live.share.connect.posit.cloud/" target="_blank" rel="noopener"><img class="h-auto max-w-full rounded-lg" src="https://opensource.posit.co/blog/2026-10-08_genomes-prompts-and-shiny/plotomics-live.gif" alt="An animation of Plotomics Live. On the Visium spatial transcriptomics page, capture spots in eleven color-coded clusters sit over a breast cancer tissue section, above a footer reading 3,798 capture spots, 11 clusters, 72 genes in panel. The spot opacity slider fades the spots to show the H&amp;E-stained tissue underneath, then brings them back. The coloring switches to ERBB2 expression, and the engine toggle swaps the Shiny React component for the ggplot2 (classic) image. The animation then moves to the Protein structure page, where the AlphaFold model of TP53, colored by pLDDT confidence, turns." title="Two of the 26 pages. The engine toggle swaps the React component for the ggplot2 image, over the same server-side computation." loading="lazy"></a>
<figcaption class="text-sm text-center text-gray-500">Two of the 26 pages. The engine toggle swaps the React component for the ggplot2 image, over the same server-side computation.</figcaption>
</figure>
</div>
<h2 id="connecting-biological-information-that-lives-everywhere">Connecting biological information that lives everywhere
</h2>
<p>Scale was only one recurring problem. The other was fragmentation.</p>
<p>A lot of computational biology does not involve one database or one algorithm. It involves assembling an answer from many different resources.</p>
<p>Variant interpretation is a good example. If I am reviewing a variant, I may want information from <a href="https://www.ncbi.nlm.nih.gov/clinvar/" target="_blank" rel="noopener">ClinVar</a>, <a href="https://gnomad.broadinstitute.org/" target="_blank" rel="noopener">gnomAD</a>, <a href="https://www.ensembl.org/" target="_blank" rel="noopener">Ensembl</a>, <a href="https://www.uniprot.org/" target="_blank" rel="noopener">UniProt</a>, <a href="https://alphafold.ebi.ac.uk/" target="_blank" rel="noopener">AlphaFold</a>, <a href="https://platform.opentargets.org/" target="_blank" rel="noopener">Open Targets</a>, <a href="https://www.dbnsfp.org/" target="_blank" rel="noopener">dbNSFP</a>, <a href="https://www.ebi.ac.uk/ProtVar/" target="_blank" rel="noopener">ProtVar</a>, and several other sources.</p>
<p><a href="https://posit-variant-reviewer.share.connect.posit.cloud/" target="_blank" rel="noopener"><strong>Variant Reviewer</strong></a> (<a href="https://github.com/samuelbharti/variant-reviewer" target="_blank" rel="noopener">source</a>) brings those sources together in one reactive workflow.</p>
<p>A researcher can start from a gene, a variant, or both, and the app combines clinical significance, population frequency, prediction scores, gene constraint, protein domains and 3D protein structure.</p>
<div class="not-prose">
<figure>
<a href="https://posit-variant-reviewer.share.connect.posit.cloud/" target="_blank" rel="noopener"><img class="h-auto max-w-full rounded-lg" src="https://opensource.posit.co/blog/2026-10-08_genomes-prompts-and-shiny/variant-reviewer.gif" alt="An animation of Variant Reviewer. Loading the BRAF V600E example fills the gene and variant boxes, and Review fills the cards: BRAF on the gene card, chr7:g.140753336A&gt;T and p.Val600Glu on the variant card, UniProt P15056 at position 600 on the protein card, and conflicting classifications of pathogenicity on the ClinVar card. The assistant on the right is then asked what ClinVar and gnomAD say about this variant, and answers with the ClinVar classification and its conditions and a gnomAD exome frequency of 2 in 1,460,618 alleles." title="One gene, one variant, one page. Each card is a different public service." loading="lazy"></a>
<figcaption class="text-sm text-center text-gray-500">One gene, one variant, one page. Each card is a different public service.</figcaption>
</figure>
</div>
<p>But building Variant Reviewer surfaced another problem. Every external biological service behaves differently: its own identifiers, its own failure modes, its own rate limits. One might return a clean &ldquo;not found&rdquo; response while another throws an error. And one slow or unavailable service should not break the entire application.</p>
<p>Once I started solving those problems in one app, it became apparent that I should not keep solving them separately.</p>
<h2 id="building-the-infrastructure-once">Building the infrastructure once
</h2>
<p>Five packages grew out of problems that kept appearing across the applications.</p>
<h3 id="biobouncer">biobouncer
</h3>
<p><a href="https://opensource.posit.co/software/biobouncer/"><strong>biobouncer</strong></a> handles biological identifier validation. Gene symbols change. Ontology identifiers have different namespaces. Variants can be syntactically valid but biologically meaningless. Public databases each have their own identifier conventions.</p>
<p>biobouncer provides a common layer for validating these inputs using pattern checks, cached resources, remote validation, and existence checks. It also has implementations across R, Python, and TypeScript backed by the same conformance data.</p>
<p>If the same identifier moves through an R pipeline, a Python analysis, and a web interface, I want &ldquo;valid&rdquo; to mean the same thing everywhere.</p>
<h3 id="biohttp">biohttp
</h3>
<p><a href="https://opensource.posit.co/software/biohttp/"><strong>biohttp</strong></a> handles the repetitive infrastructure around external APIs. Retries, throttling, caching, batching, timeouts, circuit breaking, and structured errors should not need to be rebuilt in every scientific application.</p>
<p>It also makes an important distinction between a service successfully returning no biological result and the service itself failing. That sounds small until an application depends on several external databases at once.</p>
<h3 id="bioclients">bioclients
</h3>
<p><a href="https://opensource.posit.co/software/bioclients/"><strong>bioclients</strong></a> sits on top of that transport layer and provides R clients for 29 biological services. Ensembl, UniProt, gnomAD, Open Targets, ClinVar, AlphaFold, and the rest each get a client that understands the structure and meaning of what they return.</p>
<p>Network requests and response parsing are separated so parsers can be tested against stored responses without requiring a live API.</p>
<p>The separation is intentional:</p>
<ul>
<li><strong>biohttp handles how we talk to a service.</strong></li>
<li><strong>bioclients handles what that biological service returns.</strong></li>
</ul>
<h3 id="plotomics">plotomics
</h3>
<p><strong>plotomics</strong> is the exception in this list. I described it above, because it came out of one specific problem before I was thinking about shared infrastructure at all. It belongs here anyway, for the same reason as the rest: the rendering layer only had to be built once.</p>
<h3 id="biocohort">biocohort
</h3>
<p>Before APIs and visualization even matter, most omics projects have another problem: keeping the study organized. Subjects, samples, assays, species, and the manual corrections made along the way often end up spread across spreadsheets and scripts.</p>
<p><a href="https://opensource.posit.co/software/biocohort/"><strong>biocohort</strong></a> provides a validated study representation for keeping those relationships together. The same manifest can describe multiple assay types and organisms, generate pipeline sample sheets, and preserve corrections that would otherwise disappear inside preprocessing code.</p>
<p>This one is particularly close to my own research because these small bookkeeping problems become very expensive once a project grows.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-10-08_genomes-prompts-and-shiny/package-stack.png"
      alt="A diagram. The ten applications are listed as chips inside a dashed box at the top. Below them are five rows, each showing a package&rsquo;s hex sticker next to its name and its job: biobouncer for whether an identifier means anything, biohttp for how we talk to a service, bioclients for what each biological service returns, plotomics for one rendering core in three languages, and biocohort for how a study stays organized."  title="The five packages, and the one question each of them answers." 
      loading="lazy"
    ><figcaption class="text-sm text-center text-gray-500">The five packages, and the one question each of them answers.</figcaption>
  </figure></div>
</p>
<p>This shift from app-specific code to reusable infrastructure was the biggest change in how I approached the internship. Instead of asking, &ldquo;How do I make this app work?&rdquo;, I started asking, &ldquo;Which part of this problem will the next app also need?&rdquo;</p>
<h2 id="from-retrieving-data-to-synthesizing-evidence">From retrieving data to synthesizing evidence
</h2>
<p>Once those pieces became reusable, I could build higher-level research workflows on top of them.</p>
<p><a href="https://posit-gene-list-builder.share.connect.posit.cloud/" target="_blank" rel="noopener"><strong>Gene List Builder</strong></a> (<a href="https://github.com/samuelbharti/gene-list-builder" target="_blank" rel="noopener">source</a>) came from another task I have done manually many times: starting with a disease and assembling a defensible list of associated genes.</p>
<p>The application resolves the disease to an ontology term, queries multiple gene-disease resources, normalizes the results, removes duplicates, and produces a ranked list using transparent and adjustable source weights.</p>
<p>The evidence comes from the underlying databases, the scoring is visible, and the provenance remains available. An optional AI layer can then help with curation after that deterministic evidence-gathering step.</p>
<p><a href="https://posit-genescout.share.connect.posit.cloud/" target="_blank" rel="noopener"><strong>GeneScout</strong></a> (<a href="https://github.com/samuelbharti/genescout" target="_blank" rel="noopener">source</a>) takes the same idea further.</p>
<p>It acts as an evidence-review workbench for evaluating candidate genes in a disease context. Its deterministic layer resolves genes, retrieves public evidence, keeps citations attached, scores the evidence, and applies caveats. An optional agent can then synthesize what was retrieved and suggest follow-up directions.</p>
<p>Both apps rest on the design principle I came away from the summer caring about most:</p>
<p><strong>AI can help interpret scientific evidence without needing to be the source of the evidence.</strong></p>
<div class="not-prose">
<figure>
<a href="https://posit-genescout.share.connect.posit.cloud/" target="_blank" rel="noopener"><img class="h-auto max-w-full rounded-lg" src="https://opensource.posit.co/blog/2026-10-08_genomes-prompts-and-shiny/genescout.gif" alt="An animation of GeneScout. The setup page holds four example gene lists and the study context NF1-associated cancer (MPNST). After Rank genes, the review shows 32 candidates ranked across 12 sources, with EGFR first. Selecting NF1 switches the panel on the right to its composite score of 0.55 and its breakdown by source, from Open Targets association to STRING connectivity. Opening its grounded evidence lists 38 cited items, starting with an Open Targets association with neurofibromatosis type 1." title="Every number in the breakdown traces back to a public source. Curate with AI and Analyze with specialists are separate, optional steps." loading="lazy"></a>
<figcaption class="text-sm text-center text-gray-500">Every number in the breakdown traces back to a public source. Curate with AI and Analyze with specialists are separate, optional steps.</figcaption>
</figure>
</div>
<h2 id="turning-analyses-into-tools-researchers-can-actually-explore">Turning analyses into tools researchers can actually explore
</h2>
<p>I built several more focused applications around common genomics workflows.</p>
<p><a href="https://posit-recount-explorer.share.connect.posit.cloud/" target="_blank" rel="noopener"><strong>Recount Explorer</strong></a> (<a href="https://github.com/samuelbharti/recount-explorer" target="_blank" rel="noopener">source</a>) provides an interactive entry point into <a href="https://rna.recount.bio/" target="_blank" rel="noopener">recount3</a>, allowing researchers to browse uniformly processed RNA-seq studies, inspect quality metrics, explore expression and PCA, and export data, without writing any code.</p>
<p><a href="https://posit-de-explorer.share.connect.posit.cloud/" target="_blank" rel="noopener"><strong>DE Explorer</strong></a> (<a href="https://github.com/posit-dev/shiny-showcase-bioinformatics/tree/main/apps/de-explorer" target="_blank" rel="noopener">source</a>) makes differential-expression results interactive, moving from a PCA of sample-level structure to a volcano plot, a filterable results table, and a heatmap of the genes you select.</p>
<p><a href="https://posit-signature-scoring.share.connect.posit.cloud/" target="_blank" rel="noopener"><strong>Signature Scoring</strong></a> (<a href="https://github.com/posit-dev/shiny-showcase-bioinformatics/tree/main/apps/signature-scoring" target="_blank" rel="noopener">source</a>) summarizes expression profiles into pathway-level scores and lets researchers compare those scores across groups before drilling down into the genes driving a pathway.</p>
<p><a href="https://posit-drug-perturbation.share.connect.posit.cloud/" target="_blank" rel="noopener"><strong>Drug Perturbation</strong></a> (<a href="https://github.com/posit-dev/shiny-showcase-bioinformatics/tree/main/apps/drug-perturbation" target="_blank" rel="noopener">source</a>) explores connectivity between gene-expression signatures and reference perturbations, including potential mimics and reversers across different cellular contexts. Its reference panel is synthetic, built over the TCGA-BRCA gene space.</p>
<p><a href="https://posit-genome-explorer.share.connect.posit.cloud/" target="_blank" rel="noopener"><strong>Genome Explorer</strong></a> (<a href="https://github.com/posit-dev/shiny-showcase-bioinformatics/tree/main/apps/genome-explorer" target="_blank" rel="noopener">source</a>) places recurrent variants into an interactive genomic view and lets users move from cohort-level patterns to individual loci.</p>
<p>These five are deliberately narrower than the applications above. An analysis can end in a tool: a thin Shiny layer turns a computational result into something the researcher who understands the biology can actually explore.</p>
<div class="not-prose">
<figure>
<a href="https://posit-shiny-showcase-bioinformatics.share.connect.posit.cloud/" target="_blank" rel="noopener"><img class="h-auto max-w-full rounded-lg" src="https://opensource.posit.co/blog/2026-10-08_genomes-prompts-and-shiny/focused-apps.png" alt="Five application screenshots in a labelled grid: Recount Explorer, DE Explorer, Signature Scoring, Drug Perturbation, and Genome Explorer. Each shows a populated dashboard, in order a study catalogue, a PCA with threshold controls, a pathway activity contrast, a connectivity leaderboard, and a genome browser above a variant table." title="Five smaller applications, each wrapped around one common genomics workflow." loading="lazy"></a>
<figcaption class="text-sm text-center text-gray-500">Five smaller applications, each wrapped around one common genomics workflow.</figcaption>
</figure>
</div>
<h2 id="working-in-open-source">Working in open source
</h2>
<p>I had worked on open-source software before, but working inside Posit&rsquo;s Open Source group changed how I thought about building research software.</p>
<p>Code review was where a lot of the interesting work happened.</p>
<p>I spent much more time thinking about API design, failure modes, backwards compatibility, and what another developer would experience after I stopped actively working on a project.</p>
<p>That distinction became very clear over the summer:</p>
<p><strong>Making something run once is different from making something other people can depend on.</strong></p>
<p>I got to move well outside the R layer I knew best. Across the summer I worked with R, Python, JavaScript, TypeScript, React, WebGL, Shiny internals, and newer work around <a href="https://github.com/posit-dev/shinyreact" target="_blank" rel="noopener">shinyreact</a>, which the Shiny team introduced in <a href="https://opensource.posit.co/blog/2026-09-30_introducing-shinyreact/">Introducing shinyreact</a> at the end of September.</p>
<p>Starting the internship at Open Source Work Week in Boston made the whole experience even better. Meeting everyone in person at the beginning made the later remote collaboration feel much more natural.</p>
<p>I am very grateful to <a href="https://opensource.posit.co/people/barret-schloerke/"><strong>Barret Schloerke</strong></a>, <a href="https://opensource.posit.co/people/carson-sievert/"><strong>Carson Sievert</strong></a>, <a href="https://opensource.posit.co/people/joe-cheng/"><strong>Joe Cheng</strong></a>, the rest of the Shiny team, and everyone across Posit who provided feedback, answered questions, challenged ideas, or helped me understand another part of the stack.</p>
<p>I learned a lot from being able to build freely while surrounded by people who know these tools from the inside.</p>
<h2 id="next-for-these-projects">Next for these projects
</h2>
<p>The internship ended, but I do not really think of these projects as finished. They are open source, and many of them solve problems I still encounter in my own research. I hope researchers use them, find things I did not anticipate, and help shape where they go next.</p>
<p>The applications and supporting packages are collected in the <a href="https://github.com/posit-dev/shiny-showcase-bioinformatics" target="_blank" rel="noopener"><strong>Shiny Showcase: Bioinformatics</strong></a> repository, with source code, documentation, examples, and citation information for the individual projects. The <a href="https://posit-shiny-showcase-bioinformatics.share.connect.posit.cloud/" target="_blank" rel="noopener">live gallery</a> links every app to a running deployment you can open in a browser.</p>
<p>I presented this as a talk in August 2026. <a href="https://www.samuelbharti.com/genomes-prompts-shiny/" target="_blank" rel="noopener">The deck</a> covers the same projects in slides, and its <a href="https://github.com/samuelbharti/genomes-prompts-shiny" target="_blank" rel="noopener">source</a> is public.</p>
<p>If you work in computational biology, genomics, or research software, I would genuinely love to hear what is useful and what is missing.</p>
<p>Try an app, use one of the packages, or open an issue if something breaks or could work better. Pull requests are welcome. Or tell me about the research-software problem you keep solving manually.</p>
<p>A lot of this work started exactly that way. I noticed recurring problems in my own research that felt harder than they should be, and I tried to build the tools I wished I already had.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-10-08_genomes-prompts-and-shiny/featured.png" length="413419" type="image/png" />
    </item>
    <item>
      <title>posit::glimpse() Newsletter – October 2026</title>
      <link>https://opensource.posit.co/blog/2026-10-07_glimpse-10-2026/</link>
      <pubDate>Wed, 07 Oct 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-10-07_glimpse-10-2026/</guid>
      <dc:creator>Isabella Velásquez</dc:creator><description><![CDATA[<p>Hello, everyone! We’ve officially wrapped up posit::conf(2026), and to everyone who joined us in Houston or online, thank you for making it incredible 💙🧡 Between the keynote reveals, lightning talks, and workshops, it was a lot to take in! In this edition, we’ve rounded up all the major news and announcements in one place.</p>
<p>Want to catch up on talks? While we prepare the videos for YouTube, all session recordings are available now on the event portal:</p>
<ul>
<li>Register / log in at <a href="https://conf.posit.co" target="_blank" rel="noopener">conf.posit.co</a></li>
<li>Go to &ldquo;Schedule&rdquo;</li>
<li>Select any session and click &ldquo;View recording&rdquo;</li>
</ul>









  





  







<div class="not-prose grid grid-cols-2 md:grid-cols-3 gap-4 mt-2 mb-6">
  
    
    
    
    

    
    
      
      
      
      
      
        
        
        
          
            
            
            
            
            
          
        
          
            
            
            
              
              
            
            
            
          
        
          
            
            
            
              
              
            
            
            
          
        
          
        
      
      <figure class="m-0">
        <img
          src="https://opensource.posit.co/blog/2026-10-07_glimpse-10-2026/photos/photo1_hu_6fb08991979a00f7.jpeg"
          alt="photos/photo1.jpeg"
          class="w-full aspect-[4/3] object-cover rounded-lg cursor-pointer outline-none"
          style="object-position: center"
          loading="lazy"
          
          data-gallery-group="gallery-0"
          data-gallery-src="https://opensource.posit.co/blog/2026-10-07_glimpse-10-2026/photos/photo1.jpeg"
          data-gallery-caption=""
          
          data-gallery-webp="/blog/2026-10-07_glimpse-10-2026/photos/photo1_hu_c111386dc651e3ed.webp 640w, /blog/2026-10-07_glimpse-10-2026/photos/photo1_hu_a70580f20689bc35.webp 960w, /blog/2026-10-07_glimpse-10-2026/photos/photo1_hu_5f3f89496c54f45.webp 1280w"
          data-gallery-jpeg="/blog/2026-10-07_glimpse-10-2026/photos/photo1_hu_3733283b694e3e95.jpeg 640w, /blog/2026-10-07_glimpse-10-2026/photos/photo1_hu_e41a7fc1c4c722ad.jpeg 960w, /blog/2026-10-07_glimpse-10-2026/photos/photo1_hu_abfc3a8d2a5de3c.jpeg 1280w"
          role="button"
          tabindex="0"
          
        >
        
      </figure>
    
  
    
    
    
    

    
    
      
      
      
      
      
        
        
        
          
            
            
            
            
            
          
        
          
            
            
            
              
              
            
            
            
          
        
          
            
            
            
              
              
            
            
            
          
        
          
        
      
      <figure class="m-0">
        <img
          src="https://opensource.posit.co/blog/2026-10-07_glimpse-10-2026/photos/photo2_hu_fd39cc2dae09a981.jpeg"
          alt="photos/photo2.jpeg"
          class="w-full aspect-[4/3] object-cover rounded-lg cursor-pointer outline-none"
          style="object-position: center"
          loading="lazy"
          
          data-gallery-group="gallery-0"
          data-gallery-src="https://opensource.posit.co/blog/2026-10-07_glimpse-10-2026/photos/photo2.jpeg"
          data-gallery-caption=""
          
          data-gallery-webp="/blog/2026-10-07_glimpse-10-2026/photos/photo2_hu_231fb87d898db1c7.webp 640w, /blog/2026-10-07_glimpse-10-2026/photos/photo2_hu_ca84e3ba03e94d6f.webp 960w, /blog/2026-10-07_glimpse-10-2026/photos/photo2_hu_ae974601d02b8171.webp 1280w"
          data-gallery-jpeg="/blog/2026-10-07_glimpse-10-2026/photos/photo2_hu_2037ed65cf9451e5.jpeg 640w, /blog/2026-10-07_glimpse-10-2026/photos/photo2_hu_d9de34e3c9983c0d.jpeg 960w, /blog/2026-10-07_glimpse-10-2026/photos/photo2_hu_c717be003de21464.jpeg 1280w"
          role="button"
          tabindex="0"
          
        >
        
      </figure>
    
  
    
    
    
    

    
    
      
      
      
      
      
        
        
        
          
            
            
            
            
            
          
        
          
            
            
            
              
              
            
            
            
          
        
          
            
            
            
              
              
            
            
            
          
        
          
        
      
      <figure class="m-0">
        <img
          src="https://opensource.posit.co/blog/2026-10-07_glimpse-10-2026/photos/photo3_hu_5cd54b096e78ab13.jpeg"
          alt="photos/photo3.jpeg"
          class="w-full aspect-[4/3] object-cover rounded-lg cursor-pointer outline-none"
          style="object-position: center"
          loading="lazy"
          
          data-gallery-group="gallery-0"
          data-gallery-src="https://opensource.posit.co/blog/2026-10-07_glimpse-10-2026/photos/photo3.jpeg"
          data-gallery-caption=""
          
          data-gallery-webp="/blog/2026-10-07_glimpse-10-2026/photos/photo3_hu_5851fec0224c6f18.webp 640w, /blog/2026-10-07_glimpse-10-2026/photos/photo3_hu_e500064cd3693752.webp 960w, /blog/2026-10-07_glimpse-10-2026/photos/photo3_hu_8d370cb3ea37eb67.webp 1280w"
          data-gallery-jpeg="/blog/2026-10-07_glimpse-10-2026/photos/photo3_hu_27b827b979b5a5fb.jpeg 640w, /blog/2026-10-07_glimpse-10-2026/photos/photo3_hu_40bd6a7f550a9dde.jpeg 960w, /blog/2026-10-07_glimpse-10-2026/photos/photo3_hu_c6555aa88282daa2.jpeg 1280w"
          role="button"
          tabindex="0"
          
        >
        
      </figure>
    
  
</div>

<h2 id="announcing-the-winners-of-the-posit-impact-awards">Announcing the winners of the Posit Impact Awards
</h2>
<p>We were thrilled to launch the inaugural Posit Impact Awards to celebrate stories of measurable, meaningful change across our community. At posit::conf(2026), we were honored to announce our eight winners, each receiving a conference pass to join us in Houston!</p>
<p>A huge congratulations to all our winners, and a heartfelt thank you for the incredible work you do (and for sharing it with us)!</p>
<ul>
<li>Explore their stories in the <a href="https://posit.co/blog/2026-posit-impact-awards" target="_blank" rel="noopener">2026 Posit Impact Awards</a> blog post and join us at the <a href="https://posit.co/data-science-hangout" target="_blank" rel="noopener">Data Science Hangout</a> to hear from other Impact Award winners and nominees featured in the next few weeks.</li>
</ul>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-10-07_glimpse-10-2026/images/impact-awards.jpeg"
      alt="Graphic presenting the 2026 Posit Impact Awards winners: Paulo Villarroel (Mission-Critical), Chendi Liao (Transformation), Claudio T. Rebelo (Scale &amp; Reach), Skyler Elmstrom (Resilient Impact), Jon Harmon (Community Builder), Christian Martinez (Open Source), and Amanda Perez &amp; Romina Mir (ROI &amp; Efficiency)." 
      loading="lazy"
    >
  </figure></div>
</p>
<h2 id="open-source-updates-from-positconf2026">Open Source updates from posit::conf(2026)
</h2>
<h3 id="introducing-commons">Introducing commons
</h3>
<p><a href="https://posit-dev.github.io/commons/" target="_blank" rel="noopener">commons</a> 0.1.0 introduces a new framework for building trustworthy self-service data analysis agents in R and Python. The package implements a three-tier trust labeling system that transparently indicates whether agents invoke trusted code directly, write new code justified by vetted context, or provide untrusted answers. Built on Posit’s LLM stack (<a href="https://ellmer.tidyverse.org/" target="_blank" rel="noopener">ellmer</a>, <a href="https://posit-dev.github.io/chatlas/" target="_blank" rel="noopener">chatlas</a>, <a href="https://posit-dev.github.io/shinychat/" target="_blank" rel="noopener">shinychat</a>), commons enables teams to convert existing analytical code into AI agent capabilities while maintaining transparency about code sources.</p>
<ul>
<li>Learn more about <a href="https://opensource.posit.co/blog/2026-09-15_commons-0-1-0/" target="_blank" rel="noopener">commons 0.1.0</a>.</li>
<li>Join Sara &amp; Simon&rsquo;s virtual webinar on October 28th to learn more about commons. <a href="https://events.zoom.us/ev/Ajss5j9VeRMe0zw-AtFKf7AUAsthzYhaYjYPeEIu1uYAQe1K0ud1~Agb_hSt1UGxd9fUyIDC32e6jAdEtbl7G_AaLZUYhRvyZR5cGpY5Kp_A0-w?mkt_tok=NzA5LU5YTi03MDYAAAGhT3GTQTQ5s7LDxqZGUAE1zALOLmEDysbAsOyvVdJ9P72DB_IRwtWRTED6kyJA-j-YKF7WxCTY_ZlZ4rTkiyY" target="_blank" rel="noopener">Register here</a>.</li>
</ul>
<h3 id="quarto-hub">Quarto Hub
</h3>
<p>Quarto Hub is a new hosted platform from Posit. In Quarto Hub, you can create, edit, comment on, and publish your <a href="https://quarto.org/" target="_blank" rel="noopener">Quarto projects</a> (single documents, presentations, websites) collaboratively and in real time.</p>
<ul>
<li>Watch Carlos Scheidegger&rsquo;s talk, &ldquo;Quarto Hub: Collaboratively edit, create, and share documents&rdquo;, on the event portal.</li>
</ul>
<script src="https://fast.wistia.com/player.js" async></script><script src="https://fast.wistia.com/embed/gd8zwcsex7.js" async type="module"></script><style>wistia-player[media-id='gd8zwcsex7']:not(:defined) { background: center / contain no-repeat url('https://fast.wistia.com/embed/medias/gd8zwcsex7/swatch'); display: block; filter: blur(5px); padding-top:56.25%; }</style> <wistia-player media-id="gd8zwcsex7" aspect="1.7777777777777777"></wistia-player>
<h3 id="skilift-and-skipatrol">skiLift and skiPatrol
</h3>
<p>Posit has taken over maintenance over two R packages that provide comprehensive tools to work and collaborate within Snowflake’s data platform and ML ecosystem:</p>
<ul>
<li><a href="https://github.com/posit-dev/skiLift/" target="_blank" rel="noopener">skiLift</a>: a DBI-compliant Snowflake connector written in R, that authenticates and communicates via the Snowflake SQL API (REST).</li>
<li><a href="https://github.com/posit-dev/skiPatrol/" target="_blank" rel="noopener">skiPatrol</a>: an R interface to Snowflake&rsquo;s ML platform: model registry, feature store, experiments, model monitoring, and container services.</li>
</ul>
<p>Learn more in the <a href="https://posit.co/blog/r-meet-snowflake-introducing-posits-skilift-and-skipatrol-packages" target="_blank" rel="noopener">R, meet Snowflake: Introducing Posit’s skiLift and skiPatrol Packages</a> blog post.</p>
<h2 id="even-more-open-source-updates">Even more open source updates
</h2>
<p>Beyond all the exciting conf news, September was packed with major product updates and community highlights across the board!</p>
<h3 id="at-the-database-layer">At the database layer
</h3>
<h4 id="ggsql-050-readers-writers-and-beta-status">ggsql 0.5.0: Readers, Writers, and Beta status
</h4>
<p><a href="https://ggsql.org/" target="_blank" rel="noopener">ggsql</a> is a tool that implements a grammar of graphics for SQL, enabling ggplot2-style data visualization directly from database queries. ggsql 0.5.0 reaches beta status with major improvements to database connectivity and rendering capabilities. The release introduces hybrid-reader mode for read-only connections with cache support, replaces the Vega-Lite renderer with a custom writer supporting PNG, JPEG, SVG, and PDF output, and adds rich text support through extended markdown. New features include caption support, minor breaks on scales, and true variable line aesthetics with gradients.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-09-24_ggsql_0_5_0/" target="_blank" rel="noopener">ggsql 0.5.0: Readers, Writers, and Beta status</a> blog post.</li>
</ul>
<h4 id="orbital-070">orbital 0.7.0
</h4>
<p><a href="https://orbital.tidymodels.org/" target="_blank" rel="noopener">orbital</a> enables the running predictions of tidymodels workflows inside databases. orbital 0.7.0 and tidypredict 1.2.0 massively expand in-database prediction capabilities for tidymodels workflows. The release adds 27 new model/engine combinations including discriminant analysis, naive Bayes, neural networks, SVMs, partial least squares, and ensemble methods. tidypredict now functions as a developer toolkit with new generics for programmatic use, and improved error messaging guides users when probability predictions aren’t available.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-09-09_orbital-0-7-0/" target="_blank" rel="noopener">orbital 0.7.0</a> blog post.</li>
</ul>
<h3 id="ai-powered-r-tools">AI-powered R tools
</h3>
<h4 id="ellmer-050">ellmer 0.5.0
</h4>
<p><a href="https://ellmer.tidyverse.org/" target="_blank" rel="noopener">ellmer</a> makes it easy to use large language models (LLM) from R. ellmer 0.5.0 brings major improvements for working with large language models in R. New file handling functions reduce token costs by efficiently managing documents. The release adds citation tracking from web search tools, token counting for cost prediction, and streaming structured output support. Developer features include <code>tool_context()</code> for accessing request metadata and new lifecycle hooks for building agents.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-09-14_ellmer-0-5-0/" target="_blank" rel="noopener">ellmer 0.5.0</a> blog post.</li>
</ul>
<h4 id="vitals-040">vitals 0.4.0
</h4>
<p><a href="https://vitals.tidyverse.org/" target="_blank" rel="noopener">vitals</a> is a framework for large language model evaluation in R. vitals 0.4.0 brings significant performance improvements and new agent comparison capabilities for LLM evaluation in R. The release includes claude_code() and codex() helpers for benchmarking custom ellmer-built agents against leading coding agents, plus vitals_log_read() for loading log files back into resumable Chat objects. Log files are now ~4x smaller and the log viewer is substantially faster.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-09-03_vitals-0-4-0/" target="_blank" rel="noopener">vitals 0.4.0</a> blog post.</li>
</ul>
<h3 id="our-stuff-is-so-shiny">Our stuff is so Shiny
</h3>
<p>Check out all the major work done by the Shiny team in September!</p>
<h4 id="introducing-shinyreact-react-ui-backed-by-a-shiny-server">Introducing shinyreact: React UI backed by a Shiny server
</h4>
<p><a href="https://posit-dev.github.io/shinyreact/" target="_blank" rel="noopener">shinyreact</a> enables developers to build Shiny applications with React-based UIs while keeping Shiny’s reactive computation on the server. The package provides two primary React hooks for client-server communication, gives access to the entire npm ecosystem of React components, and includes AI Agent Skills for building and converting apps.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-09-30_introducing-shinyreact/" target="_blank" rel="noopener">Introducing shinyreact: React UI backed by a Shiny server </a> blog post.</li>
</ul>
<h4 id="shiny-for-python-18">Shiny for Python 1.8
</h4>
<p><a href="https://shiny.posit.co/py/" target="_blank" rel="noopener">Shiny for Python</a> 1.8 introduces in-memory server testing without browsers, enabling developers to test reactive logic with pytest using a mock connection. The release adds <code>ui.page_html()</code> for using complete HTML documents from bundlers like Vite, and <code>session.allow_reconnect()</code> to maintain live sessions after dropped websocket connections. Additional improvements include deprecation of <code>ui.output_text_verbatim()</code> in favor of clearer naming, reactive value destruction fixes, and multiple bug fixes for downloads, navsets, and data frames.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-09-22_shiny-python-1-8/" target="_blank" rel="noopener">Shiny for Python 1.8</a> blog post.</li>
</ul>
<h4 id="multiple-tables-saved-conversations-and-take-home-dashboards-querychat-r-040-and-python-090">Multiple tables, saved conversations, and take-home dashboards: querychat R 0.4.0 and Python 0.9.0
</h4>
<p><a href="https://posit-dev.github.io/querychat/" target="_blank" rel="noopener">querychat</a> facilitates safe and reliable natural language exploration of tabular data, powered by SQL and large language models (LLMs). querychat R 0.4.0 and Python 0.9.0 introduce multiple table support with automatic joins, persistent conversation history across sessions, and the <code>/handoff</code> command for exporting chats as downloadable Quarto dashboards, Shiny apps, or marimo notebooks. The release builds on shinychat’s full-page layout with editable messages and file attachments, adds YAML-based data dictionaries for context, and integrates with pins boards for chatting with pinned data.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-09-29_querychat-tables-handoff/" target="_blank" rel="noopener">Multiple tables, saved conversations, and take-home dashboards: querychat R 0.4.0 and Python 0.9.0</a> blog post.</li>
</ul>
<h4 id="complete-chat-applications-in-shinychat-r-050-and-python-071">Complete chat applications in shinychat: R 0.5.0 and Python 0.7.1
</h4>
<p><a href="https://posit-dev.github.io/shinychat/" target="_blank" rel="noopener">shinychat</a> provides a Shiny toolkit for building generative AI applications like chatbots and streaming content. shinychat R 0.5.0 and Python 0.7.1 deliver complete chat application capabilities with conversation history, message editing and branching, greetings, file attachments, and slash commands. The new <code>page_chat()</code> layout provides full-window interfaces with integrated navigation, tool displays, citations, and artifact previews. Features include persistent conversation storage, search and organization tools, streaming responses with thinking panels, and toolbars for contextual actions.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/" target="_blank" rel="noopener">Complete chat applications in shinychat: R 0.5.0 and Python 0.7.1</a> blog post.</li>
</ul>
<h2 id="positconf2027-will-be-in-seattle-wa-from-sept-13-15">posit::conf(2027) will be in Seattle, WA from Sept 13-15
</h2>
<p>Save the date: We&rsquo;re heading back to the Pacific Northwest for posit::conf(2027)! <a href="https://posit.co/about/subscription-management" target="_blank" rel="noopener">Subscribe to &ldquo;Events&rdquo;</a> to get notified when registration opens.</p>
<p>In the meantime, join us for our ongoing virtual events:</p>
<ul>
<li><a href="https://pos.it/dsh" target="_blank" rel="noopener">Data Science Hangout</a> for casual conversations with leaders in the data space</li>
<li><a href="https://pos.it/dslab" target="_blank" rel="noopener">Data Science Lab</a> for live coding sessions with practitioners building cool things with code</li>
</ul>
<p>Hope to see you there!</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-10-07_glimpse-10-2026/glimpse-conf.jpg" length="90759" type="image/jpeg" />
    </item>
    <item>
      <title>Introducing shinyreact: React UI backed by a Shiny server</title>
      <link>https://opensource.posit.co/blog/2026-09-30_introducing-shinyreact/</link>
      <pubDate>Wed, 30 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-09-30_introducing-shinyreact/</guid>
      <dc:creator>Barret Schloerke</dc:creator><description><![CDATA[<p>We&rsquo;re excited to introduce <a href="https://posit-dev.github.io/shinyreact/" target="_blank" rel="noopener">shinyreact</a>, a new package for R and Python. It lets you write the UI of a Shiny app in React, with any component library on npm, while reducing your Shiny server code to data-only logic.</p>
<p>shinyreact splits a Shiny app along a clean line. The Shiny server does reactive computation. The UI is a <a href="https://react.dev" target="_blank" rel="noopener">React</a> client that you own. shinyreact is the bridge between them, and it ships zero UI components of its own.</p>
<p>You can install it from CRAN or PyPI:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-1" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-1-1">R</a></li>
<li><a href="#tabset-1-2">Python</a></li>
</ul>
<div id="tabset-1-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;shinyreact&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
</div>
<div id="tabset-1-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install shinyreact</span></span></code></pre></div></div>
</div>
</div>
<p>shinyreact is new, and so is the way of building Shiny apps it proposes. We intend to keep the API small, but expect it to evolve as we learn from early adopters.</p>
<h2 id="shiny--react">Shiny + React?
</h2>
<p>For most Shiny apps, defining the UI in your app.py or app.R file allows you to construct a complete, production-ready app in just a few lines of code.</p>
<p>The trouble starts when the design asks for something Shiny and <a href="https://rstudio.github.io/bslib/" target="_blank" rel="noopener">bslib</a> don&rsquo;t have: a unique layout, richer interaction, or a component from a non-Bootstrap design system. At that point, Shiny hasn&rsquo;t had the right tool for the job.</p>
<p>React is that tool, for three reasons:</p>
<ul>
<li><strong>Ecosystem.</strong> React is the most widely used UI library on the web. Design systems, charts, tables, and maps are all one <code>npm install</code> away.</li>
<li><strong>The right model.</strong> React components are functions of state, which fits Shiny&rsquo;s reactive model naturally. When the server sends new data, the UI re-renders efficiently.</li>
<li><strong>AI assistance.</strong> LLMs have trained on an enormous amount of React code. Ask a frontier agent for a UI and it will produce better React than it will bespoke Shiny UI. This aligns with Shiny&rsquo;s goal that app developers should never be required to write low-level HTML or JavaScript themselves.</li>
</ul>
<p>Later in the post, we&rsquo;ll discuss a genomics app that renders a 584,000-cell UMAP on the GPU from a plain Shiny server. First, the basics.</p>
<h2 id="old-faithful-with-shinyreact">Old Faithful with shinyreact
</h2>
<p>Here is the classic Old Faithful histogram app as a shinyreact app:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-2" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-2-1">R</a></li>
<li><a href="#tabset-2-2">Python</a></li>
</ul>
<div id="tabset-2-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">shiny</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">shinyreact</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Set up the page UI using shinyreact</span>
</span></span><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="nf">page_react</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">x</span> <span class="o">&lt;-</span> <span class="n">faithful</span><span class="o">$</span><span class="n">waiting</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">breaks</span> <span class="o">&lt;-</span> <span class="nf">reactive</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nf">seq</span><span class="p">(</span><span class="nf">min</span><span class="p">(</span><span class="n">x</span><span class="p">),</span> <span class="nf">max</span><span class="p">(</span><span class="n">x</span><span class="p">),</span> <span class="n">length.out</span> <span class="o">=</span> <span class="n">input</span><span class="o">$</span><span class="n">bin_count</span> <span class="o">+</span> <span class="m">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Use `reactive_output()` to send data to the client</span>
</span></span><span class="line"><span class="cl">  <span class="n">output</span><span class="o">$</span><span class="n">dist_data</span> <span class="o">&lt;-</span> <span class="nf">reactive_output</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="n">bins</span> <span class="o">&lt;-</span> <span class="nf">hist</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">breaks</span> <span class="o">=</span> <span class="nf">breaks</span><span class="p">(),</span> <span class="n">plot</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">list</span><span class="p">(</span><span class="n">breaks</span> <span class="o">=</span> <span class="nf">I</span><span class="p">(</span><span class="n">bins</span><span class="o">$</span><span class="n">breaks</span><span class="p">),</span> <span class="n">counts</span> <span class="o">=</span> <span class="nf">I</span><span class="p">(</span><span class="n">bins</span><span class="o">$</span><span class="n">counts</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span></span></span></code></pre></div></div>
</div>
<div id="tabset-2-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="nn">pd</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny.express</span> <span class="kn">import</span> <span class="nb">input</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shinyreact</span> <span class="kn">import</span> <span class="n">reactive_output</span><span class="p">,</span> <span class="n">set_react_page</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Set up the page UI using shinyreact</span>
</span></span><span class="line"><span class="cl"><span class="n">set_react_page</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">x</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s2">&#34;faithful.csv&#34;</span><span class="p">)[</span><span class="s2">&#34;waiting&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">to_numpy</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@reactive_output</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">dist_data</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">breaks</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">linspace</span><span class="p">(</span><span class="n">x</span><span class="o">.</span><span class="n">min</span><span class="p">(),</span> <span class="n">x</span><span class="o">.</span><span class="n">max</span><span class="p">(),</span> <span class="nb">input</span><span class="o">.</span><span class="n">bin_count</span><span class="p">()</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">counts</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">histogram</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">bins</span><span class="o">=</span><span class="n">breaks</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;breaks&#34;</span><span class="p">:</span> <span class="n">breaks</span><span class="o">.</span><span class="n">tolist</span><span class="p">(),</span> <span class="s2">&#34;counts&#34;</span><span class="p">:</span> <span class="n">counts</span><span class="o">.</span><span class="n">tolist</span><span class="p">()}</span></span></span></code></pre></div></div>
</div>
</div>
<p>Two things are different from a traditional Shiny app.</p>
<ol>
<li><strong>The UI is one line.</strong> <code>page_react()</code> (or <code>set_react_page()</code> in Shiny Express) serves the React client that lives in your app&rsquo;s <code>www/</code> directory. There&rsquo;s no <code>sliderInput()</code> or <code>plotOutput()</code>. The <code>www/</code> directory contains the static assets for the React app: <code>ui.js</code> and <code>ui.css</code> (when available).</li>
<li><strong>The output is data.</strong> <code>reactive_output()</code> has no matching UI function. This is a new concept for the Shiny ecosystem! <code>renderPlot()</code> sends an image for <code>plotOutput()</code> to place. <code>reactive_output()</code> sends plain JSON, here the histogram&rsquo;s <code>breaks</code> and <code>counts</code>. Like any render function, <code>reactive_output()</code> re-executes each time its reactive dependencies change. The server sends facts and React decides how to present them.</li>
</ol>
<p>Now the client:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-tsx" data-lang="tsx"><span class="line"><span class="cl"><span class="c1">// src/ui.tsx (which compiles to www/ui.js)
</span></span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">App() {</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="p">[</span><span class="nx">binCount</span><span class="p">,</span> <span class="nx">setBinCount</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useShinyInput</span><span class="p">(</span><span class="s2">&#34;bin_count&#34;</span><span class="p">,</span> <span class="mi">30</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">bins</span> <span class="o">=</span> <span class="nx">useShinyOutputValue</span><span class="p">(</span><span class="s2">&#34;dist_data&#34;</span><span class="p">,</span> <span class="kc">null</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">main</span> <span class="na">className</span><span class="o">=</span><span class="s">&#34;layout&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="o">=</span><span class="s">&#34;bin_count&#34;</span><span class="p">&gt;</span><span class="nb">Number</span> <span class="k">of</span> <span class="nx">bins</span><span class="o">:</span><span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">input</span>
</span></span><span class="line"><span class="cl">        <span class="na">id</span><span class="o">=</span><span class="s">&#34;bin_count&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="na">type</span><span class="o">=</span><span class="s">&#34;range&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="na">min</span><span class="o">=</span><span class="p">{</span><span class="mi">1</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="na">max</span><span class="o">=</span><span class="p">{</span><span class="mi">50</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="na">value</span><span class="o">=</span><span class="p">{</span><span class="nx">binCount</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="na">onChange</span><span class="o">=</span><span class="p">{(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">setBinCount</span><span class="p">(</span><span class="nb">Number</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">))}</span>
</span></span><span class="line"><span class="cl">      <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">Histogram</span> <span class="na">bins</span><span class="o">=</span><span class="p">{</span><span class="nx">bins</span><span class="p">}</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">main</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<figure>
<img src="https://opensource.posit.co/blog/2026-09-30_introducing-shinyreact/hello-app.gif" data-fig-alt="A Shiny app with a range slider labeled Number of bins on the left and a histogram of Old Faithful waiting times on the right. As the slider moves from 30 to 8, 45, 20, and back to 30, the histogram redraws with that many bars and the caption updates to match." alt="The Old Faithful app: dragging the bin-count slider re-renders the histogram as the server sends new breaks and counts." />
<figcaption aria-hidden="true">The Old Faithful app: dragging the bin-count slider re-renders the histogram as the server sends new breaks and counts.</figcaption>
</figure>
<p>If you&rsquo;ve written React before, this is an ordinary component. <code>Histogram</code> is whatever you like: a hand-written SVG, a charting library, or a component from your design system. The only shinyreact-specific parts are two hooks:</p>
<ul>
<li><code>useShinyInput()</code> works like React&rsquo;s <code>useState()</code>, except that the value is also sent to the server as <code>input$bin_count</code> (or <code>input.bin_count()</code> in Python). Calling <code>setBinCount()</code> updates the UI and triggers the server&rsquo;s reactive graph.</li>
<li><code>useShinyOutputValue()</code> reads the value of a <code>reactive_output()</code>. When the server recomputes <code>dist_data</code>, the component re-renders with the new data.</li>
</ul>
<p>Those two hooks cover the vast majority of apps.</p>
<h2 id="ids-and-json-are-the-contract">IDs and JSON are the contract
</h2>
<p>The client and server share exactly two things: IDs and JSON values. If you write the client in TypeScript, each hook takes an optional type for its value, such as <code>useShinyInput&lt;number&gt;(&quot;bin_count&quot;, 30)</code>, and your editor will then flag a mismatch that <code>Shiny.setInputValue()</code> never could.</p>
<p>Here is the full round trip for the Old Faithful app:</p>
<ol>
<li>The client calls <code>useShinyInput(&quot;bin_count&quot;, 30)</code>, which sends <code>{&quot;bin_count&quot;: 30}</code> to the server.</li>
<li>The server reads <code>input$bin_count</code>, runs its reactive graph, and computes <code>dist_data</code> using <code>reactive_output()</code>.</li>
<li>The client receives the <code>dist_data</code> result as <code>{&quot;dist_data&quot;: {&quot;breaks&quot;: [...], &quot;counts&quot;: [...]}}</code>, and <code>useShinyOutputValue(&quot;dist_data&quot;)</code> hands it to React.</li>
</ol>
<p>That narrow boundary is what makes a shinyreact app easy to reason about. The server doesn&rsquo;t know or care how the histogram is drawn, and the client doesn&rsquo;t know how the bins are computed. Each side can be reviewed, tested, and rewritten independently, whether a person or an agent wrote it.</p>
<p>When you need more, a few other hooks are available. <code>useShinyOutputStatus()</code> tells you when an output is recalculating so you can show a skeleton, and <code>useShinyMessageHandler()</code> receives one-off messages pushed from the server with <code>send_message()</code>. See the <a href="https://posit-dev.github.io/shinyreact/js/" target="_blank" rel="noopener">JavaScript API reference</a> for the full list.</p>
<h2 id="you-dont-have-to-write-the-react-yourself">You don&rsquo;t have to write the React yourself
</h2>
<p>A fair reaction to all of this is, &ldquo;but I chose Shiny so I wouldn&rsquo;t have to write JavaScript!&rdquo;. That&rsquo;s still the goal. What&rsquo;s changed is that today&rsquo;s AI agents are very good at writing React, far better than they are at writing custom Shiny UI, because there is so much more React in the world for them to learn from.</p>
<p>With shinyreact, your job is to own the server, which is where your data and domain logic live, and to describe and review the UI. To make that concrete, both packages ship <a href="https://posit-dev.github.io/shinyreact/articles/agent-skills.html" target="_blank" rel="noopener">Agent Skills</a>:</p>
<ul>
<li><strong><code>shinyreact-build-app</code></strong> scaffolds a new shinyreact app from a description.</li>
<li><strong><code>shinyreact-convert-app</code></strong> opens an existing Shiny app in a browser, describes what it does in plain English, and then rewrites the UI in React against the same server.</li>
</ul>
<p>The day-to-day loop is familiar. You edit <code>src/ui.tsx</code> (or ask an agent to), the build step writes <code>www/ui.js</code>, and you reload the running Shiny app to see the change. The server side is unchanged: <code>runApp()</code> or <code>shiny run</code>, exactly as before.</p>
<p>Node.js isn&rsquo;t required, since a client can be a single <code>www/ui.js</code> file with no build step. However, we do strongly recommend it as it gives you a proper development environment: a build step gets you TypeScript, linting, and formatting.</p>
<h2 id="keep-what-you-already-have">Keep what you already have
</h2>
<p>shinyreact doesn&rsquo;t ask you to throw away the rest of the Shiny ecosystem.</p>
<ul>
<li><strong>Existing outputs.</strong> <code>renderPlotly()</code>, <code>render.data_frame</code>, and other render functions work as before. Drop a <code>&lt;ShinyOutput id=&quot;...&quot; /&gt;</code> into your React tree, and the output&rsquo;s JavaScript and CSS dependencies are delivered automatically.</li>
<li><strong>Modules.</strong> <code>ShinyModuleProvider</code> namespaces hook IDs to match a server-side module.</li>
<li><strong>Bookmarking.</strong> URL and server bookmarking seed the initial values of <code>useShinyInput()</code>.</li>
</ul>
<h2 id="testing-at-every-layer">Testing at every layer
</h2>
<p>Because the client and server only share IDs and JSON, each layer can be tested on its own. The server is the layer most Shiny developers care about, and it needs no browser at all. Use <code>shiny::testServer()</code> in R, or the new <code>local_server</code> pytest fixture in <a href="https://opensource.posit.co/blog/2026-09-22_shiny-python-1-8">Shiny for Python 1.8</a>: set inputs and assert on the JSON that comes out.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_histogram</span><span class="p">(</span><span class="n">local_server</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">local_server</span><span class="o">.</span><span class="n">set_inputs</span><span class="p">(</span><span class="n">bin_count</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">data</span> <span class="o">=</span> <span class="n">local_server</span><span class="o">.</span><span class="n">get_output</span><span class="p">(</span><span class="s2">&#34;dist_data&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="nb">len</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s2">&#34;counts&#34;</span><span class="p">])</span> <span class="o">==</span> <span class="mi">10</span></span></span></code></pre></div></div>
<p>The other layers have their own tools:</p>
<ul>
<li><strong>The client.</strong> Ordinary JavaScript unit tests, with whichever test runner you (or your agent) prefer.</li>
<li><strong>The wire.</strong> <code>wire_tap()</code> for <a href="https://rstudio.github.io/shinytest2/" target="_blank" rel="noopener">shinytest2</a> and <code>WireTap</code> for Playwright record the JSON crossing the websocket during a browser test. That gives you an end-to-end assertion on what the client actually sent and what the server actually returned, without reaching into the rendered DOM.</li>
<li><strong>The behavior.</strong> Each example app ships a <code>FEATURES.md</code>: a nested list where every leaf is one checkable claim about the app, written in plain English. A person can read it as a spec, and an agent with a browser can walk it and turn each claim into a deterministic check against the running app.</li>
</ul>
<p>The <a href="https://posit-dev.github.io/shinyreact/articles/testing.html" target="_blank" rel="noopener">testing article</a> covers all of them.</p>
<h2 id="in-the-wild-plotomics-live">In the wild: Plotomics Live
</h2>
<p>Over the summer, Shiny intern <a href="https://www.samuelbharti.com" target="_blank" rel="noopener">Samuel Bharti</a> built a <a href="https://posit-shiny-showcase-bioinformatics.share.connect.posit.cloud/" target="_blank" rel="noopener">collection of bioinformatics Shiny apps</a>. Most of them are plain Shiny and bslib. The one that reached for shinyreact did so because the visualizations demanded it.</p>
<p><a href="https://posit-plotomics-live.share.connect.posit.cloud/" target="_blank" rel="noopener">Plotomics Live</a> (<a href="https://github.com/samuelbharti/plotomics-live" target="_blank" rel="noopener">source</a>, <a href="https://doi.org/10.5281/zenodo.21936926" target="_blank" rel="noopener">DOI</a>) is a 26-page gallery of GPU-accelerated genomics visualizations, from oncoplots to a one-million-point Xenium spatial view and an interactive 584,000-cell UMAP. Large data skips JSON entirely and moves as compact binary typed arrays straight to the GPU. Because React owns the component, a new selection updates the data in place without re-mounting the visualization or reallocating GPU buffers.</p>
<p><video controls autoplay loop muted playsinline src="https://opensource.posit.co/blog/2026-09-30_introducing-shinyreact/plotomics-live.mp4" class="w-full border rounded" title="Plotomics Live: one million Xenium detections rendered with WebGL, with hover tooltips and a legend of marker classes"></video></p>
<p>In Samuel&rsquo;s words:</p>
<blockquote>
<p>R stays the analysis engine, React becomes the visualization layer, and shinyreact removes the custom JavaScript bindings, manual message passing, and serialization code that used to sit between them.</p>
</blockquote>
<h2 id="whats-next">What&rsquo;s next
</h2>
<p>We&rsquo;re working on two directions next:</p>
<ul>
<li><strong>Embedding React components in existing apps</strong>, so you can adopt shinyreact one piece at a time without porting a whole app.</li>
<li><strong>Wrapping shinyreact in your own package</strong>, so you can build a component once and ship it the way bslib ships its components.</li>
</ul>
<h2 id="learn-more">Learn more
</h2>
<ul>
<li>Documentation: <a href="https://posit-dev.github.io/shinyreact/" target="_blank" rel="noopener">posit-dev.github.io/shinyreact</a></li>
<li>Source and example apps: <a href="https://github.com/posit-dev/shinyreact" target="_blank" rel="noopener">github.com/posit-dev/shinyreact</a></li>
<li>posit::conf(2026) talk slides: <a href="https://schloerke.com/presentation-2026-09-15-posit-conf-shinyreact/" target="_blank" rel="noopener">Beyond Bootstrap: Building Custom Shiny UI with React</a></li>
</ul>
<p>Give shinyreact a try, and please <a href="https://github.com/posit-dev/shinyreact/issues" target="_blank" rel="noopener">let us know</a> what you build and what breaks.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-09-30_introducing-shinyreact/feature.png" length="310650" type="image/png" />
    </item>
    <item>
      <title>Multiple tables, saved conversations, and take-home dashboards: querychat R 0.4.0 and Python 0.9.0</title>
      <link>https://opensource.posit.co/blog/2026-09-29_querychat-tables-handoff/</link>
      <pubDate>Tue, 29 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-09-29_querychat-tables-handoff/</guid>
      <dc:creator>Carson Sievert</dc:creator>
      <dc:creator>Garrick Aden-Buie</dc:creator><description><![CDATA[<p>I&rsquo;m thrilled to share the latest <a href="https://posit-dev.github.io/querychat" target="_blank" rel="noopener">querychat</a> release for both R (v0.4.0) and Python (v0.9.0).
Grab the latest from CRAN or PyPI:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-1" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-1-1">R</a></li>
<li><a href="#tabset-1-2">Python</a></li>
</ul>
<div id="tabset-1-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;querychat&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
</div>
<div id="tabset-1-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install -U querychat</span></span></code></pre></div></div>
</div>
</div>
<p>This release adds several headline features, including support for multiple tables, <code>data-dict.yml</code>, a full-page chat layout, support for pins, and a new <code>/handoff</code> command.
It also builds on <a href="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1">shinychat&rsquo;s recent momentum</a>.
As a result, querychat gets chat features like history and file attachments basically for free.
<code>querychat_app()</code> provides a quick and useful way to start chatting with data and getting bespoke <a href="https://opensource.posit.co/blog/2026-06-17_querychat-ggsql">ggsql visualizations</a>, and it now uses shinychat&rsquo;s <code>page_chat()</code> for a full chat app experience.</p>
<p>See the <a href="https://github.com/posit-dev/querychat/blob/main/pkg-r/NEWS.md" target="_blank" rel="noopener">R release notes</a> and the <a href="https://github.com/posit-dev/querychat/blob/main/pkg-py/CHANGELOG.md" target="_blank" rel="noopener">Python changelog</a> for the complete list, including <a href="#a-few-changes-for-existing-apps">a few changes for existing apps</a> if you&rsquo;re upgrading.</p>
<h2 id="full-page-chat-layout">Full-page chat layout
</h2>
<p><code>querychat_app()</code> / <code>QueryChat.app()</code> now put the chat front and center (built on shinychat&rsquo;s <code>page_chat()</code>), leaving more breathing room for things you create within the chat.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-2" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-2-1">R</a></li>
<li><a href="#tabset-2-2">Python</a></li>
</ul>
<div id="tabset-2-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">palmerpenguins</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">querychat_app</span><span class="p">(</span><span class="n">penguins</span><span class="p">)</span></span></span></code></pre></div></div>
</div>
<div id="tabset-2-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">querychat</span> <span class="kn">import</span> <span class="n">QueryChat</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">palmerpenguins</span> <span class="kn">import</span> <span class="n">load_penguins</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span><span class="n">load_penguins</span><span class="p">(),</span> <span class="s2">&#34;penguins&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span><span class="o">.</span><span class="n">app</span><span class="p">()</span></span></span></code></pre></div></div>
</div>
</div>
<script src="https://fast.wistia.com/player.js" async></script>
<script src="https://fast.wistia.com/embed/xe4aalo9yw.js" async type="module"></script>
<style>wistia-player[media-id='xe4aalo9yw']:not(:defined) { background: center / contain no-repeat url('https://fast.wistia.com/embed/medias/xe4aalo9yw/swatch'); display: block; filter: blur(5px); padding-top:75.21%; }</style>
<p><wistia-player media-id="xe4aalo9yw" aspect="1.3296296296296297"></wistia-player></p>
<p>A view of the actual data is always accessible via the data source drawer on the right-hand side.
In the case of <a href="#multiple-tables">multiple tables</a>, you&rsquo;ll see the active table<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, as well as other available tables below it.</p>
<img src="https://opensource.posit.co/blog/2026-09-29_querychat-tables-handoff/multi-table.png" alt="A view of QueryChat.app() with the data source drawer opened." class="shadow rounded" />
<p>The new <code>page()</code> method brings this same full-page chat layout to your own apps.
Your users get the chat front and center, and you can still add custom views on other <code>pages</code>, in the <code>drawer</code>, or in the <code>sidebar</code>.</p>
<p>Learn more about <a href="https://posit-dev.github.io/querychat/r/articles/build.html" target="_blank" rel="noopener">building custom apps in R</a> and <a href="https://posit-dev.github.io/querychat/py/build.html" target="_blank" rel="noopener">Python</a>.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-3" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-3-1">R</a></li>
<li><a href="#tabset-3-2">Python</a></li>
</ul>
<div id="tabset-3-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span><span class="n">penguins</span><span class="p">,</span> <span class="s">&#34;penguins&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="n">qc</span><span class="o">$</span><span class="nf">page</span><span class="p">(</span><span class="s">&#34;Penguins Explorer&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
</div>
<div id="tabset-3-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">querychat.express</span> <span class="kn">import</span> <span class="n">QueryChat</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">palmerpenguins</span> <span class="kn">import</span> <span class="n">load_penguins</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span><span class="n">load_penguins</span><span class="p">(),</span> <span class="s2">&#34;penguins&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span><span class="o">.</span><span class="n">page</span><span class="p">(</span><span class="s2">&#34;Penguins Explorer&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
</div>
</div>
<h2 id="conversation-history">Conversation history
</h2>
<p>Another major improvement is persistent conversation history (<a href="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1#return-to-earlier-conversations">mostly thanks to shinychat</a>).
In addition to starting new chats and returning to previous ones, conversations now persist across page reloads and timeouts.
As a result, it is now much more difficult to lose your progress.</p>
<img src="https://opensource.posit.co/blog/2026-09-29_querychat-tables-handoff/history.png" alt="A view of QueryChat.app() with the history sidebar opened." class="shadow rounded" />
<p>Also, now that shinychat supports <a href="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1#edit-a-message-and-compare-answers">editable messages</a>, <a href="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1#stream-responses-and-show-thinking">canceling responses</a>, <a href="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1#attach-files">file attachments</a>, and more, querychat does too.</p>
<h2 id="multiple-tables">Multiple tables
</h2>
<p>querychat now supports multiple tables in a single chat instance.
If those tables reside in a singular source, like a database, you can add them all in one fell swoop with the <code>add_tables()</code> method.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-4" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-4-1">R</a></li>
<li><a href="#tabset-4-2">Python</a></li>
</ul>
<div id="tabset-4-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">querychat</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span><span class="o">$</span><span class="nf">add_tables</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="nf">c</span><span class="p">(</span><span class="s">&#34;customers&#34;</span><span class="p">,</span> <span class="s">&#34;orders&#34;</span><span class="p">,</span> <span class="s">&#34;order_items&#34;</span><span class="p">))</span></span></span></code></pre></div></div>
</div>
<div id="tabset-4-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">querychat</span> <span class="kn">import</span> <span class="n">QueryChat</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span><span class="o">.</span><span class="n">add_tables</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;customers&#34;</span><span class="p">,</span> <span class="s2">&#34;orders&#34;</span><span class="p">,</span> <span class="s2">&#34;order_items&#34;</span><span class="p">])</span></span></span></code></pre></div></div>
</div>
</div>
<p>querychat&rsquo;s query and visualization tools handle joins across these tables, so a single question can span all of them.
To write a query like the one below, the LLM first needs to know what&rsquo;s in each table: column names, types, and value ranges.
So it starts by fetching the schema of each table it needs (&ldquo;Fetch schemas&rdquo;), then generates the query with that metadata in mind.</p>
<img src="https://opensource.posit.co/blog/2026-09-29_querychat-tables-handoff/cross-join.png" alt="A chat asking for average order value by acquisition channel. The LLM fetches schemas for the customers, orders, and order_items tables, then runs a SQL query that joins all three." class="shadow rounded" />
<p>In a custom app, the new <code>table()</code> method gives your server code reactive access to any table, including whatever filters the LLM has applied to it.
That means you can keep building your own plots and views in Shiny, and your users can drive them just by chatting.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-5" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-5-1">R</a></li>
<li><a href="#tabset-5-2">Python</a></li>
</ul>
<div id="tabset-5-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">output</span><span class="o">$</span><span class="n">order_price</span> <span class="o">&lt;-</span> <span class="nf">renderPlot</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="n">orders_tbl</span> <span class="o">&lt;-</span> <span class="n">qc</span><span class="o">$</span><span class="nf">table</span><span class="p">(</span><span class="s">&#34;orders&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># LLM can perform filter queries on $df()</span>
</span></span><span class="line"><span class="cl">  <span class="n">orders_df</span> <span class="o">&lt;-</span> <span class="n">orders_tbl</span><span class="o">$</span><span class="nf">df</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="nf">hist</span><span class="p">(</span><span class="n">orders_df</span><span class="o">$</span><span class="n">price</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div>
</div>
<div id="tabset-5-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nd">@render.plot</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">_</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">  <span class="n">orders_tbl</span> <span class="o">=</span> <span class="n">qc</span><span class="o">.</span><span class="n">table</span><span class="p">(</span><span class="s2">&#34;orders&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># LLM can perform filter queries on .df()</span>
</span></span><span class="line"><span class="cl">  <span class="n">orders_df</span> <span class="o">=</span> <span class="n">orders_tbl</span><span class="o">.</span><span class="n">df</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="n">plt</span><span class="o">.</span><span class="n">hist</span><span class="p">(</span><span class="n">orders_df</span><span class="p">[</span><span class="s2">&#34;price&#34;</span><span class="p">])</span></span></span></code></pre></div></div>
</div>
</div>
<h2 id="provide-context-data-dict">Provide context: <code>data-dict</code>
</h2>
<p>querychat does its best to gather context from the data itself.
When the LLM fetches a table&rsquo;s schema, it gets whatever metadata querychat can compute from the data.
That&rsquo;s a good start, but in practice it often isn&rsquo;t enough.
Column names can be cryptic, coded values need decoding, and nothing in the data says what &ldquo;active customer&rdquo; means to your business or how tables relate.
In the <a href="#multiple-tables">example above</a>, the LLM had to infer from column names alone that <code>orders.customer_id</code> points to <code>customers.id</code>.</p>
<p>A <strong>data dictionary</strong> is how you fill in what the data can&rsquo;t say about itself.
It&rsquo;s a YAML file that follows the <a href="https://data-dict.tidyverse.org/" target="_blank" rel="noopener">data-dict</a> spec.
Alongside plain-English descriptions, it has its own fields for column types, allowed values, keys, and the relationships between tables.
This is now the preferred way to describe your data:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">tables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">customers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">One row per customer.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">columns</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">acquisition_channel</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">enum</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">values</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">organic, paid_search, social, referral]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">How the customer first found us.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">orders</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">One row per order.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">columns</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">customer_id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">number(id)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">constraints</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">foreign_key]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">order_items</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">One row per item in an order.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">columns</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">price</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">number(quantity)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Item price in USD.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">relationships</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Each order belongs to one customer.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cardinality</span><span class="p">:</span><span class="w"> </span><span class="l">many-to-one</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">join</span><span class="p">:</span><span class="w"> </span><span class="l">orders.customer_id = customers.id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Each order has one or more items.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cardinality</span><span class="p">:</span><span class="w"> </span><span class="l">many-to-one</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">join</span><span class="p">:</span><span class="w"> </span><span class="l">order_items.order_id = orders.id</span></span></span></code></pre></div></div>
<p>Pass it in as <code>data_dict = &quot;dictionary.yml&quot;</code> (R) / <code>data_dict=&quot;dictionary.yml&quot;</code> (Python).
When the LLM fetches a table&rsquo;s schema, any column you&rsquo;ve documented comes straight from your dictionary, with nothing left to infer.
querychat only computes metadata from the data for the columns your dictionary doesn&rsquo;t cover.</p>
<h2 id="extract-insights-handoff">Extract insights: <code>/handoff</code>
</h2>
<p>Over the course of a conversation, querychat tends to produce a pile of results, some more useful than others.
The useful ones deserve to live on in a reproducible artifact that doesn&rsquo;t depend on the chat app.</p>
<p>That&rsquo;s the idea behind the new <strong><code>/handoff</code></strong> slash command.
It&rsquo;s available in every querychat app, with no setup required.
When a user types <code>/handoff</code> into the chat input, a wizard opens where they select the results that matter, choose an output format (e.g., Quarto, marimo, Shiny, Jupyter), and add any presentation instructions for the LLM to follow when it generates the handoff document.</p>
<img src="https://opensource.posit.co/blog/2026-09-29_querychat-tables-handoff/handoff-wizard.png" alt="The handoff wizard" class="shadow rounded" />
<p>When the user finishes the wizard, the handoff document&rsquo;s source code streams into a code editor, where they can revise it by hand or with AI assistance.
A download button then gives them a zip bundle with the handoff document, a README file, and the data sources (if they&rsquo;re small enough).</p>
<img src="https://opensource.posit.co/blog/2026-09-29_querychat-tables-handoff/handoff-download.png" alt="The handoff editor" class="shadow rounded" />
<h2 id="chat-with-pinned-data">Chat with pinned data
</h2>
<p>querychat can now chat with data pinned to a <a href="https://pins.rstudio.com/" target="_blank" rel="noopener">pins</a> board.
Pass the board and the pin name, and querychat reads the pin (parquet, CSV, JSON, RDS, and more) and uses its title, description, and tags as the starting data description:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-6" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-6-1">R</a></li>
<li><a href="#tabset-6-2">Python</a></li>
</ul>
<div id="tabset-6-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">pins</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">querychat</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">board</span> <span class="o">&lt;-</span> <span class="nf">board_connect</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">querychat_app</span><span class="p">(</span><span class="n">board</span><span class="p">,</span> <span class="s">&#34;my_pin&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
</div>
<div id="tabset-6-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install <span class="s2">&#34;querychat[pins]&#34;</span></span></span></code></pre></div></div>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pins</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">querychat</span> <span class="kn">import</span> <span class="n">QueryChat</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">board</span> <span class="o">=</span> <span class="n">pins</span><span class="o">.</span><span class="n">board_connect</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span><span class="n">board</span><span class="p">,</span> <span class="s2">&#34;my_pin&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span><span class="o">.</span><span class="n">app</span><span class="p">()</span></span></span></code></pre></div></div>
</div>
</div>
<p>For more control, such as setting the table name used in SQL, use the new <code>PinSource</code> class directly.
Multiple pins, or pins mixed with ordinary data frames, also work together in one chat.
Everything is materialized into a shared DuckDB connection behind the scenes, so the LLM can join and filter across all of it.
See the <a href="https://posit-dev.github.io/querychat/r/articles/data-sources.html" target="_blank" rel="noopener">data sources guide for R</a> and <a href="https://posit-dev.github.io/querychat/py/data-sources.html" target="_blank" rel="noopener">Python</a> for details.</p>
<h2 id="a-few-changes-for-existing-apps">A few changes for existing apps
</h2>
<p>This release also includes a handful of breaking changes, mostly around how querychat manages connections and bookmarking now that history is built in.
If you&rsquo;re upgrading, skim the <a href="https://github.com/posit-dev/querychat/blob/main/pkg-r/NEWS.md" target="_blank" rel="noopener">R NEWS</a> or <a href="https://github.com/posit-dev/querychat/blob/main/pkg-py/CHANGELOG.md" target="_blank" rel="noopener">Python CHANGELOG</a> breaking-changes sections before you do.</p>
<h2 id="learn-more">Learn more
</h2>
<ul>
<li><a href="https://posit-dev.github.io/querychat/py/" target="_blank" rel="noopener">querychat documentation</a> (<a href="https://posit-dev.github.io/querychat/r/" target="_blank" rel="noopener">R</a>) &mdash; full guides on data sources, context, tools, and deployment</li>
<li><a href="https://data-dict.tidyverse.org/" target="_blank" rel="noopener">data-dict</a> &mdash; the data dictionary spec querychat now reads</li>
<li><a href="https://ggsql.org" target="_blank" rel="noopener">ggsql</a> &mdash; the grammar of graphics for SQL that powers querychat&rsquo;s visualizations</li>
<li><a href="https://posit-dev.github.io/shinychat/py/" target="_blank" rel="noopener">shinychat</a> (<a href="https://posit-dev.github.io/shinychat/r/" target="_blank" rel="noopener">R</a>) &mdash; the chat UI toolkit querychat builds on</li>
<li><a href="https://posit-dev.github.io/chatlas/" target="_blank" rel="noopener">chatlas</a> (<a href="https://ellmer.tidyverse.org" target="_blank" rel="noopener">ellmer</a>) &mdash; the underlying LLM tool-calling libraries</li>
<li><a href="https://github.com/posit-dev/querychat" target="_blank" rel="noopener">Source on GitHub</a> &mdash; issues, discussions, and contributions welcome</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>We thank everyone who contributed to these releases, for opening issues, submitting pull requests, and providing feedback:
<a href="https://github.com/gadenbuie" target="_blank" rel="noopener">@gadenbuie</a>,
<a href="https://github.com/hadley" target="_blank" rel="noopener">@hadley</a>,
<a href="https://github.com/iainwallacebms" target="_blank" rel="noopener">@iainwallacebms</a>,
<a href="https://github.com/iamYannC" target="_blank" rel="noopener">@iamYannC</a>,
<a href="https://github.com/jnhyeon" target="_blank" rel="noopener">@jnhyeon</a>,
<a href="https://github.com/kolabearafk" target="_blank" rel="noopener">@kolabearafk</a>, and
<a href="https://github.com/thisisnic" target="_blank" rel="noopener">@thisisnic</a>.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>By default, the active table is the first one supplied.
However, if the LLM is prompted to show a filtered/sorted view of a table, then that table becomes active (and the drawer will automatically open).&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-09-29_querychat-tables-handoff/featured.png" length="227292" type="image/png" />
    </item>
    <item>
      <title>Shiny for Python 1.8</title>
      <link>https://opensource.posit.co/blog/2026-09-22_shiny-python-1-8/</link>
      <pubDate>Tue, 22 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-09-22_shiny-python-1-8/</guid>
      <dc:creator>Barret Schloerke</dc:creator><description><![CDATA[<p>We&rsquo;re happy to announce that <a href="https://pypi.org/project/shiny/" target="_blank" rel="noopener">Shiny for Python v1.8</a> is now on PyPI!</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install -U shiny</span></span></code></pre></div></div>
<p>The highlights:
* <a href="#in-memory-server-testing"><code>test_server()</code></a> runs your app&rsquo;s server logic in memory so you can test it without a browser,
* <a href="#bring-your-own-html-document"><code>ui.page_html()</code></a> lets a complete HTML document (say, the <code>index.html</code> your JS bundler emits) be the app&rsquo;s UI, and
* <a href="#session-reconnection"><code>session.allow_reconnect()</code></a> lets the browser reconnect to a live session after a dropped connection.</p>
<p>Full details are in the <a href="https://github.com/posit-dev/py-shiny/blob/main/CHANGELOG.md" target="_blank" rel="noopener">Shiny for Python changelog</a>.</p>
<h2 id="in-memory-server-testing">In-memory server testing
</h2>
<p>Until now, testing a Shiny for Python app meant one of two things: unit-test the pure functions your server calls, or spin up the app and a browser with Playwright and test end to end. There was nothing in between for the part that actually makes an app a Shiny app: the reactive graph.</p>
<p>New in v1.8, <a href="https://shiny.posit.co/py/api/testing/testserver.test_server.html" target="_blank" rel="noopener"><code>shiny.testserver.test_server()</code></a> runs a server function, Express app, or <code>shiny.App</code> against a mock connection. There&rsquo;s no browser and no network server, and your test stays a plain synchronous function: <code>test_server()</code> drives the event loop for you, so you never write <code>async def</code> or <code>await</code>. (If your test already runs inside an event loop, use <code>test_server_async()</code> instead.) Set inputs, let the reactive graph settle, and assert on outputs, all in an ordinary <a href="https://docs.pytest.org/" target="_blank" rel="noopener">pytest</a> test. It&rsquo;s the Python counterpart to Shiny for R&rsquo;s <a href="https://shiny.posit.co/r/reference/shiny/latest/testServer.html" target="_blank" rel="noopener"><code>testServer()</code></a>.</p>
<p>For the common case, an <code>app.py</code> next to your test file, the new <code>local_server</code> pytest fixture is the whole setup:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_doubling_app</span><span class="p">(</span><span class="n">local_server</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">local_server</span><span class="o">.</span><span class="n">set_inputs</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">&#34;Ada&#34;</span><span class="p">,</span> <span class="n">n</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">local_server</span><span class="o">.</span><span class="n">is_ok</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">local_server</span><span class="o">.</span><span class="n">get_output</span><span class="p">(</span><span class="s2">&#34;greeting&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;Hello, Ada!&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">local_server</span><span class="o">.</span><span class="n">get_output</span><span class="p">(</span><span class="s2">&#34;doubled&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;20&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Inputs you don&#39;t name keep their values, so `name` is still &#34;Ada&#34;.</span>
</span></span><span class="line"><span class="cl">    <span class="n">local_server</span><span class="o">.</span><span class="n">set_inputs</span><span class="p">(</span><span class="n">n</span><span class="o">=</span><span class="mi">21</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">local_server</span><span class="o">.</span><span class="n">get_output</span><span class="p">(</span><span class="s2">&#34;doubled&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;42&#34;</span></span></span></code></pre></div></div>
<p><code>local_server</code> is the in-memory sibling of the existing <code>local_app</code> fixture, but function-scoped: a session remembers the inputs set so far, so each test gets a fresh one.</p>
<p>Each output also reports how it turned out, which is what to reach for when the assertion isn&rsquo;t about equality:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_reports_a_bad_value</span><span class="p">(</span><span class="n">local_server</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">local_server</span><span class="o">.</span><span class="n">set_inputs</span><span class="p">(</span><span class="n">n</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">local_server</span><span class="o">.</span><span class="n">is_ok</span> <span class="ow">is</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">    <span class="n">failed</span> <span class="o">=</span> <span class="n">local_server</span><span class="o">.</span><span class="n">get_output</span><span class="p">(</span><span class="s2">&#34;doubled&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">failed</span><span class="o">.</span><span class="n">status</span> <span class="o">==</span> <span class="s2">&#34;error&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="s2">&#34;must be positive&#34;</span> <span class="ow">in</span> <span class="n">failed</span><span class="o">.</span><span class="n">error</span></span></span></code></pre></div></div>
<p>Modules work too. Reach into one with its namespaced id (<code>&quot;counter-n&quot;</code>), or take a scope and use the bare ids the module&rsquo;s own code uses:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_counter_module</span><span class="p">(</span><span class="n">local_server</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">counter</span> <span class="o">=</span> <span class="n">local_server</span><span class="o">.</span><span class="n">make_scope</span><span class="p">(</span><span class="s2">&#34;counter&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">counter</span><span class="o">.</span><span class="n">set_inputs</span><span class="p">(</span><span class="n">n</span><span class="o">=</span><span class="mi">7</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">counter</span><span class="o">.</span><span class="n">get_output</span><span class="p">(</span><span class="s2">&#34;label&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;n=7&#34;</span></span></span></code></pre></div></div>
<p>And when you need something other than <code>app.py</code>, call <code>test_server()</code> directly as a context manager. It accepts a path, a <code>shiny.App</code>, or a bare server function (handy for testing a module&rsquo;s server function on its own):</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny.testserver</span> <span class="kn">import</span> <span class="n">test_server</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_the_other_app</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">test_server</span><span class="p">(</span><span class="s2">&#34;other_app.py&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">ts</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">ts</span><span class="o">.</span><span class="n">set_inputs</span><span class="p">(</span><span class="n">n</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">assert</span> <span class="n">ts</span><span class="o">.</span><span class="n">get_output</span><span class="p">(</span><span class="s2">&#34;tripled&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;30&#34;</span></span></span></code></pre></div></div>
<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">What about plots and client data?</span>
</div>
<div class="callout-body">
<p>A real browser reports things like output sizes and the page URL back to the server, and <code>@render.plot</code> needs a width and height before it can draw. <code>test_server()</code> sends sensible stand-ins for all of these as soon as the session starts, so plots render out of the box. Override the default client data with <code>client_data=</code>, or change one output&rsquo;s size mid-test with <code>set_inputs()</code>.</p>
</div>
</div>
<p>The bundled <code>shiny-for-python</code> <a href="https://opensource.posit.co/blog/2026-08-04_shiny-r-1-14-python-1-7#agent-skills">Agent Skill</a> has a new <code>test-server</code> topic as well, so coding agents reach for in-memory server tests instead of hand-built sessions or a browser when only server logic needs checking.</p>
<p>New to testing Shiny apps? Start with <a href="https://shiny.posit.co/py/docs/unit-testing.html" target="_blank" rel="noopener">Unit testing</a> and <a href="https://shiny.posit.co/py/docs/end-to-end-testing.html" target="_blank" rel="noopener">End-to-end testing</a> on the Shiny for Python website, then browse the <a href="https://shiny.posit.co/py/api/testing/index.html" target="_blank" rel="noopener">testing API reference</a>.</p>
<h2 id="bring-your-own-html-document">Bring your own HTML document
</h2>
<p>Shiny&rsquo;s <code>ui.page_*()</code> functions build the HTML document for you. That&rsquo;s usually what you want, but sometimes you already have one: the <code>index.html</code> a JS bundler like Vite emits, a hand-written template, or a page produced by another tool entirely.</p>
<p><a href="https://shiny.posit.co/py/api/core/ui.page_html.html" target="_blank" rel="noopener"><code>ui.page_html()</code></a> takes that document, as a string or a <code>Path</code>, and serves it as-is. Shiny&rsquo;s own HTML dependencies (plus any you pass via <code>extra_deps=</code>) are inserted where you put a placeholder <code>&lt;meta&gt;</code> tag, and their files are served by the app:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="cp">&lt;!doctype html&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;shiny-dependency-placeholder&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;module&#34;</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;/assets/index.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;app&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span></span></span></code></pre></div></div>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny</span> <span class="kn">import</span> <span class="n">App</span><span class="p">,</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app_dir</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">server</span><span class="p">(</span><span class="nb">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">(</span><span class="n">ui</span><span class="o">.</span><span class="n">page_html</span><span class="p">(</span><span class="n">app_dir</span> <span class="o">/</span> <span class="s2">&#34;index.html&#34;</span><span class="p">),</span> <span class="n">server</span><span class="p">)</span></span></span></code></pre></div></div>
<p>If your document marks the spot differently, set <code>deps_replace_pattern=</code>. And because <code>ui.page_html()</code> returns a regular UI object, you can return it from a UI function (<code>App(ui=lambda request: ...)</code>), which is what bookmarking requires.</p>
<p>In Express, pass the document to <a href="https://shiny.posit.co/py/api/express/express.ui.page_opts.html" target="_blank" rel="noopener"><code>ui.page_opts(html=)</code></a>. The whole app is routed through <code>ui.page_html()</code>: top-level UI markup is dropped, since the document already <em>is</em> the page, but any HTML dependencies it brings along are kept.</p>
<p>This is the Python counterpart to Shiny for R&rsquo;s <code>shinyApp(ui = htmlTemplate(&quot;index.html&quot;, document_ = TRUE))</code> with <code>attachDependencies()</code>.</p>
<h2 id="session-reconnection">Session reconnection
</h2>
<p>When the websocket between the browser and the server drops, Shiny shows the &ldquo;Disconnected from server&rdquo; overlay and the client gives up. If your hosting environment keeps sessions alive after a client disconnects (Posit Connect and Shiny Server both can), that&rsquo;s a missed opportunity: the session is still right there.</p>
<p><a href="https://shiny.posit.co/py/api/core/Session.html#shiny.Session.allow_reconnect" target="_blank" rel="noopener"><code>session.allow_reconnect(True)</code></a> tells the client to instead show a countdown dialog and try to reconnect. On success, the browser sends its current input values back to the server, and the server recalculates outputs and sends them down again. Users pick up right where they left off after a flaky Wi-Fi blip or a laptop lid closing.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny</span> <span class="kn">import</span> <span class="n">App</span><span class="p">,</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">server</span><span class="p">(</span><span class="nb">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">session</span><span class="o">.</span><span class="n">allow_reconnect</span><span class="p">(</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">(</span><span class="n">ui</span><span class="o">.</span><span class="n">page_fluid</span><span class="p">(</span><span class="o">...</span><span class="p">),</span> <span class="n">server</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Pass <code>&quot;force&quot;</code> to attempt the reconnect anywhere, which is useful for exercising the countdown UI on a local <code>shiny run</code> server (where the attempt starts a fresh session rather than resuming the old one). This is the Python counterpart to Shiny for R&rsquo;s <a href="https://shiny.posit.co/r/reference/shiny/latest/session.html" target="_blank" rel="noopener"><code>session$allowReconnect()</code></a>.</p>
<h2 id="deprecation-uioutput_text_verbatim">Deprecation: <code>ui.output_text_verbatim()</code>
</h2>
<p><code>ui.output_text_verbatim()</code> now emits a <code>ShinyDeprecationWarning</code>. It has long been a leftover from Shiny for R&rsquo;s <code>verbatimTextOutput()</code>, and Python has had clearer names for both jobs for a while:</p>
<ul>
<li>For code or other monospaced text, use <code>ui.output_code()</code> with <code>@render.code</code>.</li>
<li>For plain text, use <code>ui.output_text()</code> with <code>@render.text</code>.</li>
</ul>
<p>The matching Playwright controller, <code>playwright.controller.OutputTextVerbatim</code>, is deprecated alongside it; use <code>playwright.controller.OutputCode</code> instead.</p>
<h2 id="other-improvements">Other improvements
</h2>
<p>A few more changes worth a quick mention. The full list is in the <a href="https://github.com/posit-dev/py-shiny/blob/main/CHANGELOG.md" target="_blank" rel="noopener">changelog</a>:</p>
<ul>
<li>Closing a session no longer destroys the reactive values and calcs created in it. Since v1.6.1, refreshing the page while an <code>@reactive.extended_task</code> was in flight could raise <code>DestroyedReactiveError</code> once it settled. Values and calcs are now left readable at their last value and reclaimed by garbage collection; effects are still destroyed on close.</li>
<li><code>ui.input_slider()</code> and <code>ui.update_slider()</code> now encode <code>datetime.date</code> values as UTC midnight, matching Shiny for R, so they no longer shift by a day when the server runs in a timezone ahead of UTC. Naive <code>datetime.datetime</code> values round-trip unchanged too.</li>
<li><code>@render.download_button</code> and <code>@render.download_link</code> now honor <code>@output(id=)</code>. Previously the URL and the registered handler disagreed on the id, so clicking the control returned a 404.</li>
<li><code>@expressify</code> and <code>@render.express</code> no longer fail with <code>RuntimeError: Failed to find function '...' in AST</code> when another decorator has changed the function&rsquo;s <code>__name__</code>, a pattern often used to give each <code>@render.express</code> function in a loop a unique output id.</li>
<li>Navsets created with an <code>id</code> now use it as their <code>data-tabsetid</code>, so tab panes get stable DOM ids instead of ones built from a random integer. This makes the markup reproducible and easier to target from custom CSS and JavaScript. (Thanks, <a href="https://github.com/pevolution-ahmed" target="_blank" rel="noopener">@pevolution-ahmed</a>!)</li>
<li><code>ui.show_offcanvas()</code> now accepts the <code>id</code> of an <code>ui.offcanvas()</code> panel already in the UI, matching <code>ui.hide_offcanvas()</code> and <code>ui.toggle_offcanvas()</code>. It also accepts bare tag content, wrapping it in a new anonymous panel.</li>
<li><code>@render.data_frame</code> now renders data frames whose column names are empty or not strings; column ids are positional and never derived from the name.</li>
<li><code>@render.ui</code> outputs inside a <code>ui.popover()</code> or <code>ui.tooltip()</code> without a <code>title=</code> no longer get stuck showing &ldquo;recalculating&rdquo;.</li>
<li><code>shiny run --app-dir &lt;dir&gt; &lt;app&gt;</code> now honors <code>--app-dir</code> for Shiny Express apps.</li>
<li><code>ui.input_task_button(type=None)</code> no longer drops the class its input binding needs, so the button is bound and clicking it works.</li>
<li><code>playwright.controller.Offcanvas</code> gains <code>open()</code>, <code>loc_trigger</code>, <code>loc_title</code>, <code>loc_footer</code>, and <code>expect_title()</code>, <code>expect_footer()</code>, and <code>expect_placement()</code>.</li>
<li><code>playwright.controller.InputSelectize</code> no longer clicks the page body to close its dropdown, so a test can&rsquo;t accidentally fire the app&rsquo;s own click handlers.</li>
</ul>
<h2 id="in-closing">In closing
</h2>
<p>We&rsquo;re excited to see what you build (and test) with this release. As always, if you have questions or feedback, <a href="https://discord.gg/yMGCamUMnS" target="_blank" rel="noopener">join us on Discord</a> or open an issue on <a href="https://github.com/posit-dev/py-shiny/issues/new" target="_blank" rel="noopener">posit-dev/py-shiny</a>. Happy Shiny-ing!</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thank you to all the folks who helped make this release happen by opening issues and contributing code:</p>
<p><a href="https://github.com/ambevill" target="_blank" rel="noopener">@ambevill</a>, <a href="https://github.com/arabidopsis" target="_blank" rel="noopener">@arabidopsis</a>, <a href="https://github.com/bealdav" target="_blank" rel="noopener">@bealdav</a>, <a href="https://github.com/chernojagne" target="_blank" rel="noopener">@chernojagne</a>, <a href="https://github.com/ChidiebereNjoku" target="_blank" rel="noopener">@ChidiebereNjoku</a>, <a href="https://github.com/cpsievert" target="_blank" rel="noopener">@cpsievert</a>, <a href="https://github.com/danieldebondt-tf" target="_blank" rel="noopener">@danieldebondt-tf</a>, <a href="https://github.com/drewe7192" target="_blank" rel="noopener">@drewe7192</a>, <a href="https://github.com/eeshsaxena" target="_blank" rel="noopener">@eeshsaxena</a>, <a href="https://github.com/ErdaradunGaztea" target="_blank" rel="noopener">@ErdaradunGaztea</a>, <a href="https://github.com/FBruzzesi" target="_blank" rel="noopener">@FBruzzesi</a>, <a href="https://github.com/gadenbuie" target="_blank" rel="noopener">@gadenbuie</a>, <a href="https://github.com/jat255" target="_blank" rel="noopener">@jat255</a>, <a href="https://github.com/jubilee2" target="_blank" rel="noopener">@jubilee2</a>, <a href="https://github.com/karangattu" target="_blank" rel="noopener">@karangattu</a>, <a href="https://github.com/kramerrs" target="_blank" rel="noopener">@kramerrs</a>, <a href="https://github.com/MichielNoback" target="_blank" rel="noopener">@MichielNoback</a>, <a href="https://github.com/mykolaskrynnyk" target="_blank" rel="noopener">@mykolaskrynnyk</a>, <a href="https://github.com/nightcityblade" target="_blank" rel="noopener">@nightcityblade</a>, <a href="https://github.com/nvelden" target="_blank" rel="noopener">@nvelden</a>, <a href="https://github.com/pevolution-ahmed" target="_blank" rel="noopener">@pevolution-ahmed</a>, <a href="https://github.com/schloerke" target="_blank" rel="noopener">@schloerke</a>, <a href="https://github.com/shawnboltz" target="_blank" rel="noopener">@shawnboltz</a>, <a href="https://github.com/tjpalanca" target="_blank" rel="noopener">@tjpalanca</a>, <a href="https://github.com/weichisyu" target="_blank" rel="noopener">@weichisyu</a>, and <a href="https://github.com/xiruizhao" target="_blank" rel="noopener">@xiruizhao</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-09-22_shiny-python-1-8/feature.gif" length="127580" type="image/gif" />
    </item>
    <item>
      <title>AI Newsletter: New releases from ellmer, shinychat, and commons</title>
      <link>https://opensource.posit.co/blog/2026-09-18_ai-newsletter/</link>
      <pubDate>Fri, 18 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-09-18_ai-newsletter/</guid>
      <dc:creator>Sara Altman</dc:creator>
      <dc:creator>Simon Couch</dc:creator><description><![CDATA[<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title"><strong>Subscribe to the AI Newsletter!</strong></span>
</div>
<div class="callout-body">
<p>The AI newsletter is published as an RSS feed. Follow it in your favorite reader:</p>
<p><a href="https://opensource.posit.co/tags/ai-newsletter/index.xml" target="_blank" rel="noopener noreferrer" class="btn-shortcode inline-flex mb-5 mr-5 items-center px-4 py-3 text-sm leading-5 gap-2 rounded-lg bg-blue-400 !text-white font-semibold align-middle hover:bg-blue-500 transition no-underline">Subscribe via RSS</a></p>
<p><strong>Want the newsletter as an email?</strong> Paste the feed URL, <a href="https://opensource.posit.co/tags/ai-newsletter/index.xml" target="_blank" rel="noopener">https://opensource.posit.co/tags/ai-newsletter/index.xml</a>, into a free RSS-to-email service such as <a href="https://blogtrottr.com/" target="_blank" rel="noopener">Blogtrottr</a>, <a href="https://feedrabbit.com/" target="_blank" rel="noopener">Feedrabbit</a>, or <a href="https://follow.it/" target="_blank" rel="noopener">Follow.it</a>, and each new issue will arrive in your inbox.</p>
</div>
</div>
<p>Last week, three packages in Posit&rsquo;s open-source AI stack shipped significant releases. We introduced commons 0.1.0, and ellmer and shinychat both received substantial updates. These releases are part of a broader effort to make it as easy as possible to build modern chat applications in R and Python.</p>
<p>In this newsletter, we&rsquo;ll take a quick tour of what&rsquo;s new.</p>
<h2 id="introducing-commons">Introducing commons
</h2>
<p><strong>commons, a new framework for building trustworthy self-service data analysis agents in R and Python, is now on CRAN.</strong> Read the full announcement <a href="https://opensource.posit.co/blog/2026-09-15_commons-0-1-0">here</a>.</p>
<p>The Python package is currently in a pre-release beta stage, with more features arriving over the next few weeks.</p>
<p>If you&rsquo;re a data analyst, data scientist, statistical programmer, or other data practitioner, you likely have extensive domain knowledge and a collection of <em>trusted code</em> that you already use in analyses, apps, reports, and packages. The core idea behind commons is that we can leverage this trusted code to improve an agent&rsquo;s correctness.</p>
<p>A commons agent first searches for a trusted calculation. If it finds one that can answer the user&rsquo;s question, it can run that vetted code and the answer is deterministically marked as verified.</p>
<img src="https://opensource.posit.co/blog/2026-09-18_ai-newsletter/images/commons-01-traffic-trend.gif" title="A commons agent answering a question with a trusted calculation." data-fig-alt="A commons agent answers how site traffic is trending by finding and running a trusted calculation. The resulting chart and answer are marked as verified." />
<p>If it doesn&rsquo;t find a trusted calculation, the agent searches trusted context before writing custom R, Python, or SQL. The answer is either given a citation or marked as &ldquo;untrusted,&rdquo; depending on whether the agent provides a verified citation that supports its approach.</p>
<p>The model doesn&rsquo;t decide how trustworthy its answer is. commons assigns each label deterministically based on the analysis path taken by the agent.</p>
<img src="https://opensource.posit.co/blog/2026-09-18_ai-newsletter/images/trust-flow.svg" class="column-page" data-fig-alt="Flow diagram showing how commons routes questions. It first searches trusted calculations. If it finds one, it runs the calculation and returns a verified answer. Otherwise, it searches trusted context, writes custom code, and returns either a cited or lower-trust answer." />
<p>commons also ships with an agent skill to help you create a commons agent and functions for analyzing your users&rsquo; conversations.</p>
<h2 id="shinychat-v050-r-and-v071-python">shinychat v0.5.0 (R) and v0.7.1 (Python)
</h2>
<p><strong>shinychat v0.5.0 for R and v0.7.1 for Python bring together more of what you need to build a complete chat application.</strong> Several of these shinychat updates also made commons possible!</p>
<p>Read the full blog post <a href="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1">here</a>. There are many more updates worth checking out.</p>
<h3 id="page_chat"><code>page_chat()</code>
</h3>
<p>Use <code>page_chat()</code> instead of the bslib <code>page_*()</code> functions when you want the chat to be the center of your application. <code>page_chat()</code> creates a full-window, chatbot-oriented layout with support for navigation pages, conversation history, an <a href="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1#artifact-drawer">artifact drawer</a>, and more.</p>
<img src="https://opensource.posit.co/blog/2026-09-18_ai-newsletter/images/shinychat-page-chat.png" title="A shinychat app built `page_chat()`." class="column-page" data-fig-alt="A full-window Site traffic assistant built with page_chat, showing a traffic-trend question, a compact calculation activity row, the assistant&#39;s answer, and the chat input." />
<h3 id="conversation-history">Conversation history
</h3>
<p>Conversation history is enabled by default when you use <code>chat_server()</code> in R or <code>Chat(client=...)</code> in Python, allowing users to start a new conversation, switch between saved conversations, search them, rename them, and delete them.</p>
<img src="https://opensource.posit.co/blog/2026-09-18_ai-newsletter/images/shinychat-history.png" title="Previous conversations shown in the chat history sidebar." class="column-page" data-fig-alt="The Site traffic assistant with its history sidebar open, showing controls to search or start a conversation and three saved traffic-analysis conversations beside the active chat." />
<h3 id="readable-tool-calls-and-citations">Readable tool calls and citations
</h3>
<p>This shinychat release also includes several improvements for understanding how a model arrived at its response.</p>
<p>One such improvement is readable tool calls. By default, related tool calls are grouped into compact, single-line &ldquo;activity rows&rdquo;, keeping them from overwhelming the conversation. You can still inspect the individual tool calls by expanding a row.</p>
<p>shinychat also displays citations returned by providers&rsquo; built-in web-search and web-fetch tools.</p>
<img src="https://opensource.posit.co/blog/2026-09-18_ai-newsletter/images/shinychat-tools-citations.png" title="A grouped activity row and an open citation." data-fig-alt="A Shinychat answer with a compact grouped activity row for searching documentation and querying the warehouse, plus an open citation identifying sessions_daily as the canonical site-traffic source." />
<h2 id="ellmer-050">ellmer 0.5.0
</h2>
<p><strong><a href="https://opensource.posit.co/blog/2026-09-14_ellmer-0-5-0">ellmer 0.5.0</a> is now on CRAN.</strong> ellmer makes it easy to work with LLMs from R.</p>
<p>Read the full announcement <a href="https://opensource.posit.co/blog/2026-09-14_ellmer-0-5-0">here</a>. Many of the features made available in this release are also available in recent releases of <a href="https://github.com/posit-dev/chatlas/releases" target="_blank" rel="noopener">chatlas</a>, ellmer&rsquo;s sibling package in Python.</p>
<h3 id="citations">Citations
</h3>
<p>When a model uses a supported built-in web tool, ellmer now returns and displays the provider-supplied citations. This works with <code>claude_tool_web_search()</code>, <code>claude_tool_web_fetch()</code>, <code>google_tool_web_search()</code>, and <code>openai_tool_web_search()</code>, helping you identify the sources behind the model&rsquo;s answer.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_openai</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">$</span><span class="nf">register_tool</span><span class="p">(</span><span class="nf">openai_tool_web_search</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">$</span><span class="nf">chat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;What is the most recent version of ellmer on CRAN? Look it up.&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; The most recent CRAN release of **ellmer** is **version 0.5.0**, published</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; **September 4, 2026**.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ([cran.r-project.org](https://cran.r-project.org/package%3Dellmer))[1]</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Sources</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] CRAN: Package ellmer: https://cran.r-project.org/package%3Dellmer</span></span></span></code></pre></div></div>
<h3 id="tokens-and-costs">Tokens and costs
</h3>
<p>Managing tokens and costs is an important part of working with LLMs.</p>
<p>Ever want to know how many tokens an input will take before sending it? For supported providers, you can now use <code>chat$token_count()</code> to estimate input token use. Instead of actually sending the request to the model, it sends the request to the provider&rsquo;s token-counting API.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_openai</span><span class="p">(</span><span class="n">model</span> <span class="o">=</span> <span class="s">&#34;gpt-5.6-luna&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">prompt</span> <span class="o">&lt;-</span> <span class="nf">content_pdf_file</span><span class="p">(</span><span class="s">&#34;example-document.pdf&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">$</span><span class="nf">token_count</span><span class="p">(</span><span class="n">prompt</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] 252</span></span></span></code></pre></div></div>
<p>This estimates only the tokens used by the input and does not predict the number of output tokens, so it won&rsquo;t represent the total round-trip count.</p>
<p>Companies frequently release new models and change their prices. Use the new function <code>models_update_prices()</code> to download and cache the latest pricing data from the ellmer GitHub repo. Cost estimates reported by <code>token_usage()</code>, <code>Chat$get_cost()</code>, <code>Chat$get_tokens()</code>, and printed <code>Chat</code> objects use this data.</p>
<h3 id="send-files-to-the-model">Send files to the model
</h3>
<p>It&rsquo;s often useful to send files as part of a chat. You can now send CSV, Markdown, code, and other text-based files to a model with <code>content_document_file()</code> and <code>content_document_url()</code>. For large files or files reused across multiple turns, if you&rsquo;re using <code>chat_openai()</code>, <code>chat_anthropic()</code>, or <code>chat_google_gemini()</code>, use <code>chat$file_upload()</code> instead. It uploads the file once and returns a reference for <code>$chat()</code>. This avoids repeatedly sending the file and reduces token usage and cost.</p>
<h2 id="solid-improvements-for-custom-agents">Solid improvements for custom agents
</h2>
<p>Taken together, these releases make it easier to build more complete custom agents. ellmer manages model interactions in R, shinychat provides the user-facing chat interface, and commons adds a framework for data analysis agents that builds upon these two.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-09-18_ai-newsletter/images/hero.png" length="543199" type="image/png" />
    </item>
    <item>
      <title>Introducing commons</title>
      <link>https://opensource.posit.co/blog/2026-09-15_commons-0-1-0/</link>
      <pubDate>Tue, 15 Sep 2026 13:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-09-15_commons-0-1-0/</guid>
      <dc:creator>Simon Couch</dc:creator>
      <dc:creator>Sara Altman</dc:creator>
      <dc:creator>Josh Taillon</dc:creator><description><![CDATA[<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title"><strong>Learn more about commons at our Posit Monthly Workflow Demo</strong></span>
</div>
<div class="callout-body">
<p>Join Sara and Simon on October 28th at 11:00 AM CT to explore Posit Commons!</p>
<p><a href="https://events.zoom.us/ev/Ajss5j9VeRMe0zw-AtFKf7AUAsthzYhaYjYPeEIu1uYAQe1K0ud1~Agb_hSt1UGxd9fUyIDC32e6jAdEtbl7G_AaLZUYhRvyZR5cGpY5Kp_A0-w" target="_blank" rel="noopener">Register here</a>.</p>
</div>
</div>
<p>We&rsquo;re hootin&rsquo; and hollerin&rsquo; to share <a href="https://posit-dev.github.io/commons/" target="_blank" rel="noopener">commons</a>, an R and Python package that helps data scientists build trustworthy data analysis agents.</p>
<video class="column-page" autoplay loop muted playsinline controls preload="metadata" aria-label="Screen recording of a commons agent answering 'How is traffic trending for our site?' by running a trusted calculation and displaying a chart showing daily site visits increased 22%.">
  <source src="https://opensource.posit.co/blog/2026-09-15_commons-0-1-0/commons-01-traffic-trend.mp4" type="video/mp4">
  Your browser does not support embedded videos.
</video>
<p>The package is built on <a href="https://ellmer.tidyverse.org/" target="_blank" rel="noopener">ellmer</a>, <a href="https://posit-dev.github.io/chatlas/" target="_blank" rel="noopener">chatlas</a>, and <a href="https://github.com/posit-dev/shinychat/" target="_blank" rel="noopener">shinychat</a>, Posit&rsquo;s open source LLM stack. You can use whatever model you want from any of the providers supported by those packages with it.</p>
<p>To install the R package, run:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;commons&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>To install the Python package from PyPI, run:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-py" data-lang="py"><span class="line"><span class="cl"><span class="n">pip</span> <span class="n">install</span> <span class="n">commons</span></span></span></code></pre></div></div>
<p>The Python package is currently in a pre-release beta stage, but you can install and play around with it today, with more features arriving over the next few weeks.</p>
<h2 id="design-philosophy">Design philosophy
</h2>
<p>AI agents for data analysis can range from overly cautious and narrowly correct to wildly and confidently incorrect. commons provides a framework for you to design more trustworthy analysis agents by providing them with access to existing <strong>trusted code</strong>, while still allowing them enough flexibility to answer novel, realistic questions.</p>
<p>If you are a data analyst, data scientist, statistical programmer, or other data practitioner, you likely have a deep understanding of your problem domain and a collection of trusted code you depend on for your analyses and use to create apps, reports, and packages. The core idea behind commons is that we can improve an agent&rsquo;s correctness by giving it the right access and documentation to run this code that you have already vetted.</p>
<style>
.commons-inline-icon {
  display: inline-block;
  height: 1.25em;
  margin: 0 0.08em;
  vertical-align: -0.25em;
  width: 1.25em;
}
</style>
<p>When answering questions, commons agents first search through a pool of trusted code. If the agent finds an appropriate piece of trusted code, it can invoke it directly, and its response will be tagged with a green shield icon <img src="https://opensource.posit.co/blog/2026-09-15_commons-0-1-0/trusted-icon.svg" class="commons-inline-icon" alt="">. If it doesn&rsquo;t, it will search through relevant context before writing its own SQL, R, or Python. If the agent can find trusted context that justifies its approach, it can provide a citation <img src="https://opensource.posit.co/blog/2026-09-15_commons-0-1-0/citation-mark.svg" class="commons-inline-icon" alt=""> to it at the end of its answer, which will be deterministically checked by commons. Otherwise, the answer is marked with a small warning label <img src="https://opensource.posit.co/blog/2026-09-15_commons-0-1-0/warning-icon.svg" class="commons-inline-icon" alt="">.</p>
<img src="https://opensource.posit.co/blog/2026-09-15_commons-0-1-0/trust-flow.svg" class="column-page" alt="Flow diagram. A commons agent searches trusted calculations. If it finds a relevant calculation, it runs the trusted calculation and returns a verified answer. Otherwise, it searches context, writes SQL or R, and returns either a cited or untrusted answer.">
<p>Notably, the agent itself does not decide how to label a response. commons instead labels answers deterministically, based on the path the agent takes to get it to its answer.</p>
<h2 id="get-started">Get started
</h2>
<p>To get started with the R package, check out the <a href="https://posit-dev.github.io/commons/r/articles/commons.html" target="_blank" rel="noopener">introductory vignette</a>. The package ships with an <a href="https://posit-dev.github.io/commons/r/articles/commons.html#working-with-the-agent-skill" target="_blank" rel="noopener">agent skill</a> to help you hook your trusted code and context up to the agent.</p>
<p>The Python package (although still in beta) is based on the same ideas, and the resulting apps will look <em>very</em> similar regardless of whether you use R or Python—they literally share the same CSS! Check out the <a href="https://posit-dev.github.io/commons/py/" target="_blank" rel="noopener">Python package site</a> to learn more.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-09-15_commons-0-1-0/commons.png" length="1439234" type="image/png" />
    </item>
    <item>
      <title>Complete chat applications in shinychat: R 0.5.0 and Python 0.7.1</title>
      <link>https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/</link>
      <pubDate>Tue, 15 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/</guid>
      <dc:creator>Garrick Aden-Buie</dc:creator>
      <dc:creator>Carson Sievert</dc:creator><description><![CDATA[<p>We&rsquo;re excited to announce <a href="https://posit-dev.github.io/shinychat/r/" target="_blank" rel="noopener">shinychat v0.5.0 for R</a> and <a href="https://posit-dev.github.io/shinychat/py/" target="_blank" rel="noopener">shinychat v0.7.1 for Python</a>.
This release brings the pieces of a complete chat application together around the conversation itself.</p>
<p>shinychat is a toolkit for building complete, conversation-centered chat applications with Shiny.
The R package pairs with <a href="https://ellmer.tidyverse.org/" target="_blank" rel="noopener">ellmer</a>, and the Python package pairs with <a href="https://posit-dev.github.io/chatlas/" target="_blank" rel="noopener">chatlas</a>.
Install the latest releases from CRAN or PyPI:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-1" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-1-1">R</a></li>
<li><a href="#tabset-1-2">Python</a></li>
</ul>
<div id="tabset-1-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;shinychat&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
</div>
<div id="tabset-1-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install -U shinychat</span></span></code></pre></div></div>
</div>
</div>
<p>We cover a lot in this post, and there&rsquo;s even more in the releases.
See the <a href="https://github.com/posit-dev/shinychat/blob/main/pkg-r/NEWS.md" target="_blank" rel="noopener">R release notes</a> and the <a href="https://github.com/posit-dev/shinychat/blob/main/pkg-py/CHANGELOG.md" target="_blank" rel="noopener">Python changelog</a> for the complete list of changes, including <a href="#a-few-changes-for-existing-apps">a few changes for existing apps</a> if you&rsquo;re upgrading.</p>
<h2 id="build-a-chat-application">Build a chat application
</h2>
<div class="w-full aspect-4/3">
      <video
        src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/complete-app.mp4"
        class="w-full h-full object-contain"
        title="A complete chat application: the history sidebar lists saved conversations, the assistant answers with a tool activity row and citations, and the artifact drawer opens beside the chat with a plot"
        controls></video>
    </div>
<p>A useful chat application needs more than a text box and a streaming response.
Your users need a way to return to an earlier conversation, start a new one, correct a question, compare answers, inspect sources, and see what the model is doing when it calls a tool.
They may also need to upload a file, open a preview, or move between the chat and the rest of the application.</p>
<p>shinychat gives you sensible starting points for building that experience.
Pair it with <a href="https://ellmer.tidyverse.org/" target="_blank" rel="noopener">ellmer</a> in R or <a href="https://posit-dev.github.io/chatlas/" target="_blank" rel="noopener">chatlas</a> in Python, and you can get a working chat app running with little setup.
The chat application model has three layers:</p>
<ol>
<li><code>page_chat()</code> gives you a full-window chat app with space for navigation, history, tools, and supporting content.</li>
<li><code>chat_ui()</code> lets you place chat wherever it fits best in your application.</li>
<li><code>chat_server()</code> for R or <code>Chat(client=...)</code> for Python connects your app to an <code>ellmer</code> or <code>chatlas</code> client and enables the integrated chat features.</li>
</ol>
<p>When you want a fully custom experience or need a model client other than ellmer or chatlas, the lower-level pieces are still available for you to assemble yourself.</p>
<h2 id="start-with-page_chat">Start with <code>page_chat()</code>
</h2>
<p>When chat is the center of your application, use <code>page_chat()</code>.
It gives your users a full-window experience with a <a href="#create-a-chat-app">chat home</a>, <a href="#complete-application">navigation pages</a>, <a href="#complete-application">sidebars</a>, <a href="#toolbars">toolbars</a>, <a href="#return-to-earlier-conversations">conversation history</a>, and an <a href="#artifact-drawer">artifact drawer</a>.
Users can move to a settings or sources page while their conversation keeps working and streaming.
The <a href="https://posit-dev.github.io/shinychat/r/articles/get-started.html" target="_blank" rel="noopener">Get started</a> guide for R and the <a href="https://posit-dev.github.io/shinychat/py/page-chat.html" target="_blank" rel="noopener">Page chat</a> guide for Python walk through the full layout.</p>
<h3 id="create-a-chat-app">Create a chat app
</h3>
<p>Build a shinychat application starts similarly in both languages:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-2" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-2-1">R</a></li>
<li><a href="#tabset-2-2">Python</a></li>
</ul>
<div id="tabset-2-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">shiny</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">shinychat</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="nf">page_chat</span><span class="p">(</span><span class="n">title</span> <span class="o">=</span> <span class="s">&#34;Assistant&#34;</span><span class="p">,</span> <span class="n">id</span> <span class="o">=</span> <span class="s">&#34;chat&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">&lt;-</span> <span class="n">ellmer</span><span class="o">::</span><span class="nf">chat_openai</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">system_prompt</span> <span class="o">=</span> <span class="s">&#34;You are a helpful assistant.&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nf">chat_server</span><span class="p">(</span><span class="s">&#34;chat&#34;</span><span class="p">,</span> <span class="n">client</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span></span></span></code></pre></div></div>
</div>
<div id="tabset-2-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatAnthropic</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shinychat.express</span> <span class="kn">import</span> <span class="n">Chat</span><span class="p">,</span> <span class="n">page_chat</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="n">ChatAnthropic</span><span class="p">(</span><span class="n">system_prompt</span><span class="o">=</span><span class="s2">&#34;You are a helpful assistant.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="n">Chat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s2">&#34;chat&#34;</span><span class="p">,</span> <span class="n">client</span><span class="o">=</span><span class="n">client</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">page_chat</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;Assistant&#34;</span><span class="p">,</span> <span class="nb">id</span><span class="o">=</span><span class="s2">&#34;chat&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
</div>
</div>
<p>With just a few lines of code, you&rsquo;ll have a working chat app backed by a live LLM.
Passing a client to <code>chat_server()</code> in R, or to <code>Chat()</code> in Python, does all the hard work for you, fulling connecting your app to the model client and giving you a complete multi-user chat application<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<p>For a personal chat UI you can use while you develop locally, pass an ellmer client to <a href="https://posit-dev.github.io/shinychat/r/reference/chat_app.html" target="_blank" rel="noopener"><code>chat_app()</code></a> in R, or a chatlas client to <a href="https://posit-dev.github.io/shinychat/py/api/Chat.html" target="_blank" rel="noopener"><code>Chat(client=...)</code></a>, and then call <code>.app()</code> in Python.</p>
<h3 id="welcome-users">Welcome users
</h3>
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/greeting-suggestions-greeting.png" data-fig-alt="A new chat with a short welcome message and a grid of three suggestion cards beneath it." />
<p>When you&rsquo;re app opens, don&rsquo;t leave your users hanging with an empty chat canvas, gree them with <code>chat_greeting()</code> (<a href="https://posit-dev.github.io/shinychat/r/reference/chat_greeting.html" target="_blank" rel="noopener">R</a>, <a href="https://posit-dev.github.io/shinychat/py/api/chat_greeting.html" target="_blank" rel="noopener">Python</a>)!</p>
<p>Greetings can be used to explain the application, set expectations, and give users a useful first step before they write their first message.
By default, they disappear when the user starts chatting, but you can set <code>persistent = TRUE</code> in R or <code>persistent=True</code> in Python to keep one at the top of the conversation history.</p>
<p>Greetings are written in markdown and can even provide actionable suggestions.
Users can click a suggestion to fill the input, ready to edit before sending, or send it immediately.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gu">## Welcome!
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">What would you like to do?
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">*</span> &lt;span class=&#34;suggestion submit&#34;&gt;Summarize my data&lt;/span&gt;
</span></span><span class="line"><span class="cl"><span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Create a plot&lt;/span&gt;
</span></span><span class="line"><span class="cl">* &lt;span class=&#34;suggestion&#34;&gt;Explain this code&lt;/span&gt;</span></span></code></pre></div></div>
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/greeting-suggestions-fill-input.png" data-fig-alt="Clicking a suggestion card fills the chat input with the suggested prompt, ready to edit before sending." />
<p>You don&rsquo;t have to greet your users with the same message every time, you can use LLMs to generate fresh custom greetings.
To learn more, we&rsquo;ll point you to the <code>chat_greeting()</code> documentation pages (<a href="https://posit-dev.github.io/shinychat/r/reference/chat_greeting.html" target="_blank" rel="noopener">R</a>, <a href="https://posit-dev.github.io/shinychat/py/api/chat_greeting.html" target="_blank" rel="noopener">Python</a>), but it&rsquo;s worth noting that dynamic greetings can stream into the chat like any other response.</p>
<div class="w-full aspect-4/3">
      <video
        src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/greeting-stream.mp4"
        class="w-full h-full object-contain"
        title="A generated greeting streams into the empty chat: the welcome message arrives word by word, then two suggestion cards appear"
        controls></video>
    </div>
<h2 id="return-to-earlier-conversations">Return to earlier conversations
</h2>
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/history-list.png" data-fig-alt="The conversation history drawer open beside the chat, listing several named conversations under Today with a search field and a New conversation button." />
<p>One of the biggest features to arrive in this release is conversation history, giving your chat app the ability to save and return to previous conversations.
It will also persist the current conversation across page reloads and other disconnects, virtually eliminating the possibility of losing your progress.
As usual, when you connect shinychat with an ellmer or chatlas client, conversation history is wired up and enabled for you!</p>
<h3 id="save-conversations">Save conversations
</h3>
<p>The history drawer lets users:</p>
<ul>
<li>Start a new conversation.</li>
<li>Switch between saved conversations.</li>
<li>Search conversations.</li>
<li>Rename a conversation.</li>
<li>Delete a conversation.</li>
<li>Return to the conversation that was active when they last opened the app.</li>
</ul>
<p>shinychat generates a short title once the conversation has enough content.
Users can replace that title, and title generation never overwrites a manual rename.</p>
<div class="panel-tabset">
<ul id="tabset-3" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-3-1">Rename</a></li>
<li><a href="#tabset-3-2">Search</a></li>
</ul>
<div id="tabset-3-1">
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/history-actions-menu.png" data-fig-alt="The menu on a saved conversation with options to rename and delete it." />
</div>
<div id="tabset-3-2">
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/history-search.png" data-fig-alt="Typing in the history drawer search field narrows the conversation list to matching titles." />
</div>
</div>
<p>You can <code>history_options()</code> in R or <code>HistoryOptions</code> in Python to configure the conversations that shinychat saves.
The main options are:</p>
<ul>
<li><code>restore_mode</code>, which controls which conversation opens when a user returns to the app:
<ul>
<li><code>&quot;browser&quot;</code> is the default. It returns that browser to its most recent conversation without changing the URL.</li>
<li><code>&quot;url&quot;</code> puts the active conversation ID in the address bar, so users can bookmark or share a specific conversation.</li>
<li><code>&quot;bookmark&quot;</code> restores the conversation with the rest of the app state when your app uses Shiny server bookmarking.</li>
</ul>
</li>
<li><code>store</code> controls where shinychat saves conversations. Use <code>&quot;memory&quot;</code> for local development or tests, or <code>&quot;file&quot;</code> to save them on disk.</li>
<li><code>title</code> controls how the automated conversation titles are generated.</li>
</ul>
<p>For example, this configuration stores conversations on disk and puts the active conversation ID in the URL:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-4" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-4-1">R</a></li>
<li><a href="#tabset-4-2">Python</a></li>
</ul>
<div id="tabset-4-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">history</span> <span class="o">&lt;-</span> <span class="nf">history_options</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">restore_mode</span> <span class="o">=</span> <span class="s">&#34;url&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">store</span> <span class="o">=</span> <span class="s">&#34;file&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">chat_server</span><span class="p">(</span><span class="s">&#34;chat&#34;</span><span class="p">,</span> <span class="n">client</span><span class="p">,</span> <span class="n">history</span> <span class="o">=</span> <span class="n">history</span><span class="p">)</span></span></span></code></pre></div></div>
</div>
<div id="tabset-4-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shinychat</span> <span class="kn">import</span> <span class="n">Chat</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shinychat.types</span> <span class="kn">import</span> <span class="n">HistoryOptions</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">history</span> <span class="o">=</span> <span class="n">HistoryOptions</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">restore_mode</span><span class="o">=</span><span class="s2">&#34;url&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">store</span><span class="o">=</span><span class="s2">&#34;file&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="n">Chat</span><span class="p">(</span><span class="s2">&#34;chat&#34;</span><span class="p">,</span> <span class="n">client</span><span class="o">=</span><span class="n">client</span><span class="p">,</span> <span class="n">history</span><span class="o">=</span><span class="n">history</span><span class="p">)</span></span></span></code></pre></div></div>
</div>
</div>
<p>On Posit Connect, conversation history is included with the platform and is enabled automatically when you provide a model client.
The default configuration uses Connect&rsquo;s <a href="https://docs.posit.co/connect/user/structuring-content/#persistent-storage-on-posit-connect" target="_blank" rel="noopener">persistent storage</a> and scopes conversations to the authenticated user.
That gives every user a private conversation history without an additional history service or per-user setup.</p>
<p>In every restore mode, shinychat keeps the transcript in its configured store instead of putting the full conversation in the URL.</p>
<h3 id="edit-a-message-and-compare-answers">Edit a message and compare answers
</h3>
<div class="w-full aspect-4/3">
      <video
        src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/edit-branches-edit.mp4"
        class="w-full h-full object-contain"
        title="Editing an earlier message and resending it starts a new branch, and the sibling navigation control appears on the response"
        controls></video>
    </div>
<p>Editing a message now creates a new conversation <strong>branch</strong>.
When a user edits and resends an earlier message, shinychat forks the conversation at that point: the original question and its later messages remain on one branch, while the edited question begins another. Users can move between the answers with the branch controls in the message.</p>
<div class="panel-tabset">
<ul id="tabset-5" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-5-1">Branch 1</a></li>
<li><a href="#tabset-5-2">Branch 2</a></li>
</ul>
<div id="tabset-5-1">
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/edit-branches-original.png" data-fig-alt="The original conversation with an assistant response showing a 1 / 2 sibling navigation control." />
</div>
<div id="tabset-5-2">
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/edit-branches-new.png" data-fig-alt="The same conversation after editing a message, with the new branch&#39;s response selected and the sibling navigation control showing 2 / 2." />
</div>
</div>
<p>Branches help when a prompt is almost right or when a model takes an unhelpful direction, and they make comparing answers easy without starting over.
And they are part of the saved conversation, so users return to their place in the conversation after a reload.</p>
<h2 id="add-content-and-controls">Add content and controls
</h2>
<p>When chat is part of a larger application, your users still need access to filters, settings, sources, and results.
<code>page_chat()</code> gives you a place to put those alongside the conversation: a drawer for results, toolbars for controls, and offcanvas panels for settings you would rather keep off screen.</p>
<h3 id="artifact-drawer">Artifact drawer
</h3>
<p><code>chat_drawer()</code> gives you a place to show previews, rendered reports, tables, plots, or other bits of Shiny UI next to your chat.
Your users can keep the conversation visible while they inspect a result.</p>
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/complete-app-nav-drawer.png" data-fig-alt="The research assistant app with the Research assistant and Sources navigation pages in the header, the conversation in the main region, and the artifact drawer open beside the chat showing a bar chart of penguin counts." />
<p>See the <a href="https://posit-dev.github.io/shinychat/r/reference/chat_drawer.html" target="_blank" rel="noopener">drawer documentation for R</a> or <a href="https://posit-dev.github.io/shinychat/py/api/chat_drawer.html" target="_blank" rel="noopener">Python</a> for the full API.
The <a href="#complete-application">complete application example</a> combines a drawer with the rest of the application layout.</p>
<h3 id="toolbars">Toolbars
</h3>
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/toolbars-home.png" data-fig-alt="A close-up of a research assistant chat. Part of the conversation is visible beside the global toolbar&#39;s Refresh, Help, and Answer settings buttons, with a response style selector below the chat input." />
<p>Your app may need a Help button that works on every page, while the chat home needs an action that&rsquo;s only relevant when you&rsquo;re looking at the conversation. <code>page_chat()</code> gives each action a home through scoped <a href="https://opensource.posit.co/blog/2026-05-26_introducing-toolbars">toolbars</a>, built on the toolbar components that bslib and Shiny shipped earlier this year.</p>
<p>If you want an action to follow users through the whole app &mdash; pass it to <code>toolbar_global</code>. Put chat-home actions in <code>toolbar</code> in <code>page_chat()</code>, and give a <a href="#complete-application">secondary page</a> its own <code>toolbar</code> through <code>chat_nav_panel()</code>. <code>toolbar_input</code> puts related actions below the message box.</p>
<p>See the <a href="https://posit-dev.github.io/shinychat/r/articles/get-started.html" target="_blank" rel="noopener">R get started guide</a> or the <a href="https://posit-dev.github.io/shinychat/py/page-chat.html" target="_blank" rel="noopener">Python Page chat guide</a> for the full toolbar API.</p>
<h3 id="offcanvas-panels">Offcanvas panels
</h3>
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/toolbars-offcanvas.png" data-fig-alt="The research assistant app with the Answer settings offcanvas open along the right edge, showing a target length slider and a citations checkbox beside the conversation." />
<p><code>page_chat()</code> pairs <a href="https://opensource.posit.co/blog/2026-08-04_shiny-r-1-14-python-1-7">offcanvas panels</a> with secondary content, such as an answer-length slider or citation setting, and a toolbar button can open an <strong>Answer settings</strong> panel from any page:</p>
<h3 id="complete-application">Complete application
</h3>
<p>As your app grows, <code>page_chat()</code> can grow around the conversation. You can add secondary pages and a sidebar for filters or other app UI and the application menu keeps those options available on narrow screens.</p>
<p>The following example brings the toolbars, sidebar, navigation, and drawer together.</p>
<details class="callout callout-tip" role="note" aria-label="Tip">
<summary class="callout-header">
<span class="callout-title">A complete <code>page_chat()</code> example</span>
</summary>
<div class="callout-body">
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-6" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-6-1">R</a></li>
<li><a href="#tabset-6-2">Python</a></li>
</ul>
<div id="tabset-6-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="nf">page_chat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;Research assistant&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">id</span> <span class="o">=</span> <span class="s">&#34;chat&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">toolbar</span> <span class="o">=</span> <span class="n">bslib</span><span class="o">::</span><span class="nf">toolbar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">bslib</span><span class="o">::</span><span class="nf">toolbar_input_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;clear_chat&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;Clear conversation&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">icon</span> <span class="o">=</span> <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span><span class="s">&#34;arrow-counterclockwise&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">toolbar_global</span> <span class="o">=</span> <span class="n">bslib</span><span class="o">::</span><span class="nf">toolbar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">bslib</span><span class="o">::</span><span class="nf">toolbar_input_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;help&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;Help&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">icon</span> <span class="o">=</span> <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span><span class="s">&#34;question-circle&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">sidebar</span> <span class="o">=</span> <span class="nf">chat_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">tags</span><span class="o">$</span><span class="nf">p</span><span class="p">(</span><span class="s">&#34;Use filters to focus the results.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">history</span> <span class="o">=</span> <span class="kc">FALSE</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">pages_navbar</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">chat_nav_panel</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;Sources&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">tags</span><span class="o">$</span><span class="nf">p</span><span class="p">(</span><span class="s">&#34;Sources selected during this session appear here.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="n">toolbar</span> <span class="o">=</span> <span class="n">bslib</span><span class="o">::</span><span class="nf">toolbar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">bslib</span><span class="o">::</span><span class="nf">toolbar_input_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;refresh_sources&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;Refresh&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">icon</span> <span class="o">=</span> <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span><span class="s">&#34;arrow-repeat&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">drawer</span> <span class="o">=</span> <span class="nf">chat_drawer</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">tags</span><span class="o">$</span><span class="nf">p</span><span class="p">(</span><span class="s">&#34;Select a result to inspect it here.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">title</span> <span class="o">=</span> <span class="s">&#34;Latest result&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">open</span> <span class="o">=</span> <span class="kc">FALSE</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
</div>
<div id="tabset-6-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">faicons</span> <span class="kn">import</span> <span class="n">icon_svg</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny</span> <span class="kn">import</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shinychat</span> <span class="kn">import</span> <span class="n">chat_drawer</span><span class="p">,</span> <span class="n">chat_nav_panel</span><span class="p">,</span> <span class="n">chat_sidebar</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shinychat.express</span> <span class="kn">import</span> <span class="n">page_chat</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">page_chat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Research assistant&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nb">id</span><span class="o">=</span><span class="s2">&#34;chat&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">toolbar</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">toolbar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">toolbar_input_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="nb">id</span><span class="o">=</span><span class="s2">&#34;clear_chat&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">label</span><span class="o">=</span><span class="s2">&#34;Clear conversation&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">icon</span><span class="o">=</span><span class="n">icon_svg</span><span class="p">(</span><span class="s2">&#34;arrow-counterclockwise&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">toolbar_global</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">toolbar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">toolbar_input_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="nb">id</span><span class="o">=</span><span class="s2">&#34;help&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">label</span><span class="o">=</span><span class="s2">&#34;Help&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">icon</span><span class="o">=</span><span class="n">icon_svg</span><span class="p">(</span><span class="s2">&#34;question-circle&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">sidebar</span><span class="o">=</span><span class="n">chat_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">p</span><span class="p">(</span><span class="s2">&#34;Use filters to focus the results.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">history</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">pages_navbar</span><span class="o">=</span><span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="n">chat_nav_panel</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Sources&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">ui</span><span class="o">.</span><span class="n">p</span><span class="p">(</span><span class="s2">&#34;Sources selected during this session appear here.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">toolbar</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">toolbar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="n">ui</span><span class="o">.</span><span class="n">toolbar_input_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                    <span class="nb">id</span><span class="o">=</span><span class="s2">&#34;refresh_sources&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="n">label</span><span class="o">=</span><span class="s2">&#34;Refresh&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="n">icon</span><span class="o">=</span><span class="n">icon_svg</span><span class="p">(</span><span class="s2">&#34;arrow-repeat&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="n">drawer</span><span class="o">=</span><span class="n">chat_drawer</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">p</span><span class="p">(</span><span class="s2">&#34;Select a result to inspect it here.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">title</span><span class="o">=</span><span class="s2">&#34;Latest result&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nb">open</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
</div>
</div>
<p>Users see the <strong>Clear conversation</strong> button while they chat, the <strong>Help</strong> button on every page, a <strong>Sources</strong> page with its own <strong>Refresh</strong> toolbar, and a <strong>Latest result</strong> drawer beside the conversation.</p>
</div>
</details>
<h2 id="show-how-the-model-reached-an-answer">Show how the model reached an answer
</h2>
<p>Understanding how an LLM arrived at an answer is just as &mdash; if not more &mdash; important than getting the answer from the model.
A response can include ordinary text, thinking content, web activity, citations, tool calls, tool results, and custom UI.
shinychat works hard to make the model&rsquo;s work visible and presents each part in a way that helps users understand the answer and what produced it.</p>
<h3 id="keep-tool-calls-readable">Keep tool calls readable
</h3>
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/tool-calls-collapsed.png" data-fig-alt="A sales assistant conversation where two SQL queries and a schema read appear as compact activity rows above the answer." />
<p>Tool calls are now shown as compact activity rows instead of letting them take over the conversation, refining the <a href="https://opensource.posit.co/blog/2025-11-20_shinychat-tool-ui">tool-call cards shinychat introduced last year</a>.
By default, related calls are grouped together into a single row, and users can still expand a group, open an individual call, and inspect the request and result when they need more detail.</p>
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/tool-calls-expanded.png" data-fig-alt="The grouped tool-call row expanded to show the two SQL queries with row count and result previews." />
<p>Opening an individual call shows the request and the result in a card:</p>
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/tool-calls-result.png" data-fig-alt="The SQL query expanded to a card showing the full tool call arguments and the query result as a small table." />
<p>Grouping keeps the answer readable, and the request and result stay one click away.
To customize grouping or register tools, see <a href="https://posit-dev.github.io/shinychat/r/articles/tool-ui.html" target="_blank" rel="noopener">Tool UI in shinychat for R</a>, <a href="https://shiny.posit.co/py/docs/genai-tools.html" target="_blank" rel="noopener">Tools in Shiny for Python</a>, <a href="https://ellmer.tidyverse.org/articles/tool-calling.html" target="_blank" rel="noopener">tool/function calling in ellmer</a>, or <a href="https://posit-dev.github.io/chatlas/get-started/tools.html" target="_blank" rel="noopener">tool calling in chatlas</a>.</p>
<h3 id="show-citations-for-web-search-and-fetch">Show citations for web search and fetch
</h3>
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/citations-popover.png" data-fig-alt="An assistant response where each cited claim is underlined and a pill reading Internal report +1 marks the message&#39;s sources, with the citation popover open just below the pill showing the source name, a link, the supporting passage, and controls to move between the message&#39;s two citations." />
<p>Many LLM providers offer built-in web search and web fetch tools that let your agent search the web, and their APIs return citations when the model uses that content in a reply. shinychat now displays those citations automatically.</p>
<p>For example, here&rsquo;s how to register Claude&rsquo;s tools with an ellmer or chatlas client:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-7" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-7-1">R</a></li>
<li><a href="#tabset-7-2">Python</a></li>
</ul>
<div id="tabset-7-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ellmer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">client</span><span class="o">$</span><span class="nf">register_tool</span><span class="p">(</span><span class="nf">claude_tool_web_search</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="n">client</span><span class="o">$</span><span class="nf">register_tool</span><span class="p">(</span><span class="nf">claude_tool_web_fetch</span><span class="p">())</span></span></span></code></pre></div></div>
</div>
<div id="tabset-7-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatAnthropic</span><span class="p">,</span> <span class="n">tool_web_fetch</span><span class="p">,</span> <span class="n">tool_web_search</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="n">ChatAnthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">kwargs</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;default_headers&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;anthropic-beta&#34;</span><span class="p">:</span> <span class="s2">&#34;web-fetch-2025-09-10&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">client</span><span class="o">.</span><span class="n">register_tool</span><span class="p">(</span><span class="n">tool_web_search</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="n">client</span><span class="o">.</span><span class="n">register_tool</span><span class="p">(</span><span class="n">tool_web_fetch</span><span class="p">())</span></span></span></code></pre></div></div>
</div>
</div>
<p>When this client is used with <code>chat_server()</code>, citations are connected directly to the portions of the assistant&rsquo;s response that they support.</p>
<p>Custom retrieval applications, like the RAG systems you can build with <a href="https://ragnar.tidyverse.org/" target="_blank" rel="noopener">ragnar</a> or <a href="https://opensource.posit.co/blog/2026-04-14_rag-with-raghilda">raghilda</a>, can use the same citation UI by prompting the assistant to use a <code>&lt;shiny-aside&gt;</code> tag to attach a source to a claim.</p>
<h3 id="stream-responses-and-show-thinking">Stream responses and show thinking
</h3>
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/thinking-collapsed.png" data-fig-alt="An assistant response with a collapsed panel reading Thought for 4s between the user&#39;s question and the answer." />
<p>With <code>chat_server()</code> in R or <code>Chat(client=...)</code> in Python, shinychat streams responses and shows supported thinking content in a collapsible panel.
Users can cancel a slow response with the stop button or the Escape key, and the partial response stays in the conversation.</p>
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/streaming-stop.png" data-fig-alt="While a response streams in, the send button at the right of the chat input becomes a red stop button." />
<h2 id="add-files-and-shortcuts">Add files and shortcuts
</h2>
<h3 id="attach-files">Attach files
</h3>
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/attachments-plot.png" data-fig-alt="A plot attached to the chat input as a thumbnail chip above the prompt Explain this plot, with the attach button at the left of the input." />
<p>File attachments are now supported in shinychat! Your users can send images, PDFs, and text files through a file picker, drag and drop, or paste, and shinychat sends each file to the model alongside the user&rsquo;s message. When you use <code>chat_server()</code> in R or <code>Chat(client=...)</code> in Python, your app gets that support for free.</p>
<h3 id="add-slash-commands">Add slash commands
</h3>
<img src="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/slash-commands-palette.png" data-fig-alt="The slash command palette open above the chat input, listing /help, /search, and /clear with short descriptions." />
<p>You can now register chat shortcuts, or <em>slash commands</em>, with <a href="https://posit-dev.github.io/shinychat/r/reference/chat_server.html" target="_blank" rel="noopener"><code>chat$slash_command()</code></a> in R or <a href="https://posit-dev.github.io/shinychat/py/api/Chat.html" target="_blank" rel="noopener"><code>@chat.slash_command()</code></a> in Python. The command palette appears when users type <code>/</code>, and they serve as a way to trigger server-side code, inject context or additional prompting, or even just take an action in your app, all from the chat input.</p>
<p>Check out the <a href="https://posit-dev.github.io/shinychat/r/" target="_blank" rel="noopener">shinychat for R</a> or <a href="https://posit-dev.github.io/shinychat/py/" target="_blank" rel="noopener">shinychat for Python</a> documentation for details.</p>
<h2 id="more-shinychat-powered-apps">More shinychat-powered apps
</h2>
<p>The next release of <a href="https://posit-dev.github.io/querychat/" target="_blank" rel="noopener">querychat</a> will bring these chat features to data applications, including conversation history, attachments, tool displays, and citations.
It will introduce a page-first <code>querychat_app()</code> workflow and a new <code>page()</code> API for adding querychat to an existing Shiny page.</p>
<p><a href="https://posit-dev.github.io/btw/news/index.html#btw-150" target="_blank" rel="noopener">btw 1.5.0</a> already uses shinychat 0.5.0 to give <code>btw_app()</code> a complete coding assistant for your R projects.
It adds conversation history, a <code>page_chat()</code> layout, and slash commands to an assistant that can use your R session, project files, and package documentation.</p>
<h2 id="a-few-changes-for-existing-apps">A few changes for existing apps
</h2>
<p>Existing <code>chat_ui()</code> applications remain supported when chat shares a page with other top-level content. When the conversation should fill the application instead, choose <code>page_chat()</code> and use it as the outermost page container; nesting it inside another page layout breaks the full-window layout and history experience.</p>
<p>In R, <code>chat_mod_ui()</code> and <code>chat_mod_server()</code> are soft-deprecated in favor of pairing <code>chat_ui()</code> and <code>chat_server()</code> by ID. In both languages, a startup message no longer seeds a conversation when history is enabled; use a greeting or append messages through the chat object instead.</p>
<p>The release also protects users from unsafe model-authored Markdown, shows an error when a response fails before streaming starts, and preserves tool results, citations, attachments, and other rich content when users return to a conversation.</p>
<p>With <code>page_chat()</code>, <code>chat_server()</code> or <code>Chat(client=...)</code>, and the history options, you can now give your users a complete chat application: saved conversations they can return to, messages they can edit into new branches, greetings and suggestions to start from, and responses with visible tool calls, citations, and thinking.</p>
<p>Read the <a href="https://posit-dev.github.io/shinychat/r/" target="_blank" rel="noopener">shinychat for R documentation</a> or the <a href="https://posit-dev.github.io/shinychat/py/" target="_blank" rel="noopener">shinychat for Python documentation</a> to explore the examples.
For the complete list of changes, see the <a href="https://github.com/posit-dev/shinychat/blob/main/pkg-r/NEWS.md" target="_blank" rel="noopener">R release notes</a> and the <a href="https://github.com/posit-dev/shinychat/blob/main/pkg-py/CHANGELOG.md" target="_blank" rel="noopener">Python changelog</a>.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>We thank everyone who contributed to these releases, for opening issues,
submitting pull requests, and providing feedback:
<a href="https://github.com/bastianolea" target="_blank" rel="noopener">@bastianolea</a>,
<a href="https://github.com/bianchenhao" target="_blank" rel="noopener">@bianchenhao</a>,
<a href="https://github.com/christophsax" target="_blank" rel="noopener">@christophsax</a>,
<a href="https://github.com/cpsievert" target="_blank" rel="noopener">@cpsievert</a>,
<a href="https://github.com/crissthiandi" target="_blank" rel="noopener">@crissthiandi</a>,
<a href="https://github.com/elnelson575" target="_blank" rel="noopener">@elnelson575</a>,
<a href="https://github.com/gadenbuie" target="_blank" rel="noopener">@gadenbuie</a>,
<a href="https://github.com/Harshit28j" target="_blank" rel="noopener">@Harshit28j</a>,
<a href="https://github.com/JamesHWade" target="_blank" rel="noopener">@JamesHWade</a>,
<a href="https://github.com/jcheng5" target="_blank" rel="noopener">@jcheng5</a>,
<a href="https://github.com/jlxAtNovozymes" target="_blank" rel="noopener">@jlxAtNovozymes</a>,
<a href="https://github.com/jnhyeon" target="_blank" rel="noopener">@jnhyeon</a>,
<a href="https://github.com/jose-c-milliman" target="_blank" rel="noopener">@jose-c-milliman</a>,
<a href="https://github.com/kaipingyang" target="_blank" rel="noopener">@kaipingyang</a>,
<a href="https://github.com/lucasrod16" target="_blank" rel="noopener">@lucasrod16</a>,
<a href="https://github.com/markmcd" target="_blank" rel="noopener">@markmcd</a>,
<a href="https://github.com/nbenn" target="_blank" rel="noopener">@nbenn</a>,
<a href="https://github.com/parmsam" target="_blank" rel="noopener">@parmsam</a>,
<a href="https://github.com/schloerke" target="_blank" rel="noopener">@schloerke</a>,
<a href="https://github.com/shea-parkes" target="_blank" rel="noopener">@shea-parkes</a>,
<a href="https://github.com/simonpcouch" target="_blank" rel="noopener">@simonpcouch</a>,
<a href="https://github.com/slupczynskim" target="_blank" rel="noopener">@slupczynskim</a>,
<a href="https://github.com/thisisnic" target="_blank" rel="noopener">@thisisnic</a>,
<a href="https://github.com/wlandau" target="_blank" rel="noopener">@wlandau</a>, and
<a href="https://github.com/xx02al" target="_blank" rel="noopener">@xx02al</a>.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>If you&rsquo;re new to LLM apps with Shiny, <a href="https://opensource.posit.co/blog/2025-09-15_shiny-side-of-llms-part-3">Build Your First LLM App with Shiny</a> walks through the process from the beginning in detail.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-09-15_shinychat-r-0.5.0-python-0.7.1/images/og-header.png" length="78200" type="image/png" />
    </item>
    <item>
      <title>Positron September Release Highlights</title>
      <link>https://opensource.posit.co/blog/2026-09-09_positron-2026-09-release/</link>
      <pubDate>Wed, 09 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-09-09_positron-2026-09-release/</guid>
      <dc:creator>Julia Silge</dc:creator><description><![CDATA[<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Note</span>
</div>
<div class="callout-body">
<p><a href="https://positron.posit.co" target="_blank" rel="noopener">Positron</a> is Posit&rsquo;s new, next-generation IDE for data science. Positron is designed to be an extensible, polyglot tool for exploring data and reproducible authoring in Python, R, and more.</p>
</div>
</div>
<p>Welcome back to another edition of our monthly Positron updates! Each month we share highlights from our <a href="https://positron.posit.co/release-notes" target="_blank" rel="noopener">latest release</a> and useful resources. <a href="https://opensource.posit.co/blog/2026-08-13_positron-2026-08-release">Last release</a> we told you about new Data Connections sources, a round of polish for inline output in Quarto documents, and help installing missing packages. This milestone brings a redesigned welcome page, a first version of Import Data, new Posit Assistant features, expanded Data Connections, package vulnerability scanning, and a more responsive Console.</p>
<h2 id="welcome-page-refresh-and-interpreter-setup">Welcome page refresh and interpreter setup
</h2>
<p>We redesigned the Positron welcome page. It now leads with an environment setup card that checks whether Python and R are ready to use, with actions to help resolve any problem it finds. The Positron badge and name now come with a <strong>Help</strong> button that opens the Help pane, and a banner links to the walkthroughs, including a new &ldquo;Get Started with Positron&rdquo; walkthrough that covers the Positron panes, keyboard shortcuts, built-in extensions, and Git.</p>
<img src="https://opensource.posit.co/blog/2026-09-09_positron-2026-09-release/welcome-page.gif" data-fig-align="center" data-fig-alt="The redesigned Positron Welcome page, showing an Environment setup card with Python checks (3 of 4 passed) including a Create Python Environment button, and R already set up successfully." />
<p>The environment setup theme continues into interpreter selection itself. When you select a Python managed by your operating system or a package manager, Positron now offers to create a virtual environment for your workspace instead of installing packages into that shared interpreter. With no folder open, the environment Positron creates now lives at <code>~/.virtualenvs/positron</code> rather than <code>~/.venv</code>, so it stays in the interpreter picker after a restart, and Positron asks before creating an environment in your home directory. We also removed the confusing startup notification that reported no interpreters were found while linking to documentation saying no setup was needed, and interpreters installed under <code>/opt/python</code> are now labeled <code>Global</code> instead of <code>Unknown</code> in the interpreter picker.</p>
<h2 id="import-data">Import Data
</h2>
<p>Before we started work this month, importing data via the Positron UI was our most upvoted feature request and this release now delivers a first version. When you view a CSV or TSV file in the Data Explorer, a new <strong>Import Data</strong> button in the action bar opens a dialog. The dialog shows the code to load that file into a data frame. You can copy the code, or click <strong>Import</strong> to run it in the console, starting a session if one is not already running.</p>
<img src="https://opensource.posit.co/blog/2026-09-09_positron-2026-09-release/import-data-excel.gif" data-fig-align="center" data-fig-alt="An Excel spreadsheet open in the Data Explorer with an Import Data button in the action bar, next to a Python console session ready to run the generated import code." />
<p>Import Data supports CSV and TSV files in Python with pandas and in R with the readr package, as well as Excel workbooks and Parquet files (with readxl and nanoparquet in R). The generated code can reproduce the filters and sorts you have applied in the Data Explorer, and it names the file by a workspace-relative path when the file is inside your workspace, so the code is easier to share and rerun. You can open the dialog from the Data Explorer, the File menu, the Variables pane, or the File Explorer context menu.</p>
<h2 id="posit-assistant">Posit Assistant
</h2>
<p>This release brings a new <strong>Agent Layout</strong> that opens <a href="https://pos.it/assistant" target="_blank" rel="noopener">Posit Assistant</a> in the editor area with a compact Session pane, giving you more visibility into the agent&rsquo;s actions as it works alongside your code. Configuring language model providers also gets a redesign. Our new Configure LLM Providers modal groups providers by connection state, so you can see at a glance which providers are ready to use. If you have trouble with the new dialog and need to switch back, set <a href="positron://settings/assistant.newProviderModal"><code>assistant.newProviderModal</code></a> to <code>false</code>.</p>
<img src="https://opensource.posit.co/blog/2026-09-09_positron-2026-09-release/provider-modal-dialog.png" data-fig-align="center" data-fig-alt="The Configure LLM Providers modal in Positron, listing connected providers (Posit AI Pass, Anthropic, GitHub Copilot) and additional model providers available to connect (Amazon Bedrock, Microsoft Foundry, OpenAI)." />
<p>You can now configure multiple custom providers, each with its own name, type, endpoint, credential, and model list; the previous single &ldquo;Custom Provider&rdquo; option is now called &ldquo;OpenAI Compatible&rdquo; to better match what it actually does. Amazon Bedrock users get a smoother experience as well. An expired AWS SSO session can be renewed right from the provider modal instead of requiring <code>aws sso login</code> in a terminal, and the AWS profile and region can now be set in the configuration dialog rather than only through environment variables or a hand-edited <code>providers.json</code>. Speaking of which, <code>providers.json</code> now accepts comments, and your comments survive edits that Positron makes to the file.</p>
<h2 id="data-connections">Data Connections
</h2>
<p>The Data Connections preview keeps growing. A new ODBC data connection driver lets you browse any database with an installed ODBC driver in the Connections pane and open it in the Data Explorer. Data sources already configured on your machine appear automatically. Databricks gains OAuth sign-in on desktop and reads <code>DATABRICKS_TOKEN</code>, <code>DATABRICKS_HOST</code>, and <code>DATABRICKS_CONFIG_FILE</code> credentials managed by Posit Workbench automatically. A <strong>Disconnect</strong> option in the context menu closes a connection and any Data Explorers opened from it, and each data connection driver now gets its own log output channel.</p>
<p>Smaller improvements round out the preview. You can now set <a href="positron://settings/dataConnections.enabled"><code>dataConnections.enabled</code></a> per workspace, so a repository can turn on the Connections pane for anyone who opens it. The tree is shallower so table and column names get more of the panel&rsquo;s width, and a new <a href="positron://settings/dataConnections.tree.indent"><code>dataConnections.tree.indent</code></a> setting controls the indentation. DuckDB connections now default to read-only, so the Connections pane and a Python or R session can have the same database open at once; when a lock conflict does happen, the error now explains that another session has locked the database.</p>
<h2 id="package-security-vulnerabilities">Package security vulnerabilities
</h2>
<p>The Packages pane now shows known security vulnerabilities (Common Vulnerabilities and Exposures scoring) for installed Python and R packages, so you can see at a glance whether something in your environment has a known CVE. The data comes from your environment&rsquo;s own Posit Package Manager repository when it has one, and from the public Posit instance otherwise, so what you see reflects the same package source your organization already governs.</p>
<img src="https://opensource.posit.co/blog/2026-09-09_positron-2026-09-release/packages-pane-cve.png" data-fig-align="center" data-fig-alt="The Packages pane showing the tornado package&#39;s Security tab with three known vulnerabilities listed by severity, each with a CVSS score, description, and the version where it was fixed." />
<h2 id="a-more-responsive-console">A more responsive Console
</h2>
<p>Console code submission is now faster, always shows visual feedback, and can be canceled while a completeness check is in flight; the Console no longer waits indefinitely with no feedback when a kernel is slow or unreachable. The Console breaks multi-statement input into complete expressions and executes them statement by statement for languages that support it. A new <a href="positron://settings/console.promptWhenIncomplete"><code>console.promptWhenIncomplete</code></a> setting runs submitted code immediately without a completeness check.</p>
<p>A few more Console fixes are worth knowing about. The Console no longer takes focus at runtime startup when you are working in another view or editor, <strong>Interrupt</strong> stays visible after switching between busy consoles, and session names now ellipsize to fit as the console tab list narrows.</p>
<h2 id="whats-coming-next">What&rsquo;s coming next
</h2>
<ul>
<li>posit::conf(2026) is next week! Our team will have several sessions on Positron, and there is still time to <a href="https://conf.posit.co/2026/" target="_blank" rel="noopener">register</a> to join us virtually from anywhere in the world.</li>
<li>Meet Posit at <a href="https://posit.co/events/cdao-government-2026" target="_blank" rel="noopener">CDAO Government</a> on September 22-23 in Washington, D.C. Stop by our booth to talk data modernization and where agentic AI fits in government.</li>
</ul>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Tip</span>
</div>
<div class="callout-body">
<p><a href="https://positron.posit.co/download" target="_blank" rel="noopener">Download Positron</a> to try out the new features and improvements in this release!</p>
</div>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-09-09_positron-2026-09-release/featured.svg" length="93678" type="image/svg&#43;xml" />
    </item>
    <item>
      <title>posit::glimpse() Newsletter – September 2026</title>
      <link>https://opensource.posit.co/blog/2026-09-02_glimpse-2026-09/</link>
      <pubDate>Wed, 02 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-09-02_glimpse-2026-09/</guid>
      <dc:creator>Isabella Velásquez</dc:creator><description><![CDATA[<blockquote>
<p>Welcome to our newsletter, posit::glimpse()!</p>
<p>If you&rsquo;re currently reading this on our blog, consider subscribing to Product Updates - Open Source on our <a href="https://posit.co/about/subscription-management" target="_blank" rel="noopener">subscription page</a> to receive this newsletter directly in your inbox.</p>
</blockquote>
<p>Welcome to this month&rsquo;s roundup of the latest open-source developments from Posit! Our update list is slightly shorter this time around, because our team is hard at work putting the final touches on <a href="https://posit.co/conference/" target="_blank" rel="noopener"><strong>posit::conf</strong></a>!</p>
<p>In this special feature, we are spotlighting a few presentations that will be broadcast live throughout the conference. Be sure to <a href="https://conf.posit.co/2026/registration/" target="_blank" rel="noopener"><strong>register today</strong></a> if you&rsquo;d like to tune in live. And, if you enjoy being chronically online like me, I&rsquo;ll be hanging out in the Discord community server throughout the event, alongside your 1000+ closest data pals 👾</p>
<h2 id="key-product-updates-and-new-releases">Key product updates and new releases
</h2>
<h3 id="positron-august-release-highlights">Positron August Release Highlights
</h3>
<p>The <a href="https://positron.posit.co/" target="_blank" rel="noopener">Positron</a> 2026.08 Release blog post highlights several key updates and improvements to Posit’s next-generation data science IDE, including expanded Data Connections (preview), more polished Quarto inline output, centralized AI provider configuration, and performance and reliability upgrades.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-08-13_positron-2026-08-release/">Positron August Release Highlights</a> blog post.</li>
</ul>
<h3 id="kimi-k3-and-glm-52-are-now-in-posit-ai">Kimi K3 and GLM 5.2 are now in Posit AI
</h3>
<p>Posit has added two new open-weights models, Kimi K3 and GLM 5.2, to the <a href="https://posit.ai" target="_blank" rel="noopener">Posit AI</a> platform. Both models stream tokens nearly twice as fast as Anthropic’s models in internal testing and offer lower cost-per-token rates (partly due to more efficient tokenizers and no extra charge for cache writes).</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-08-10_kimi-k3-glm-5-2-posit-ai/">Kimi K3 and GLM 5.2 are now in Posit AI</a> blog post.</li>
<li>Learn how to pick your models in the <a href="https://opensource.posit.co/blog/2026-08-14_ai-newsletter/">AI Newsletter: How to choose a model</a> blog post.</li>
</ul>
<h3 id="cudaml-040">cuda.ml 0.4.0
</h3>
<p><a href="https://mlverse.github.io/cuda.ml/" target="_blank" rel="noopener">cuda.ml</a> is an R package that brings <a href="https://docs.nvidia.com/cuml/" target="_blank" rel="noopener">NVIDIA GPU-accelerated machine learning</a> directly to R workflows. This release introduces streamlined setup and developer experience and expanded tree-ensemble inference.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-08-21_cuda-ml-0-4-0/">cuda.ml 0.4.0: GPU-accelerated machine learning from R</a> blog post.</li>
</ul>
<h3 id="orbital-060">Orbital 0.6.0
</h3>
<p><a href="https://posit-dev.github.io/orbital/" target="_blank" rel="noopener">Orbital</a> converts Scikit-learn pipelines into SQL queries so that they can run in your database. Orbital 0.6.0 adds PyTorch neural network support, enabling trained torch.nn.Sequential models to compile directly to SQL for database-native inference without requiring a Python runtime, ONNX Runtime, or separate model server.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-08-17_pyorbital-0-6-0/">Neural networks in Orbital for Python 0.6.0: PyTorch straight to your database</a> blog post.</li>
</ul>
<h3 id="recipes-140">recipes 1.4.0
</h3>
<p><a href="https://recipes.tidymodels.org/" target="_blank" rel="noopener">recipes</a> lets you create a pipeable sequence of feature engineering steps. Recipes 1.4.0 introduces a new way to look inside a recipe partway through, a substantial speedup for steps that are applied to many columns, and multi-column support in <code>step_regex()</code> and <code>step_count()</code>.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-08-26_recipes-1-4-0/">recipes 1.4.0</a> blog post.</li>
</ul>
<h3 id="themis-110">themis 1.1.0
</h3>
<p><a href="https://themis.tidymodels.org/" target="_blank" rel="noopener">themis</a> contains extra steps for the recipes package for dealing with unbalanced data. Version 1.1.0 adds eleven new sampling steps for handling unbalanced data. It also adds setting sampling targets per class and more distance metrics.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-08-13_themis-1-1-0/">themis 1.1.0</a> blog post.</li>
</ul>
<h2 id="new-cheatsheets">New cheatsheets
</h2>
<p>We’ve refreshed our cheatsheets page, which now features three brand-new additions!</p>
<ul>
<li><strong>Python Polars:</strong> Developed in partnership with Polars, Inc., this cheatsheet introduces Polars core concepts and key expressions.</li>
<li><strong>tidymodels:</strong> Updated cheatsheets detailing practical workflows across the entire tidymodels ecosystem.</li>
<li><strong>yardstick:</strong> Guidance on using yardstick within tidymodels to evaluate model predictive performance.</li>
</ul>
<p>Check them below:</p>















  
  
  
  
  

  
  
  
  
  
    
  

  
  
  
  
  
    
  

  
  
  
  
  
    
  

  
  
  
  
  



<div class="grid grid-cols-3 gap-6 xl:gap-8 xl:gap-8 mb-6">
  
    <div>
      











  
  
  
  
  
















  
  
    
  












  
  













  
  






  





  
    
  
    
  

















































<div id="item-2908cd68b8ba4555ebed001a8fc489bd" class="@container h-full">
  <a href="https://opensource.posit.co/resources/cheatsheets/polars/" title="Python Polars: The Definitive Cheatsheet"
     class="card-link flex flex-col @venti:flex-row rounded-xl  h-full">

      



      
      

    
    
    <div class="w-full @venti:w-1/2 @venti:self-center aspect-[40/21] flex-shrink-0 flex items-center justify-center overflow-hidden rounded-xl" style="background-color: color-mix(in srgb, #cd792c 25%, white 75%);">
      
        









  
  
    <img src="https://opensource.posit.co/resources/cheatsheets/polars/hex-polars.svg" alt="Python Polars: The Definitive Cheatsheet" class="mx-auto h-full object-contain px-[20%] py-[5%]" loading="lazy">
  
  


      
    </div>

    
    <div class="hidden gap-y-2 @nip:flex @venti:w-1/2 flex-auto flex-col p-3 @venti:pl-5 text-gray-700 mt-2">

      
      

      
      

      
      <h3 class="m-0! mt-3 font-semibold text-sm @grande:text-lg @venti:text-xl @trenta:text-2xl line-clamp-2 text-sm @tall:text-base @grande:text-lg @venti:text-xl @trenta:text-2xl text-gray-600">Python Polars: The Definitive Cheatsheet</h3>
      

      
        <p class="m-0! text-xs @short:text-sm @grande:text-md @venti:text-md @trenta:text-lg line-clamp-2 short@line-clamp-3 @tall:line-clamp-3 font-medium text-gray-600">Quick reference guide for transforming, analyzing, and visualizing data with Python Polars</p>
      

      
        <div class="text-sm @grande:text-md">
          <div class="mt-2 flex flex-row gap-x-4 items-center"><div class="flex flex-row flex-shrink-0"><img 
          src="https://opensource.posit.co/people/jeroen-janssens/jeroenjanssens-headshot-2021.png" 
          alt="Jeroen Janssens" 
          class="my-0! w-6 h-6 rounded-full object-cover ring-2 ring-white "
          style="z-index: 10;"
        ><img 
          src="https://opensource.posit.co/people/thijs-nieuwdorp/thijs-nieuwdorp.png" 
          alt="Thijs Nieuwdorp" 
          class="my-0! w-6 h-6 rounded-full object-cover ring-2 ring-white -ml-2"
          style="z-index: 9;"
        ></div><div class="line-clamp-2 font-medium text-gray-600">Jeroen Janssens,&nbsp;Thijs Nieuwdorp</div></div>
        </div>
      

      <div class="grow"></div>
      
        
          <div class="hidden @grande:flex flex-wrap gap-x-1 gap-y-4 py-2 overflow-hidden h-10 items-end">
            
              <span class="pill">
                Great Tables
              </span>
            
              <span class="pill">
                plotnine
              </span>
            
          </div>
        
      
    </div>
  </a>
</div>


    </div>
  
    <div>
      











  
  
  
  
  
















  
  
    
  












  
  













  
  






  





  
    
  

















































<div id="item-a3d4e12169eb42646b2fdafefb47cfdd" class="@container h-full">
  <a href="https://opensource.posit.co/resources/cheatsheets/ml-tidymodels/" title="Machine learning with tidymodels"
     class="card-link flex flex-col @venti:flex-row rounded-xl  h-full">

      



      
      

    
    
    <div class="w-full @venti:w-1/2 @venti:self-center aspect-[40/21] flex-shrink-0 flex items-center justify-center overflow-hidden rounded-xl" style="background-color: color-mix(in srgb, #1C1C29 25%, white 75%);">
      
        









  
  
    
    

    

    
      
      

      
      
      
        
      

      
      
      
      

      <picture class="hidden @max-tall:block w-full h-full">
        <source
          type="image/webp"
          srcset="/software/tidymodels/logo_hu_ca5e9532cdddf882.webp 1x,
                  /software/tidymodels/logo_hu_eb8d68152adc5d81.webp 2x" />
        <img
          src="https://opensource.posit.co/software/tidymodels/logo_hu_84b90ab5614abc10.png"
          srcset="/software/tidymodels/logo_hu_84b90ab5614abc10.png 1x,
                  /software/tidymodels/logo_hu_d8a687753dd491a7.png 2x"
          alt="tidymodels"
          class="mx-auto h-full object-contain px-[20%] py-[5%]"
          loading="lazy" />
      </picture>
    
      
      

      
      
        
      
      
        
      

      
      
      
      

      <picture class="hidden @tall:@max-grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/software/tidymodels/logo_hu_eb8d68152adc5d81.webp 1x,
                  /software/tidymodels/logo_hu_eb8d68152adc5d81.webp 2x" />
        <img
          src="https://opensource.posit.co/software/tidymodels/logo_hu_d8a687753dd491a7.png"
          srcset="/software/tidymodels/logo_hu_d8a687753dd491a7.png 1x,
                  /software/tidymodels/logo_hu_d8a687753dd491a7.png 2x"
          alt="tidymodels"
          class="mx-auto h-full object-contain px-[20%] py-[5%]"
          loading="lazy" />
      </picture>
    
      
      

      
      
        
      
      
        
      

      
      
      
      

      <picture class="hidden @grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/software/tidymodels/logo_hu_eb8d68152adc5d81.webp 1x,
                  /software/tidymodels/logo_hu_eb8d68152adc5d81.webp 2x" />
        <img
          src="https://opensource.posit.co/software/tidymodels/logo_hu_d8a687753dd491a7.png"
          srcset="/software/tidymodels/logo_hu_d8a687753dd491a7.png 1x,
                  /software/tidymodels/logo_hu_d8a687753dd491a7.png 2x"
          alt="tidymodels"
          class="mx-auto h-full object-contain px-[20%] py-[5%]"
          loading="lazy" />
      </picture>
    
  
  


      
    </div>

    
    <div class="hidden gap-y-2 @nip:flex @venti:w-1/2 flex-auto flex-col p-3 @venti:pl-5 text-gray-700 mt-2">

      
      

      
      

      
      <h3 class="m-0! mt-3 font-semibold text-sm @grande:text-lg @venti:text-xl @trenta:text-2xl line-clamp-2 text-sm @tall:text-base @grande:text-lg @venti:text-xl @trenta:text-2xl text-gray-600">Machine learning with tidymodels</h3>
      

      
        <p class="m-0! text-xs @short:text-sm @grande:text-md @venti:text-md @trenta:text-lg line-clamp-2 short@line-clamp-3 @tall:line-clamp-3 font-medium text-gray-600">A map of the tidymodels packages, grouped by where each one fits in the machine learning workflow</p>
      

      
        <div class="text-sm @grande:text-md">
          <div class="mt-2 flex flex-row gap-x-4 items-center"><div class="flex flex-row flex-shrink-0"><img 
          src="https://opensource.posit.co/people/edgar-ruiz/profile.jpg" 
          alt="Edgar Ruiz" 
          class="my-0! w-6 h-6 rounded-full object-cover ring-2 ring-white "
          style="z-index: 10;"
        ></div><div class="line-clamp-2 font-medium text-gray-600">Edgar Ruiz</div></div>
        </div>
      

      <div class="grow"></div>
      
        
          <div class="hidden @grande:flex flex-wrap gap-x-1 gap-y-4 py-2 overflow-hidden h-10 items-end">
            
              <span class="pill">
                tidymodels
              </span>
            
          </div>
        
      
    </div>
  </a>
</div>


    </div>
  
    <div>
      











  
  
  
  
  
















  
  
    
  












  
  













  
  






  





  
    
  

















































<div id="item-de272c2135908741feb1e0b0f2c223a2" class="@container h-full">
  <a href="https://opensource.posit.co/resources/cheatsheets/ml-measure-performance/" title="Measure model performance with yardstick"
     class="card-link flex flex-col @venti:flex-row rounded-xl  h-full">

      



      
      

    
    
    <div class="w-full @venti:w-1/2 @venti:self-center aspect-[40/21] flex-shrink-0 flex items-center justify-center overflow-hidden rounded-xl" style="background-color: color-mix(in srgb, #DB1E25 25%, white 75%);">
      
        









  
  
    
    

    

    
      
      

      
      
      
        
      

      
      
      
      

      <picture class="hidden @max-tall:block w-full h-full">
        <source
          type="image/webp"
          srcset="/software/yardstick/logo_hu_89544a764e9e99f0.webp 1x,
                  /software/yardstick/logo_hu_1a82d12a9e8dfc22.webp 2x" />
        <img
          src="https://opensource.posit.co/software/yardstick/logo_hu_e59943c1b04b29e0.png"
          srcset="/software/yardstick/logo_hu_e59943c1b04b29e0.png 1x,
                  /software/yardstick/logo_hu_866c2a6b59c395dc.png 2x"
          alt="yardstick"
          class="mx-auto h-full object-contain px-[20%] py-[5%]"
          loading="lazy" />
      </picture>
    
      
      

      
      
        
      
      
        
      

      
      
      
      

      <picture class="hidden @tall:@max-grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/software/yardstick/logo_hu_1a82d12a9e8dfc22.webp 1x,
                  /software/yardstick/logo_hu_1a82d12a9e8dfc22.webp 2x" />
        <img
          src="https://opensource.posit.co/software/yardstick/logo_hu_866c2a6b59c395dc.png"
          srcset="/software/yardstick/logo_hu_866c2a6b59c395dc.png 1x,
                  /software/yardstick/logo_hu_866c2a6b59c395dc.png 2x"
          alt="yardstick"
          class="mx-auto h-full object-contain px-[20%] py-[5%]"
          loading="lazy" />
      </picture>
    
      
      

      
      
        
      
      
        
      

      
      
      
      

      <picture class="hidden @grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/software/yardstick/logo_hu_1a82d12a9e8dfc22.webp 1x,
                  /software/yardstick/logo_hu_1a82d12a9e8dfc22.webp 2x" />
        <img
          src="https://opensource.posit.co/software/yardstick/logo_hu_866c2a6b59c395dc.png"
          srcset="/software/yardstick/logo_hu_866c2a6b59c395dc.png 1x,
                  /software/yardstick/logo_hu_866c2a6b59c395dc.png 2x"
          alt="yardstick"
          class="mx-auto h-full object-contain px-[20%] py-[5%]"
          loading="lazy" />
      </picture>
    
  
  


      
    </div>

    
    <div class="hidden gap-y-2 @nip:flex @venti:w-1/2 flex-auto flex-col p-3 @venti:pl-5 text-gray-700 mt-2">

      
      

      
      

      
      <h3 class="m-0! mt-3 font-semibold text-sm @grande:text-lg @venti:text-xl @trenta:text-2xl line-clamp-2 text-sm @tall:text-base @grande:text-lg @venti:text-xl @trenta:text-2xl text-gray-600">Measure model performance with yardstick</h3>
      

      
        <p class="m-0! text-xs @short:text-sm @grande:text-md @venti:text-md @trenta:text-lg line-clamp-2 short@line-clamp-3 @tall:line-clamp-3 font-medium text-gray-600">Quick reference guide for measuring how well a model predicts with yardstick</p>
      

      
        <div class="text-sm @grande:text-md">
          <div class="mt-2 flex flex-row gap-x-4 items-center"><div class="flex flex-row flex-shrink-0"><img 
          src="https://opensource.posit.co/people/edgar-ruiz/profile.jpg" 
          alt="Edgar Ruiz" 
          class="my-0! w-6 h-6 rounded-full object-cover ring-2 ring-white "
          style="z-index: 10;"
        ></div><div class="line-clamp-2 font-medium text-gray-600">Edgar Ruiz</div></div>
        </div>
      

      <div class="grow"></div>
      
        
          <div class="hidden @grande:flex flex-wrap gap-x-1 gap-y-4 py-2 overflow-hidden h-10 items-end">
            
              <span class="pill">
                yardstick
              </span>
            
          </div>
        
      
    </div>
  </a>
</div>


    </div>
  
</div>


<h2 id="positconf2026-talk-sneak-peeks">posit::conf(2026) talk sneak peeks
</h2>
<p>With over 100 talks, it’s impossible to feature all of them in this newsletter (but you can explore the complete lineup on the <a href="https://conf.posit.co/2026/sessions/" target="_blank" rel="noopener">event schedule</a>!). I wanted to give a quick preview of topics you can look forward to (whether you tune in live during the conference or catch up on demand).</p>
<ol>
<li>
<h3 id="positron-in-prime-time">Positron in prime time
</h3>
</li>
</ol>
<p>It’s been about two years since the public release of Positron, and 🎵this IDE is on fire 🎵</p>
<p>The lineup of conf talks proves it. From Quarto notebooks in Positron (did you know <a href="https://positron.posit.co/quarto-inline-output.html" target="_blank" rel="noopener">inline output is available</a>?!), to using Positron in education, to how it treats SQL as a first-class language, to delightful workflows in Python and R, come see what people are cooking up with Positron.</p>
<ol start="2">
<li>
<h3 id="what-cant-you-do-with-quarto">What can’t you do with Quarto?
</h3>
</li>
</ol>
<p>The best way of discovering what Quarto can do is seeing how others are using it!</p>
<p>We’re starting off strong, with the first session of Virtual Day (September 14th) being “Quarto, R + Python”, where Björn Fisseler will discuss creating accessible reports and Wasim Lorgat sharing the history of notebooks. The Quarto theme continues on the 15th and 16th with talks on designing influential and scalable reports, creating learning ecosystems, teaching bilingual data science, developing wiki chatbots, and developing unreasonably effective dashboards and slides — all in Quarto.</p>
<ol start="3">
<li>
<h3 id="ill-definitely-be-joining">I’ll definitely be joining…
</h3>
<ul>
<li><strong>Positron and the Three Bears: Teaching Python with Positron</strong> (presented by Marc Dotson) - Practical lessons from an educator navigating Python instruction and finding the sweet spot with Positron</li>
<li><strong>First Impressions Matter: Styling Data Products That People Actually Want to Use</strong> (presented by Shelby Level) - Shelby creates visually striking data products, and I can’t wait to pick up some design inspiration and techniques from her session</li>
<li><strong>A Grammar of Graphics for SQL</strong> (presented by Thomas Lin Pedersen) - We’ll hear the developer of ggsql share the story of bringing ggplot2-style visualization concepts to SQL</li>
<li><strong>Outgrowing Spreadsheets: Rebuilding Research Data Collection with Shiny and Posit Connect</strong> (presented by Kelsey Chalmers) - Kelsey shares the before-and-after architecture and how their research team transformed a fragile, spreadsheet-based data collection process into a sustainable system using Shiny and Posit Connect</li>
</ul>
</li>
</ol>
<p>Of course, this is just the tip of the conf iceberg. One of my favorite things about conf is the diversity of talks, from real-world data science implementation, to insights from the field, to creative and whimsical experiments. I hope to <a href="https://conf.posit.co/2026/registration/" target="_blank" rel="noopener">see you there</a>!</p>
<p>If you can’t wait til conf (or if you’re reading after conf is over!), there are other events that you can join us at:</p>
<ul>
<li><a href="https://pos.it/dslab" target="_blank" rel="noopener">Data Science Lab</a>, every Tuesday at 12pm ET</li>
<li><a href="https://pos.it/dsh" target="_blank" rel="noopener">Data Science Hangout</a>, every Thursday at 12pm ET</li>
<li><a href="https://events.zoom.us/ev/Ajss5j9VeRMe0zw-AtFKf7AUAsthzYhaYjYPeEIu1uYAQe1K0ud1~Agb_hSt1UGxd9fUyIDC32e6jAdEtbl7G_AaLZUYhRvyZR5cGpY5Kp_A0-w?mkt_tok=NzA5LU5YTi03MDYAAAGhT3GTQTQ5s7LDxqZGUAE1zALOLmEDysbAsOyvVdJ9P72DB_IRwtWRTED6kyJA-j-YKF7WxCTY_ZlZ4rTkiyY" target="_blank" rel="noopener">Building Repeatable AI Workflows with Custom MCP Servers</a>, September 30</li>
<li><a href="https://opensource.posit.co/events/" target="_blank" rel="noopener">Our next conference appearance</a></li>
</ul>
<p>Have a lovely September, and reach out anytime! isabella [dot] velasquez [at] posit [dot] co</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-09-02_glimpse-2026-09/thumbnail.jpg" length="68574" type="image/jpeg" />
    </item>
    <item>
      <title>Python Polars: The Definitive Cheatsheet</title>
      <link>https://opensource.posit.co/blog/2026-08-18_polars-cheatsheet-announcement/</link>
      <pubDate>Tue, 18 Aug 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-08-18_polars-cheatsheet-announcement/</guid>
      <dc:creator>Jeroen Janssens</dc:creator>
      <dc:creator>Thijs Nieuwdorp</dc:creator><description><![CDATA[<p>Our book, <em><a href="https://polarsguide.com" target="_blank" rel="noopener">Python Polars: The Definitive Guide</a></em>, is nearly 500 pages. Over the past few weeks we spent many hours compressing it down to two.</p>
<p><em>Python Polars: The Definitive Cheatsheet</em> is out now, and it&rsquo;s free. Download it, print it, laminate it, put it on your desk.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-08-18_polars-cheatsheet-announcement/polars-cheatsheet-side-by-side.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p><a href="https://opensource.posit.co/resources/cheatsheets/polars">Get the cheatsheet</a></p>
<h2 id="whats-on-it">What&rsquo;s on it
</h2>
<p>Page one covers the fundamentals: how to get started, data types, data structures, the eager and lazy APIs, reading and writing data, and transforming data with select, filter, sort, reshape, aggregate, and join.</p>
<p>Page two is mostly expressions, which is where Polars really shines. It also includes the most common methods for working with strings, datetimes, lists, structs, and categoricals, plus styling data with Great Tables and visualizing data with Altair and Plotnine.</p>
<p>Keep in mind that this is a lossy compression. Two pages can cover many functions and methods, but there&rsquo;s no room for nuance or to explain the fundamental concepts. The cheatsheet is for remembering and referencing, not for learning Polars from scratch, and it doesn&rsquo;t replace the <a href="https://docs.pola.rs/" target="_blank" rel="noopener">official documentation</a> or our book.</p>
<h2 id="theres-an-html-version-too">There&rsquo;s an HTML version too
</h2>
<p>A laminated PDF is a good desk reference, but it isn&rsquo;t much use to a screen reader, and you can&rsquo;t copy anything out of it. So the cheatsheet also comes as HTML, with headings, tables, and selectable code. It works with a screen reader, the text reflows on a phone, and you can copy a snippet straight into your editor instead of retyping it.</p>
<h2 id="made-together-with-polars-inc">Made together with Polars, Inc.
</h2>
<p>A cheatsheet is really a long series of arguments about what matters most. Every line that makes it displaces something else. Having <a href="https://pola.rs" target="_blank" rel="noopener">Polars, Inc.</a> involved meant the people who build the library had a say in what a two-page reference should cover, and the cheatsheet is considerably better for it.</p>
<p>Posit and Polars, Inc. are different companies, but our tools overlap. Great Tables and Plotnine are Posit projects, and both work directly with Polars DataFrames. That&rsquo;s why they&rsquo;re on the sheet, and it&rsquo;s part of why this collaboration made sense.</p>
<p>We hope you find it useful. If there&rsquo;s a method you think we wasted space on, or one we should have included, we&rsquo;d like to hear about it.</p>
<p><a href="https://opensource.posit.co/resources/cheatsheets/polars">Get the cheatsheet</a></p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-08-18_polars-cheatsheet-announcement/polars-cheatsheet-announcement.png" length="2922554" type="image/png" />
    </item>
    <item>
      <title>Neural networks in Orbital for Python 0.6.0: PyTorch straight to your database</title>
      <link>https://opensource.posit.co/blog/2026-08-17_pyorbital-0-6-0/</link>
      <pubDate>Mon, 17 Aug 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-08-17_pyorbital-0-6-0/</guid>
      <dc:creator>Alessandro Molina</dc:creator><description><![CDATA[<p>Over the past couple of months I&rsquo;ve been teaching Orbital to speak PyTorch.</p>
<p>Orbital&rsquo;s whole pitch is that a trained model becomes SQL, so a database can run predictions on its own, with no Python process anywhere near it. Until 0.6.0, &ldquo;trained model&rdquo; meant scikit-learn: pipelines, trees, linear models, all <code>.fit()</code> in Python and then turned into a <code>SELECT</code> statement. It never covered what a lot of teams are actually training now: PyTorch models, not scikit-learn pipelines.</p>
<p>Orbital 0.6.0 closes that gap. A <code>torch.nn.Sequential</code> network, trained exactly the way you already train it, now compiles to the same kind of SQL a linear regression would.</p>
<p>No ONNX Runtime. No model server. No separate inference service to keep alive next to the database. Just a query.</p>
<p>Why does that work at all, for a framework Orbital was never written for? Because of a decision made long before PyTorch was ever on the table.</p>
<h2 id="why-this-isnt-a-bolt-on">Why this isn&rsquo;t a bolt-on
</h2>
<p>Orbital was never really a scikit-learn tool. Underneath, it converts a scikit-learn pipeline to ONNX (Open Neural Network Exchange, a standard graph format for trained models) using the <code>skl2onnx</code> library, then walks that graph node by node to produce SQL. Scikit-learn was always one hop removed from what Orbital actually translates.</p>
<p>Adding PyTorch meant taking the same hop from a different starting point. <code>torch.onnx.export</code> turns a <code>torch.nn.Sequential</code> model into that same kind of graph. Feed it into the translator that already existed, and the translator doesn&rsquo;t know or care whether the graph came from PyTorch or scikit-learn.</p>
<p>The proof is in how little new code that took. The entire engine for running a feed-forward network in SQL is three small classes: a <code>Gemm</code> translator for <code>Linear</code> layers, <code>ReLU</code>, <code>Sigmoid</code>. Everything else (the translator base class, the optimizer, the per-dialect SQL compiler) already existed, built earlier for trees and linear models.</p>
<p>When I first thought of support for PyTorch, I put it this way: <em>&ldquo;the underlying translation works on ONNX graphs&hellip; the same value proposition orbital already provides for scikit-learn models can apply as it is to pytorch networks exported to ONNX.&rdquo;</em> That sentence turned out to be the whole implementation plan.</p>
<p>If you want the fuller picture of how a graph becomes SQL, parser, then translator, then optimizer, the <a href="https://posit-dev.github.io/orbital/learnmore/" target="_blank" rel="noopener">architecture docs</a> walk through all three stages in order.</p>
<p>That architecture is also why I keep calling this <strong>multiple frameworks</strong>, not two frameworks. <code>Relu</code>, <code>Sigmoid</code>, <code>Tanh</code>, and <code>Softmax</code> are single translators, not one per framework. Scikit-learn&rsquo;s <code>MLPClassifier</code> and <code>MLPRegressor</code> reach them through <code>MatMul</code> and <code>Add</code>, PyTorch&rsquo;s <code>nn.Sequential</code> reaches the exact same translators through <code>Gemm</code>. One implementation, two entry points, and no reason it has to stop at two.</p>
<p>Scikit-learn and PyTorch are both real and shipping today, which is enough on its own to call this &ldquo;multiple frameworks.&rdquo; But the dependency story is already moving that direction: <a href="https://github.com/posit-dev/orbital/issues/113" target="_blank" rel="noopener">issue #113</a> proposes turning scikit-learn itself into an optional dependency, the same way PyTorch already is, so the core stops assuming any particular framework at all.</p>
<h2 id="making-it-actually-usable">Making it actually usable
</h2>
<p>Neural networks are layered, and every neuron in layer two reads every output of layer one. If Orbital just inlines those outputs at each place they&rsquo;re read, instead of naming them once, that repetition compounds from one layer to the next. Two layers doubles the inlined text. Five layers is a different order of magnitude.</p>
<p>That&rsquo;s not theoretical: <a href="https://github.com/posit-dev/orbital/issues/115" target="_blank" rel="noopener">the issue that tracked the fix</a> measured scikit-learn&rsquo;s own default <code>MLPClassifier(hidden_layer_sizes=(100,))</code>, the first thing anyone reaches for, at 53MB of generated SQL and roughly 894 seconds just to generate it. A hundred neurons in one hidden layer, and the query was already unusable.</p>
<p>PyTorch&rsquo;s <code>Gemm</code> translator never had this problem. It has called <code>preserve()</code>, materializing its output as a real SQL column, since the day it was written. Scikit-learn&rsquo;s <code>MLPClassifier</code> and <code>MLPRegressor</code> compile through different ONNX ops though: <code>MatMul</code> then <code>Add</code>, because that&rsquo;s what <code>skl2onnx</code> emits, not <code>Gemm</code>. Neither of those translators called <code>preserve()</code> at all.</p>
<p>The fix, <code>Optimizer.preserve_referenced_outputs()</code>, runs after every single node in the translation loop, for every translator, not just <code>MatMul</code> and <code>Add</code>, and checks how many times that node&rsquo;s output is actually referenced downstream. Referenced more than once, it gets materialized as a named column. Referenced once or not at all, it stays inlined, no extra column, no extra noise.</p>
<p>None of this is new machinery either. Tree ensembles already lean on the same trick: <code>preserve()</code> materializes per-tree votes, or the whole ensemble&rsquo;s aggregated vote so it isn&rsquo;t re-emitted everywhere it&rsquo;s read, as real SQL columns. <code>preserve_referenced_outputs()</code> generalizes that same idea automatically, for every translator, whether it&rsquo;s part of an ordinary pipeline or a neural network.</p>
<p>Here&rsquo;s what that fix was worth, measured on three shapes while it was being built:</p>
<table>
  <thead>
      <tr>
          <th>Network</th>
          <th>Before</th>
          <th>After</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>20→64→64→1</code> (Orbital&rsquo;s own deep-network scaling test)</td>
          <td>12.4MB, ~185s to generate</td>
          <td>234KB</td>
      </tr>
      <tr>
          <td><code>MLPClassifier(hidden_layer_sizes=(100,))</code>, 3-class (scikit-learn&rsquo;s own default)</td>
          <td>53MB, ~894s to generate</td>
          <td>46.7KB</td>
      </tr>
      <tr>
          <td><code>MLPRegressor(hidden_layer_sizes=(32, 32))</code></td>
          <td>1.75MB, ~21s to generate</td>
          <td>57.6KB</td>
      </tr>
  </tbody>
</table>
<p>Generation time collapsed just as hard: the <code>20→64→64→1</code> network above went from about 185 seconds to about 3.6 seconds. Running the resulting SQL got faster too, if less dramatically: on 200,000 rows, an <code>MLP(32,32)</code> query dropped from 0.43s to 0.35s, and an <code>MLP(100,100)</code> from 3.58s to 3.31s.</p>
<p>Same hyperparameters. Same defaults everyone actually reaches for. The difference is entirely in how the SQL gets built, not in what the network computes.</p>
<h2 id="what-it-can-do-today">What it can do today
</h2>
<p>Neural network support in 0.6.0 covers binary classification, multiclass classification, and regression, for both scikit-learn and PyTorch. Five new or updated examples in the repo prove it out: <code>pytorch_fraud_detector.py</code>, <code>pytorch_maintenance_classifier.py</code>, <code>pytorch_demand_regressor.py</code>, <code>pipeline_mlp_classifier.py</code>, <code>pipeline_mlp_regressor.py</code>.</p>
<p>The one worth walking through is the fraud detector, since it&rsquo;s the shape most teams actually need: a handful of numeric features, a binary &ldquo;is this fraud&rdquo; output, trained the same way this kind of model always is.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">FEATURES</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;amount&#34;</span><span class="p">:</span> <span class="n">orbital</span><span class="o">.</span><span class="n">types</span><span class="o">.</span><span class="n">DoubleColumnType</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;hour&#34;</span><span class="p">:</span> <span class="n">orbital</span><span class="o">.</span><span class="n">types</span><span class="o">.</span><span class="n">DoubleColumnType</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;v1&#34;</span><span class="p">:</span> <span class="n">orbital</span><span class="o">.</span><span class="n">types</span><span class="o">.</span><span class="n">DoubleColumnType</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;v2&#34;</span><span class="p">:</span> <span class="n">orbital</span><span class="o">.</span><span class="n">types</span><span class="o">.</span><span class="n">DoubleColumnType</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">model</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">Sequential</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">Linear</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">FEATURES</span><span class="p">),</span> <span class="mi">16</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">ReLU</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">Linear</span><span class="p">(</span><span class="mi">16</span><span class="p">,</span> <span class="mi">8</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">ReLU</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">Linear</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">Sigmoid</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ... train model normally: Adam, BCELoss, a plain training loop ...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">orbital_pipeline</span> <span class="o">=</span> <span class="n">orbital</span><span class="o">.</span><span class="n">parse_pytorch_model</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">FEATURES</span><span class="p">)</span></span></span></code></pre></div></div>
<p>From that one <code>orbital_pipeline</code>, two engines:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">duckdb_sql</span> <span class="o">=</span> <span class="n">orbital</span><span class="o">.</span><span class="n">export_sql</span><span class="p">(</span><span class="s2">&#34;transactions&#34;</span><span class="p">,</span> <span class="n">orbital_pipeline</span><span class="p">,</span> <span class="n">dialect</span><span class="o">=</span><span class="s2">&#34;duckdb&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">postgres_sql</span> <span class="o">=</span> <span class="n">orbital</span><span class="o">.</span><span class="n">export_sql</span><span class="p">(</span><span class="s2">&#34;transactions&#34;</span><span class="p">,</span> <span class="n">orbital_pipeline</span><span class="p">,</span> <span class="n">dialect</span><span class="o">=</span><span class="s2">&#34;postgres&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>I ran both, against a real DuckDB and a real Postgres, not just read the generated text. Same four test transactions, three ways to compute a prediction (PyTorch itself, the DuckDB query, the Postgres query), and all three agree to within 1e-5. Both queries land around 10KB, not the tens of megabytes a naive translation would have produced before 0.6.0&rsquo;s optimizer fix.</p>
<p>That works because <code>Sigmoid</code> and <code>ReLU</code> both compile to plain arithmetic: <code>EXP</code>, a division, a <code>CASE WHEN</code>. Every SQL engine has those. It&rsquo;s not an accident which activation this example uses, either: <code>Tanh</code> compiles to a native <code>TANH()</code> call instead, and not every dialect implements that the same way, so it&rsquo;s the one activation in Orbital&rsquo;s NN support with an actual portability caveat attached.</p>
<p>DuckDB and Postgres are two of the three dialects <a href="https://posit-dev.github.io/orbital/learnmore/" target="_blank" rel="noopener">Orbital actively tests in CI</a>. SQLite is the third. But that&rsquo;s a testing choice, not an architecture boundary: Orbital doesn&rsquo;t write dialect-specific SQL at all. Translation ends at ibis. <code>export_sql</code> just hands the finished expression to whichever of ibis&rsquo;s own backend compilers matches the dialect you ask for, and ibis ships about twenty of those: Snowflake, BigQuery, Trino, MySQL, and so on.</p>
<h2 id="limits-honestly">Limits, honestly
</h2>
<p>What Orbital 0.6.0 handles is feed-forward, fully connected networks: stacks of <code>Linear</code> layers with <code>ReLU</code>, <code>Sigmoid</code>, <code>Tanh</code>, or <code>Softmax</code> in between. That already covers real use cases people put into production: fraud scoring, churn, demand forecasting, risk models. None of those need a CNN or a transformer.</p>
<p>What it doesn&rsquo;t do yet is exactly what that shape excludes: no convolutions, no recurrence, no attention, no embedding layers for categorical features. If your model needs any of those, Orbital isn&rsquo;t there yet.</p>
<p>SQL size still grows with the network. A few hidden layers of 64 to 128 neurons land comfortably in the KB range. Wider or deeper than that, it&rsquo;s worth checking the generated SQL against whatever statement-size limit your engine has, before you deploy it.</p>
<p>That headroom exists at all thanks to <code>Optimizer.preserve_referenced_outputs()</code>, and that mechanism helps every translator in Orbital, not just neural networks. It&rsquo;s a big enough story on its own that I&rsquo;ll get back to in a future post.</p>
<h2 id="try-it">Try it
</h2>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install orbital<span class="o">[</span>pytorch<span class="o">]</span></span></span></code></pre></div></div>
<p>From there, the <a href="https://posit-dev.github.io/orbital/getstarted/" target="_blank" rel="noopener">getting-started guide</a> walks through this same fraud-detector shape end to end, and the <a href="https://github.com/posit-dev/orbital/tree/main/examples" target="_blank" rel="noopener">examples directory</a> has five more, covering classification and regression in both frameworks.</p>
<p>Orbital speaks PyTorch now. Scikit-learn too. Same query either way.</p>
<p>There are more frameworks already in the works. I won&rsquo;t name them here, only that the architecture was built for exactly this.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-08-17_pyorbital-0-6-0/featured.png" length="1070756" type="image/png" />
    </item>
    <item>
      <title>Positron August Release Highlights</title>
      <link>https://opensource.posit.co/blog/2026-08-13_positron-2026-08-release/</link>
      <pubDate>Thu, 13 Aug 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-08-13_positron-2026-08-release/</guid>
      <dc:creator>Julia Silge</dc:creator><description><![CDATA[<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Note</span>
</div>
<div class="callout-body">
<p><a href="https://positron.posit.co" target="_blank" rel="noopener">Positron</a> is Posit&rsquo;s new, next-generation IDE for data science. Positron is designed to be an extensible, polyglot tool for exploring data and reproducible authoring in Python, R, and more.</p>
</div>
</div>
<p>Welcome back to another edition of our monthly Positron updates! Each month we share highlights from our <a href="https://positron.posit.co/release-notes" target="_blank" rel="noopener">latest release</a> and useful resources. <a href="https://opensource.posit.co/blog/2026-07-13_positron-2026-07-release">Last release</a> we told you about several major features that came out of preview to general availability, including the <a href="https://opensource.posit.co/blog/2026-07-29_positron-jupyter-notebook-editor-ga">new notebook editor</a>. This milestone we are excited to share new functionality for SQL support, reproducible authoring, helping you know when packages are missing, and more.</p>
<h2 id="data-connections">Data Connections
</h2>
<p>Data Connections is our new way to work with SQL and database-like resources in Positron, from local files and database servers to cloud data warehouses. It is currently available as a preview feature, and you can enable it with the <a href="positron://settings/dataConnections.enabled"><code>dataConnections.enabled</code></a> setting. This release more than doubles the number of data sources you can reach. Amazon Redshift, Snowflake, Databricks, and Posit Connect pins join the existing DuckDB, PostgreSQL, and SQLite support.</p>
<img src="https://opensource.posit.co/blog/2026-08-13_positron-2026-08-release/data-connections.gif" data-fig-align="center" data-fig-alt="Browsing the schemas and tables of a DuckDB connection in the Data Connections panel, then opening a table in the Data Explorer to see its column profiles and data." />
<p>The panel itself is more capable as well. <strong>Refresh</strong> and <strong>Refresh All</strong> reload the tree while preserving what you have expanded, briefly highlighting the rows that were reloaded. Open connections now show an indicator. Collapsing a connection in the UI keeps you connected to your data source, while anything you&rsquo;ve previewed with the Data Explorer stays still open. When you remove a connection, Positron asks for confirmation and reports how many Data Explorers will close with it.</p>
<p>Data Connections is still an experimental preview, and your feedback continues to shape it. Tell us which databases and warehouses you need, and anything confusing, missing, or broken, in the <a href="https://github.com/posit-dev/positron/discussions/14571" target="_blank" rel="noopener">Data Connections discussion</a>.</p>
<h2 id="inline-output-for-quarto">Inline output for Quarto
</h2>
<p><a href="https://positron.posit.co/quarto-inline-output" target="_blank" rel="noopener">Inline output for <code>.qmd</code> documents</a> came out of preview last release, and this release brings you a substantial round of polish for this way of working. Be aware that the Quarto settings have moved into a dedicated <code>quarto.*</code> namespace with its own group in the Settings editor. The previous <code>positron.quarto.*</code> keys still work but are deprecated, and Positron will prompt you to update your settings.</p>
<p>Before a kernel starts, the kernel status names the interpreter it will start and offers an explicit <strong>Start Kernel</strong> action. When a cell fails, <strong>Fix</strong> and <strong>Explain</strong> buttons send the error to <a href="https://assistant.posit.co/" target="_blank" rel="noopener">Posit Assistant</a>, matching the Positron notebook experience. The editor also scrolls to reveal output as it is produced, which you can turn off with the new <a href="positron://settings/quarto.inlineOutput.autoScroll"><code>quarto.inlineOutput.autoScroll</code></a> setting.</p>
<p>Output renders more faithfully as well. The editor gutter now shows which statement is currently executing and per-statement progress. Positron draws images at your display&rsquo;s pixel ratio, so plots are sharp on retina screens, and Python figures now respect the <code>fig-width</code> and <code>fig-height</code> cell options.</p>
<img src="https://opensource.posit.co/blog/2026-08-13_positron-2026-08-release/inline-output-plot-metadata.png" data-fig-align="center" data-fig-alt="A Quarto document open in Positron with a Python cell that sets the fig-width and fig-height options, and the resulting matplotlib scatter plot rendered inline below the cell at that size." />
<p>HTML widgets no longer stick in the editor corner when you scroll past them, or trap scrolling instead of letting the document scroll. HTML widgets no longer render as raw HTML after a reload, and collapsed output no longer springs back open when its cell re-runs. Running code in a Quarto document also pins the editor tab now, so Positron does not silently close the document and its session when you open another file.</p>
<h2 id="ai-model-providers">AI model providers
</h2>
<p>Positron now reads AI model provider configuration from a single <code>providers.json</code> file rather than a scattered set of settings. The new release will migrate your existing configuration automatically when you start it, and deprecates the <code>authentication.*</code> and <code>positron.assistant.provider.*.enable</code> settings in favor of it. Two new commands give you direct access: <em>Open AI Provider Settings (JSON)</em> opens <code>providers.json</code> from the Command Palette, and <em>Migrate Provider Settings to providers.json</em> runs the migration on demand.</p>
<h2 id="install-missing-packages">Install missing packages
</h2>
<p>Positron now notices when your code refers to a package you do not have installed and offers to install it for you. The prompt appears for packages referenced by your scripts and notebooks in both R and Python.</p>
<img src="https://opensource.posit.co/blog/2026-08-13_positron-2026-08-release/missing-package.gif" data-fig-align="center" data-fig-alt="A Shiny app in Positron showing a missing package button in the editor toolbar. Clicking it installs bslib in the console, and the app then runs with its bubble chart in the Viewer pane." />
<p>A <code>library()</code> or <code>import</code> call for something missing becomes a single click instead of an error you have to go resolve by hand.</p>
<h2 id="performance-and-memory">Performance and memory
</h2>
<p>We continue to invest in the memory footprint, performance, and reliability of Positron. Several components now load only when they are actually needed, and turning off <a href="positron://settings/ai.enabled"><code>ai.enabled</code></a> now means Positron never loads some heavy AI-related components at all. We fixed a cluster of long-standing reliability problems around session restarts and lifecycles.</p>
<p>Startup and editing are faster as well. R and Python kernels start much faster on Windows systems with aggressive antivirus software. The interpreter session picker appears immediately instead of waiting for interpreter discovery to finish. We also fixed slow typing, formatting, and saving in long R and Python files.</p>
<h2 id="whats-coming-next">What&rsquo;s coming next
</h2>
<ul>
<li>Join <a href="https://posit.co/workflow-demo/ai-governance-workbench" target="_blank" rel="noopener">our upcoming webinar</a> on August 26 to learn about AI governance in Posit Workbench.</li>
<li>We are looking forward to posit::conf(2026) next month, where our team will have several sessions on Positron. <a href="https://conf.posit.co/2026/" target="_blank" rel="noopener">Register now</a> to join us in person in Houston or virtually from anywhere in the world.</li>
</ul>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Tip</span>
</div>
<div class="callout-body">
<p><a href="https://positron.posit.co/download" target="_blank" rel="noopener">Download Positron</a> to try out the new features and improvements in this release!</p>
</div>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-08-13_positron-2026-08-release/featured.svg" length="91423" type="image/svg&#43;xml" />
    </item>
    <item>
      <title>posit::glimpse() Newsletter – August 2026</title>
      <link>https://opensource.posit.co/blog/2026-08-05_glimpse-aug-2026/</link>
      <pubDate>Wed, 05 Aug 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-08-05_glimpse-aug-2026/</guid>
      <dc:creator>Isabella Velásquez</dc:creator><description><![CDATA[<blockquote>
<p>Welcome to our newsletter, posit::glimpse()!</p>
<p>If you&rsquo;re currently reading this on our blog, consider subscribing to Product Updates - Open Source on our <a href="https://posit.co/about/subscription-management" target="_blank" rel="noopener">subscription page</a> to receive this newsletter directly in your inbox.</p>
</blockquote>
<p>Every month, we round up some of the most important open-source news for Posit&rsquo;s community! We hope that you enjoy this newsletter and maybe even share it with a friend.</p>
<p>The table of contents on the right can help you navigate through all the updates. As you scroll, it will open up to show you subcategories →</p>
<h2 id="announcements">Announcements
</h2>
<h3 id="register-for-positconf2026">Register for posit::conf(2026)
</h3>
<p>Register for our annual conference, <a href="https://posit.co/conference/" target="_blank" rel="noopener">posit::conf(2026)</a>, happening September 14-16 in Houston and online! The amazing <a href="https://posit.co/blog/posit-conf-2026-agenda-breakdown" target="_blank" rel="noopener">speakers</a> and <a href="https://posit.co/blog/workshops-at-positconf2026" target="_blank" rel="noopener">workshops</a> will make for a wonderful time. <a href="https://conf.posit.co/2026/registration/" target="_blank" rel="noopener">Register for posit::conf here</a>.</p>
<p>For the first time, the live streams will feature automatically translated subtitles, available in French, German, Italian, Portuguese, and Spanish.</p>
<p>Tidy Dev Day is on September 17, a unique opportunity to collaboratively tackle open-source issues and work directly alongside the very developers who build and maintain the tools you use every day. <a href="https://opensource.posit.co/blog/2026-06-25_tidy-dev-day-2026/" target="_blank" rel="noopener">Learn more about Tidy Dev Day here</a>.</p>
<h3 id="migrating-to-connect-cloud">Migrating to Connect Cloud
</h3>
<p>A thank-you to everyone who published on rpubs.com, quartopub.com, shinyapps.io, and bookdown.org. We&rsquo;re consolidating all our publishing tools into one: <a href="https://connect.posit.cloud/" target="_blank" rel="noopener">Posit Connect Cloud</a>. Connect Cloud is built to handle all of the content types in one place, with one account, and we hope to make your transition as smooth as possible.</p>
<ul>
<li>Learn more in the <a href="https://posit.co/blog/migrating-connect-cloud-posits-unified-publishing-solution" target="_blank" rel="noopener">Migrating to Connect Cloud: Posit&rsquo;s unified publishing solution blog post</a>.</li>
<li>Join our <a href="https://posit.co/webinar/connect-cloud-migration" target="_blank" rel="noopener">webinar on the migration process happening September 9th</a>.</li>
</ul>
<h2 id="key-product-updates-and-new-releases">Key product updates and new releases
</h2>
<h3 id="httr2-130">httr2 1.3.0
</h3>
<p><a href="https://httr2.r-lib.org/" target="_blank" rel="noopener">httr2</a> is a comprehensive HTTP client for R for working with web APIs. httr2 1.3.0 introduces a breaking change to OAuth token caching that requires one-time re-authentication. Recent patches added 200x faster streaming for short-line responses, OAuth server metadata discovery, OpenTelemetry tracing support, and <code>httr2_translate()</code> for converting requests to curl commands.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-07-14_httr2-1-3-0/" target="_blank" rel="noopener">httr2 1.3.0 blog post</a>.</li>
</ul>
<h3 id="ir-010">ir 0.1.0
</h3>
<p>Introducing <a href="https://r-lib.github.io/ir/" target="_blank" rel="noopener">ir</a> 0.1.0, a command-line tool for running self-describing R scripts and Quarto documents with embedded package dependencies and R version requirements, inspired by Python&rsquo;s PEP 723 and <code>uv run --script</code>. The tool supports isolated environments, CRAN snapshot dates, mixed R/Python workflows, and caches resolved dependencies for efficiency, enabling one-file workflows without full project structures.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-07-23_ir-0-1-0/" target="_blank" rel="noopener">ir 0.1.0 blog post</a>.</li>
</ul>
<h3 id="lorax">lorax
</h3>
<p><a href="https://lorax.tidymodels.org/" target="_blank" rel="noopener">lorax</a> introduces a unified interface for characterizing tree- and rule-based models across 12 implementations including ranger, XGBoost, LightGBM, and random forests. The package enables extracting decision rules, identifying active predictors, converting trees to partykit format for visualization, and accessing variable importance scores through consistent APIs that work across diverse tree model packages.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-07-28_lorax/" target="_blank" rel="noopener">Introducing lorax: Speaking for the Tree-Based Models blog post</a>.</li>
</ul>
<h3 id="mcptools-100">mcptools 1.0.0
</h3>
<p><a href="https://posit-dev.github.io/mcptools/" target="_blank" rel="noopener">mcptools</a>, an R SDK for the Model Context Protocol, is now on CRAN. It brings image support for rich content in both directions, native authenticated remote server connections eliminating the npm dependency, and streamlined <a href="https://posit.co/products/enterprise/connect" target="_blank" rel="noopener">Posit Connect</a> deployment via <code>_server.yml</code>. This R SDK for the Model Context Protocol enables deploying R functions as MCP servers and fetching third-party MCP tools as R functions.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-07-06_mcptools-1-0-0/" target="_blank" rel="noopener">mcptools 1.0.0 blog post</a>.</li>
</ul>
<h3 id="quarto-110">Quarto 1.10
</h3>
<p><a href="https://quarto.org/" target="_blank" rel="noopener">Quarto</a> transforms Markdown, code, and computational output into publication-ready articles, reports, presentations, websites, and books. Version 1.10 introduces offline HTML accessibility checking with bundled axe-core and WCAG conformance level targeting, plus localized string support for template authors. The release includes important fixes for <code>quarto preview</code> reliability, shortcode resolution in math expressions, and updated dependencies including pandoc 3.10 and typst 0.15.1.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-08-03_quarto-1-10/" target="_blank" rel="noopener">Quarto 1.10 blog post</a>.</li>
</ul>
<h3 id="raghilda-v02">raghilda v0.2
</h3>
<p><a href="https://posit-dev.github.io/raghilda/" target="_blank" rel="noopener">Raghilda</a> is a Python package for building RAG (Retrieval-Augmented Generation) solutions. This release focuses on production-ready features for scaling RAG pipelines, with crawl and ingest API with caching and concurrency, a CloudflareCrawler for JavaScript-rendered sites, a PostgreSQL store backend, and NVIDIA NIM embeddings.</p>
<ul>
<li>Check out the <a href="https://opensource.posit.co/blog/2026-07-01_raghilda-0-2-0/" target="_blank" rel="noopener">raghilda v0.2 blog post</a>.</li>
</ul>
<h3 id="roxygen2-810">roxygen2 8.1.0
</h3>
<p><a href="https://roxygen2.r-lib.org/" target="_blank" rel="noopener">roxygen2</a> 8.1.0 improves performance with consolidated <code>importFrom()</code> directives that reduce package loading time from ~120ms to ~9ms for 1,000 imports, and introduces the rdtools package for faster cross-reference resolution during documentation generation. The release also adds multi-line support for <code>@importFrom</code> directives with hanging indents.</p>
<ul>
<li>Check out the <a href="https://opensource.posit.co/blog/2026-08-04_roxygen2-8-1-0/" target="_blank" rel="noopener">roxygen2 8.1.0 blog post</a>.</li>
</ul>
<h3 id="shiny-updates">Shiny updates
</h3>
<p><a href="https://shiny.posit.co/" target="_blank" rel="noopener">Shiny</a> is the framework for building interactive web applications in R and Python. Shiny for R 1.14 adds <code>startApp()</code> for non-blocking app launches and <code>session$destroy()</code> for proper module cleanup. Shiny for Python 1.7 includes bundled Agent Skills for coding agents and test mode with live JSON snapshots. bslib 0.12 introduces offcanvas panels that slide in from viewport edges, plus direct sidebar handle resizing.</p>
<ul>
<li>Read more in the <a href="https://opensource.posit.co/blog/2026-08-04_shiny-r-1-14-python-1-7/" target="_blank" rel="noopener">Shiny updates blog post</a>.</li>
</ul>
<h2 id="positron-learning-resources">Positron learning resources
</h2>
<p>The Positron team is on a roll. Here are the key features from the <a href="https://opensource.posit.co/blog/2026-07-13_positron-2026-07-release/" target="_blank" rel="noopener">July Release Highlights</a>:</p>
<ul>
<li><strong>The Positron Notebook Editor now serves as the default option for .ipynb files</strong>: You get a fully integrated experience for Jupyter Notebooks out of the box. Key tools like environment management, version control, and AI coding support work automatically the moment you open any .ipynb file.</li>
<li><strong>The Packages pane has officially transitioned out of preview status</strong>: Inspect, update, and explore installed libraries without leaving your IDE or relying on Terminal commands.</li>
<li><strong>Posit Assistant has reached general availability</strong>: AI assistance is enterprise-ready: enjoy full-fledged coding assistance, debugging help, and inline explanations directly in your workflow with improved stability and performance.</li>
<li><strong>Support for opening Excel workbooks has been added to Data Explorer</strong>: No need to export to CSV or switch to Microsoft Excel to check your data: preview, filter, inspect .xlsx sheets directly inside Positron before running any code.</li>
<li><strong>Enhanced language intelligence for R</strong>: Navigating large R projects is much faster and less error-prone with features like Go to Definition, Find References, and Rename Symbol that work across all packages and scripts in your workspace. Diagnostics update instantly even when files change externally.</li>
</ul>
<p>The team has also shared several blog posts packed with tips and tricks for getting the most out of Positron:</p>
<ul>
<li><a href="https://opensource.posit.co/blog/2026-07-08_positron-uv/" target="_blank" rel="noopener">Discover how Positron paired with uv configures your Python environment in just one click</a></li>
<li><a href="https://opensource.posit.co/blog/2026-07-15_positron-environment-management-tips/" target="_blank" rel="noopener">Explore strategies for seamlessly managing your R and Python environments within Positron</a></li>
</ul>
<h2 id="lets-learn-about-ai">Let&rsquo;s learn about AI
</h2>
<p><strong>You can now receive the AI newsletter via email!</strong> Paste the newsletter&rsquo;s <a href="https://opensource.posit.co/tags/ai-newsletter/index.xml" target="_blank" rel="noopener">RSS feed URL</a> into a free RSS-to-email service like <a href="https://blogtrottr.com/" target="_blank" rel="noopener">https://blogtrottr.com/</a> to receive the newsletter by email.</p>
<ul>
<li>AGENTS.md, Skills, MCP servers, oh my! If you&rsquo;ve heard all these terms and want to investigate the differences, check out the <a href="https://opensource.posit.co/blog/2026-07-03_ai-newsletter/" target="_blank" rel="noopener">July 3rd edition of the AI Newsletter</a>.</li>
<li>LLMs often miss subtle visual artifacts in data visualizations, and there are ways of evaluating their oversights. Learn about bluffbench in the <a href="https://opensource.posit.co/blog/2026-07-17_ai-newsletter/" target="_blank" rel="noopener">July 17th edition of the AI Newsletter</a>.</li>
<li><a href="https://assistant.posit.co/" target="_blank" rel="noopener">Posit Assistant</a> in Positron now includes an EDA log feature to help you keep track of exploratory analysis done with the agent. Learn more in the <a href="https://opensource.posit.co/blog/2026-07-31_ai-newsletter/" target="_blank" rel="noopener">July 31st edition of the AI newsletter</a>.</li>
</ul>
<h2 id="do-you-want-event-recordings-you-got-them">Do you want event recordings? You got them!
</h2>
<p>&ldquo;Will this be recorded?&rdquo; If it is, you can access our recordings directly on the <a href="https://opensource.posit.co/events/" target="_blank" rel="noopener">event pages</a>, and we do our best to post them as quickly as possible.</p>
<p>Be sure to catch the presentations featured below, including Teun van den Brand&rsquo;s session on ggsql and Rich Iannone&rsquo;s presentation on Great Docs:</p>















  
  
  
  
  

  
  
  
  
  
    
  

  
  
  
  
  
    
  

  
  
  
  
  



<div class="grid grid-cols-3 gap-6 xl:gap-8 xl:gap-8 mb-6">
  
    <div>
      











  
  
  
  
  
















  
  
    
  












  
  





  
    
  









  
  






  





  
    
  






















  
    
  





  
    
  
























<div id="item-de299f206b1033e586cb69ebed60c69b" class="@container h-full">
  <a href="https://opensource.posit.co/resources/videos/2026-06-30_duckcon-7-ggsql-a-grammar-of-graphics-for-sql-teun-van-den-brand/" title="DuckCon #7 – ggsql: A grammar of graphics for SQL (Teun van den Brand)"
     class="card-link flex flex-col @venti:flex-row rounded-xl  h-full">

      



      
      

    
    
    <div class="w-full @venti:w-1/2 @venti:self-center aspect-[40/21] flex-shrink-0 flex items-center justify-center overflow-hidden rounded-xl">
      
        









  
  
    
    

    

    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @max-tall:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-06-30_duckcon-7-ggsql-a-grammar-of-graphics-for-sql-teun-van-den-brand/thumbnail_hu_b2e1015481833d64.webp 1x,
                  /resources/videos/2026-06-30_duckcon-7-ggsql-a-grammar-of-graphics-for-sql-teun-van-den-brand/thumbnail_hu_a676e22e82ce813f.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-06-30_duckcon-7-ggsql-a-grammar-of-graphics-for-sql-teun-van-den-brand/thumbnail_hu_897602341fe091b4.jpg"
          srcset="/resources/videos/2026-06-30_duckcon-7-ggsql-a-grammar-of-graphics-for-sql-teun-van-den-brand/thumbnail_hu_897602341fe091b4.jpg 1x,
                  /resources/videos/2026-06-30_duckcon-7-ggsql-a-grammar-of-graphics-for-sql-teun-van-den-brand/thumbnail_hu_13025d5418a80e9d.jpg 2x"
          alt="DuckCon #7 – ggsql: A grammar of graphics for SQL (Teun van den Brand)"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @tall:@max-grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-06-30_duckcon-7-ggsql-a-grammar-of-graphics-for-sql-teun-van-den-brand/thumbnail_hu_a676e22e82ce813f.webp 1x,
                  /resources/videos/2026-06-30_duckcon-7-ggsql-a-grammar-of-graphics-for-sql-teun-van-den-brand/thumbnail_hu_c76545a5a0e054b1.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-06-30_duckcon-7-ggsql-a-grammar-of-graphics-for-sql-teun-van-den-brand/thumbnail_hu_13025d5418a80e9d.jpg"
          srcset="/resources/videos/2026-06-30_duckcon-7-ggsql-a-grammar-of-graphics-for-sql-teun-van-den-brand/thumbnail_hu_13025d5418a80e9d.jpg 1x,
                  /resources/videos/2026-06-30_duckcon-7-ggsql-a-grammar-of-graphics-for-sql-teun-van-den-brand/thumbnail_hu_f18dd7e1387eef85.jpg 2x"
          alt="DuckCon #7 – ggsql: A grammar of graphics for SQL (Teun van den Brand)"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-06-30_duckcon-7-ggsql-a-grammar-of-graphics-for-sql-teun-van-den-brand/thumbnail_hu_c76545a5a0e054b1.webp 1x,
                  /resources/videos/2026-06-30_duckcon-7-ggsql-a-grammar-of-graphics-for-sql-teun-van-den-brand/thumbnail_hu_bf6653beaa56c5aa.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-06-30_duckcon-7-ggsql-a-grammar-of-graphics-for-sql-teun-van-den-brand/thumbnail_hu_f18dd7e1387eef85.jpg"
          srcset="/resources/videos/2026-06-30_duckcon-7-ggsql-a-grammar-of-graphics-for-sql-teun-van-den-brand/thumbnail_hu_f18dd7e1387eef85.jpg 1x,
                  /resources/videos/2026-06-30_duckcon-7-ggsql-a-grammar-of-graphics-for-sql-teun-van-den-brand/thumbnail_hu_50c1b8a79d2695c2.jpg 2x"
          alt="DuckCon #7 – ggsql: A grammar of graphics for SQL (Teun van den Brand)"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
  
  


      
    </div>

    
    <div class="hidden gap-y-2 @nip:flex @venti:w-1/2 flex-auto flex-col p-3 @venti:pl-5 text-gray-700 mt-2">

      
      

      
      
      <div class="hidden @short:flex flex-wrap gap-x-2 items-center text-xs font-medium text-gray-500">
        
          <span>Jun 30, 2026</span>
          <span>|</span>
        
        
        
          <span>12 min</span>
          <span>|</span>
        
        
          <span>2.7k views</span>
          
        
        
      </div>
      

      
      <h3 class="m-0! mt-3 font-semibold text-sm @grande:text-lg @venti:text-xl @trenta:text-2xl line-clamp-2 text-sm @tall:text-base @grande:text-lg @venti:text-xl @trenta:text-2xl text-gray-600">DuckCon #7 – ggsql: A grammar of graphics for SQL (Teun van den Brand)</h3>
      

      
        <p class="m-0! text-xs @short:text-sm @grande:text-md @venti:text-md @trenta:text-lg line-clamp-2 short@line-clamp-3 @tall:line-clamp-3 font-medium text-gray-600">Event: DuckCon #7 (DuckDB and DuckLake community conference), https://duckdb.org/events/2026/06/24/duckcon7/ Speaker: Teun van den Brand (Posit PBC) Slides: https://blobs.duckdb.org/events/duckcon7/teun-van-den-brand-ggsql.pdf</p>
      

      
        <div class="text-sm @grande:text-md">
          <div class="mt-2 flex flex-row gap-x-4 items-center"><div class="flex flex-row flex-shrink-0"><img 
          src="https://opensource.posit.co/people/teun-van-den-brand/teun-van-den-brand.jpg" 
          alt="Teun Van den Brand" 
          class="my-0! w-6 h-6 rounded-full object-cover ring-2 ring-white "
          style="z-index: 10;"
        ></div><div class="line-clamp-2 font-medium text-gray-600">Teun Van den Brand</div></div>
        </div>
      

      <div class="grow"></div>
      
        
          <div class="hidden @grande:flex flex-wrap gap-x-1 gap-y-4 py-2 overflow-hidden h-10 items-end">
            
              <span class="pill">
                ggsql
              </span>
            
          </div>
        
      
    </div>
  </a>
</div>


    </div>
  
    <div>
      











  
  
  
  
  
















  
  
    
  












  
  





  
    
  









  
  






  





  
    
  
    
  






















  
    
  





  
    
  
























<div id="item-ca048271111b051a31910a26462be382" class="@container h-full">
  <a href="https://opensource.posit.co/resources/videos/2026-07-06_great-docs-the-future-of-documentation-the-python-exchange-june-2026/" title="Great Docs: The Future of Documentation | The Python Exchange June 2026"
     class="card-link flex flex-col @venti:flex-row rounded-xl  h-full">

      



      
      

    
    
    <div class="w-full @venti:w-1/2 @venti:self-center aspect-[40/21] flex-shrink-0 flex items-center justify-center overflow-hidden rounded-xl">
      
        









  
  
    
    

    

    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @max-tall:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-07-06_great-docs-the-future-of-documentation-the-python-exchange-june-2026/thumbnail_hu_4a79da9b97975bb1.webp 1x,
                  /resources/videos/2026-07-06_great-docs-the-future-of-documentation-the-python-exchange-june-2026/thumbnail_hu_7c3afa6c6493dda.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-07-06_great-docs-the-future-of-documentation-the-python-exchange-june-2026/thumbnail_hu_ad0f5cdbfef37964.jpg"
          srcset="/resources/videos/2026-07-06_great-docs-the-future-of-documentation-the-python-exchange-june-2026/thumbnail_hu_ad0f5cdbfef37964.jpg 1x,
                  /resources/videos/2026-07-06_great-docs-the-future-of-documentation-the-python-exchange-june-2026/thumbnail_hu_c974e8c34f17a663.jpg 2x"
          alt="Great Docs: The Future of Documentation | The Python Exchange June 2026"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @tall:@max-grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-07-06_great-docs-the-future-of-documentation-the-python-exchange-june-2026/thumbnail_hu_7c3afa6c6493dda.webp 1x,
                  /resources/videos/2026-07-06_great-docs-the-future-of-documentation-the-python-exchange-june-2026/thumbnail_hu_e2f3f8ee4b22f520.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-07-06_great-docs-the-future-of-documentation-the-python-exchange-june-2026/thumbnail_hu_c974e8c34f17a663.jpg"
          srcset="/resources/videos/2026-07-06_great-docs-the-future-of-documentation-the-python-exchange-june-2026/thumbnail_hu_c974e8c34f17a663.jpg 1x,
                  /resources/videos/2026-07-06_great-docs-the-future-of-documentation-the-python-exchange-june-2026/thumbnail_hu_af5a09ce00461bfb.jpg 2x"
          alt="Great Docs: The Future of Documentation | The Python Exchange June 2026"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-07-06_great-docs-the-future-of-documentation-the-python-exchange-june-2026/thumbnail_hu_e2f3f8ee4b22f520.webp 1x,
                  /resources/videos/2026-07-06_great-docs-the-future-of-documentation-the-python-exchange-june-2026/thumbnail_hu_6c04a9d6eaf920ed.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-07-06_great-docs-the-future-of-documentation-the-python-exchange-june-2026/thumbnail_hu_af5a09ce00461bfb.jpg"
          srcset="/resources/videos/2026-07-06_great-docs-the-future-of-documentation-the-python-exchange-june-2026/thumbnail_hu_af5a09ce00461bfb.jpg 1x,
                  /resources/videos/2026-07-06_great-docs-the-future-of-documentation-the-python-exchange-june-2026/thumbnail_hu_991395b2a0842829.jpg 2x"
          alt="Great Docs: The Future of Documentation | The Python Exchange June 2026"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
  
  


      
    </div>

    
    <div class="hidden gap-y-2 @nip:flex @venti:w-1/2 flex-auto flex-col p-3 @venti:pl-5 text-gray-700 mt-2">

      
      

      
      
      <div class="hidden @short:flex flex-wrap gap-x-2 items-center text-xs font-medium text-gray-500">
        
          <span>Jul 6, 2026</span>
          <span>|</span>
        
        
        
          <span>1h 5min</span>
          <span>|</span>
        
        
          <span>174 views</span>
          
        
        
      </div>
      

      
      <h3 class="m-0! mt-3 font-semibold text-sm @grande:text-lg @venti:text-xl @trenta:text-2xl line-clamp-2 text-sm @tall:text-base @grande:text-lg @venti:text-xl @trenta:text-2xl text-gray-600">Great Docs: The Future of Documentation | The Python Exchange June 2026</h3>
      

      
        <p class="m-0! text-xs @short:text-sm @grande:text-md @venti:text-md @trenta:text-lg line-clamp-2 short@line-clamp-3 @tall:line-clamp-3 font-medium text-gray-600">About the Python Exchange for the Department of Energy: https://meetup.doepy.org/ Sign up here to be notified when the next Python Exchange is scheduled: https://bit.ly/py-exchange
Guest Panelist: Rich Iannone
Title: Great Docs: Building the Documentation Site Generator I Always Wanted
Bio: Rich Iannone Rich is a software engineer and someone who cares a great deal about documentation and summary tables. He&rsquo;s been working on table presentation and documentation packages for quite a long time, and nobody has told him to stop yet, so he&rsquo;s still on top of all that. He started off mostly creating open-source packages in R, but more lately, he&rsquo;s been doing open-source things in Python. Rich really enjoys creating all these packages because he realizes that people can use &rsquo;em to do great things in their own work.
Abstract: Documentation generators have, for me, been a thing where there are so many trade-offs. They might make beautiful sites, but be shallow feature-wise. Or full of features but really hard to configure. After some years of working across R, Python, and the broader open-source ecosystem, I started Great Docs with a simple end goal: your docs should look really great, be easy to discover, and be ready for the AI era.
In this presentation, I will walk through the various places I drew inspiration for Great Docs (pkgdown, MATLAB, Elixir, and the Vue.js docs). I&rsquo;ll show how Great Docs makes documentation consumable by both humans and AI agents through Skills and MCP server documentation. I&rsquo;ll share some of the features I obsessively built because I wanted them in my own package docs. Finally, I&rsquo;ll discuss where this project is headed beyond Python (toward documenting CLIs, TUIs, and more).
At Don&rsquo;t Use This Code, we want to create a unique opportunity to see Python succeed and thrive within the DOE. In collaboration with an organizing committee of Python users and advocates from various DOE sites, we host regular conversations on the topic of Python in order to create a new resource for scientists, researchers, and technical staff. Our goal is to share best practices and build a strong, lasting community for Python users within the Department of Energy National Lab system.
If you, or someone you know, might make a great panelist, we&rsquo;d love to get in touch! Just send us an email at DOEPy@dutc.io if you would like to participate or have any questions
</p>
      

      
        <div class="text-sm @grande:text-md">
          <div class="mt-2 flex flex-row gap-x-4 items-center"><div class="flex flex-row flex-shrink-0"><img 
          src="https://opensource.posit.co/people/rich-iannone/rich-headshot.png" 
          alt="Rich Iannone" 
          class="my-0! w-6 h-6 rounded-full object-cover ring-2 ring-white "
          style="z-index: 10;"
        ></div><div class="line-clamp-2 font-medium text-gray-600">Rich Iannone</div></div>
        </div>
      

      <div class="grow"></div>
      
        
          <div class="hidden @grande:flex flex-wrap gap-x-1 gap-y-4 py-2 overflow-hidden h-10 items-end">
            
              <span class="pill">
                Great Docs
              </span>
            
              <span class="pill">
                pkgdown
              </span>
            
          </div>
        
      
    </div>
  </a>
</div>


    </div>
  
</div>


<p>Additionally, we have some fantastic recordings from the <a href="https://pos.it/dslab" target="_blank" rel="noopener">Data Science Lab</a>. Watch the Lab Managers share their expertise while live coding:</p>















  
  
  
  
  

  
  
  
  
  
    
  

  
  
  
  
  
    
  

  
  
  
  
  



<div class="grid grid-cols-3 gap-6 xl:gap-8 xl:gap-8 mb-6">
  
    <div>
      











  
  
  
  
  
















  
  
    
  












  
  





  
    
  









  
  






  





  
    
  
    
  
    
  
    
  






















  
    
  





  
    
  
























<div id="item-9b757f4d2908c3491363411a7cadd25a" class="@container h-full">
  <a href="https://opensource.posit.co/resources/videos/2026-07-24_live-tidytuesday-data-viz-workflow-nicola-rennie-data-science-lab/" title="Live TidyTuesday Data Viz Workflow | Nicola Rennie | Data Science Lab"
     class="card-link flex flex-col @venti:flex-row rounded-xl  h-full">

      



      
      

    
    
    <div class="w-full @venti:w-1/2 @venti:self-center aspect-[40/21] flex-shrink-0 flex items-center justify-center overflow-hidden rounded-xl">
      
        









  
  
    
    

    

    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @max-tall:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-07-24_live-tidytuesday-data-viz-workflow-nicola-rennie-data-science-lab/thumbnail_hu_91cdc9bfec4beb.webp 1x,
                  /resources/videos/2026-07-24_live-tidytuesday-data-viz-workflow-nicola-rennie-data-science-lab/thumbnail_hu_2bdf2408bfb74ae8.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-07-24_live-tidytuesday-data-viz-workflow-nicola-rennie-data-science-lab/thumbnail_hu_1cb3889dbd947c4c.jpg"
          srcset="/resources/videos/2026-07-24_live-tidytuesday-data-viz-workflow-nicola-rennie-data-science-lab/thumbnail_hu_1cb3889dbd947c4c.jpg 1x,
                  /resources/videos/2026-07-24_live-tidytuesday-data-viz-workflow-nicola-rennie-data-science-lab/thumbnail_hu_436953f457f71693.jpg 2x"
          alt="Live TidyTuesday Data Viz Workflow | Nicola Rennie | Data Science Lab"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @tall:@max-grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-07-24_live-tidytuesday-data-viz-workflow-nicola-rennie-data-science-lab/thumbnail_hu_2bdf2408bfb74ae8.webp 1x,
                  /resources/videos/2026-07-24_live-tidytuesday-data-viz-workflow-nicola-rennie-data-science-lab/thumbnail_hu_c0da8212f74d2e17.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-07-24_live-tidytuesday-data-viz-workflow-nicola-rennie-data-science-lab/thumbnail_hu_436953f457f71693.jpg"
          srcset="/resources/videos/2026-07-24_live-tidytuesday-data-viz-workflow-nicola-rennie-data-science-lab/thumbnail_hu_436953f457f71693.jpg 1x,
                  /resources/videos/2026-07-24_live-tidytuesday-data-viz-workflow-nicola-rennie-data-science-lab/thumbnail_hu_ba0631db832f77db.jpg 2x"
          alt="Live TidyTuesday Data Viz Workflow | Nicola Rennie | Data Science Lab"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-07-24_live-tidytuesday-data-viz-workflow-nicola-rennie-data-science-lab/thumbnail_hu_c0da8212f74d2e17.webp 1x,
                  /resources/videos/2026-07-24_live-tidytuesday-data-viz-workflow-nicola-rennie-data-science-lab/thumbnail_hu_830f9934dc51456c.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-07-24_live-tidytuesday-data-viz-workflow-nicola-rennie-data-science-lab/thumbnail_hu_ba0631db832f77db.jpg"
          srcset="/resources/videos/2026-07-24_live-tidytuesday-data-viz-workflow-nicola-rennie-data-science-lab/thumbnail_hu_ba0631db832f77db.jpg 1x,
                  /resources/videos/2026-07-24_live-tidytuesday-data-viz-workflow-nicola-rennie-data-science-lab/thumbnail_hu_e8ac5809a85eaadf.jpg 2x"
          alt="Live TidyTuesday Data Viz Workflow | Nicola Rennie | Data Science Lab"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
  
  


      
    </div>

    
    <div class="hidden gap-y-2 @nip:flex @venti:w-1/2 flex-auto flex-col p-3 @venti:pl-5 text-gray-700 mt-2">

      
      

      
      
      <div class="hidden @short:flex flex-wrap gap-x-2 items-center text-xs font-medium text-gray-500">
        
          <span>Jul 24, 2026</span>
          <span>|</span>
        
        
        
          <span>51 min</span>
          <span>|</span>
        
        
          <span>1.1k views</span>
          
        
        
      </div>
      

      
      <h3 class="m-0! mt-3 font-semibold text-sm @grande:text-lg @venti:text-xl @trenta:text-2xl line-clamp-2 text-sm @tall:text-base @grande:text-lg @venti:text-xl @trenta:text-2xl text-gray-600">Live TidyTuesday Data Viz Workflow | Nicola Rennie | Data Science Lab</h3>
      

      
        <p class="m-0! text-xs @short:text-sm @grande:text-md @venti:text-md @trenta:text-lg line-clamp-2 short@line-clamp-3 @tall:line-clamp-3 font-medium text-gray-600">The Data Science Lab is a live weekly call. Register at pos.it/dslab! Discord invites go out each week on lives calls. We&rsquo;d love to have you!
The Lab is an open, messy space for learning and asking questions. Think of it like pair coding with a friend or two. Learn something new, and share what you know to help others grow.
On this call, Libby Heeren is joined by Nicola Rennie, who walks through her process for making a TidyTuesday data visualization in R: live coding a chart from scratch and sharing the workflow tricks she&rsquo;s built up over five years of doing TidyTuesday!
Nicola takes this week&rsquo;s TidyTuesday &ldquo;twin cities&rdquo; dataset and builds a colorful sunburst (radial) chart from scratch, showing how she decides what to plot, wrangles the data with dplyr, and makes it pretty using showtext fonts, rcartocolor palettes, colored-text legends, and the ggview package for previewing plots at exactly the size they&rsquo;ll save at. Along the way she shares the templates and helper packages she&rsquo;s made to save time every week (can you tell she&rsquo;s a pro?!). Since TidyTuesday data is public, you can grab the same dataset and follow along, too!
Hosting crew from Posit: Libby Heeren, Isabella Velasquez
Socials and websites: Nicola Rennie&rsquo;s blog: https://nrennie.rbind.io/ Nicola Rennie&rsquo;s GitHub: https://github.com/nrennie Nicola Rennie&rsquo;s Bluesky: https://bsky.app/profile/nrennie.bsky.social Nicola Rennie&rsquo;s LinkedIn: https://www.linkedin.com/in/nicola-rennie/ Nicola Rennie&rsquo;s Mastodon: https://fosstodon.org/deck/@nrennie
Resources mentioned in the video and chat: Nicola&rsquo;s code from this session: https://nrennie.rbind.io/talks/posit-ds-lab/ TidyTuesday on GitHub: https://github.com/rfordatascience/tidytuesday Nicola&rsquo;s templates package: https://github.com/nrennie/templates showtext package (fonts in R): https://github.com/yixuan/showtext ggview package (preview plots at save size): https://github.com/idmn/ggview rcartocolor package (accessible color palettes): https://nowosad.r-universe.dev/rcartocolor ggtext package (styled/colored text): https://wilkelab.org/ggtext/ ggiraph package (interactive ggplots): https://davidgohel.github.io/ggiraph/ marquee package (markdown text in R graphics): https://marquee.r-lib.org/ Nicola&rsquo;s book, The Art of Data Visualization with ggplot2: https://nrennie.rbind.io/art-of-viz/ Bootstrap Icons: https://icons.getbootstrap.com/ okpalette (extract a palette from a photo): https://okpalette.color.pizza/
► Subscribe to Our Channel Here: https://bit.ly/2TzgcOu Follow Us Here: Website: https://www.posit.co Hangout: https://pos.it/dsh The Lab: https://pos.it/dslab LinkedIn: https://www.linkedin.com/company/posit-software Bluesky: https://bsky.app/profile/posit.co
Thanks for hanging out with us!
Timestamps 00:00 Introduction 01:18 Reviewing the TidyTuesday twin cities dataset 03:41 Demoing TidyTuesday template files and R package setup 07:25 &ldquo;Do showtext and systemfonts do different things?&rdquo; 09:04 Loading and prepping the links and cities datasets 11:45 Exploring and filtering the dataset to a chosen city 17:25 Demoing a Shiny app to copy previous TidyTuesday chart code 23:56 Demoing sunburst chart creation with coord_radial 25:47 &ldquo;Can you say why you used mutate rather than summarize?&rdquo; 31:09 Adding custom colors with the rcartocolor package 33:57 Demoing colored subtitle text with the ggtext package 40:38 Attempting to make the chart interactive with the ggiraph package 43:32 Adding automated social and source captions 46:26 &ldquo;So you can save a plot with the canvas call and it doesn&rsquo;t mess up the ggplot object?&rdquo; 49:55 &ldquo;If people would like to keep up with you and see your posts, what&rsquo;s the best way to do that?&rdquo;
</p>
      

      

      <div class="grow"></div>
      
        
          <div class="hidden @grande:flex flex-wrap gap-x-1 gap-y-4 py-2 overflow-hidden h-10 items-end">
            
              <span class="pill">
                dplyr
              </span>
            
              <span class="pill">
                ggplot2
              </span>
            
              <span class="pill">
                Shiny
              </span>
            
              <span class="pill">
                systemfonts
              </span>
            
          </div>
        
      
    </div>
  </a>
</div>


    </div>
  
    <div>
      











  
  
  
  
  
















  
  
    
  












  
  





  
    
  









  
  






  





  
    
  
    
  
    
  






















  
    
  





  
    
  
























<div id="item-d40c591fd8755cf94c245b16a57ac876" class="@container h-full">
  <a href="https://opensource.posit.co/resources/videos/2026-07-23_mind-blowing-quarto-slide-extensions-emil-hvitfeldt-data-science-lab/" title="Mind-Blowing Quarto Slide Extensions 💬 🖍️ 🤯 | Emil Hvitfeldt | Data Science Lab"
     class="card-link flex flex-col @venti:flex-row rounded-xl  h-full">

      



      
      

    
    
    <div class="w-full @venti:w-1/2 @venti:self-center aspect-[40/21] flex-shrink-0 flex items-center justify-center overflow-hidden rounded-xl">
      
        









  
  
    
    

    

    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @max-tall:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-07-23_mind-blowing-quarto-slide-extensions-emil-hvitfeldt-data-science-lab/thumbnail_hu_a4ef79aec4159f0b.webp 1x,
                  /resources/videos/2026-07-23_mind-blowing-quarto-slide-extensions-emil-hvitfeldt-data-science-lab/thumbnail_hu_12eca1b1ff66693f.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-07-23_mind-blowing-quarto-slide-extensions-emil-hvitfeldt-data-science-lab/thumbnail_hu_400ddecd3f838eab.jpg"
          srcset="/resources/videos/2026-07-23_mind-blowing-quarto-slide-extensions-emil-hvitfeldt-data-science-lab/thumbnail_hu_400ddecd3f838eab.jpg 1x,
                  /resources/videos/2026-07-23_mind-blowing-quarto-slide-extensions-emil-hvitfeldt-data-science-lab/thumbnail_hu_b22a6dec3c217c9d.jpg 2x"
          alt="Mind-Blowing Quarto Slide Extensions 💬 🖍️ 🤯 | Emil Hvitfeldt | Data Science Lab"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @tall:@max-grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-07-23_mind-blowing-quarto-slide-extensions-emil-hvitfeldt-data-science-lab/thumbnail_hu_12eca1b1ff66693f.webp 1x,
                  /resources/videos/2026-07-23_mind-blowing-quarto-slide-extensions-emil-hvitfeldt-data-science-lab/thumbnail_hu_b4bdbf1c5b2fd5df.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-07-23_mind-blowing-quarto-slide-extensions-emil-hvitfeldt-data-science-lab/thumbnail_hu_b22a6dec3c217c9d.jpg"
          srcset="/resources/videos/2026-07-23_mind-blowing-quarto-slide-extensions-emil-hvitfeldt-data-science-lab/thumbnail_hu_b22a6dec3c217c9d.jpg 1x,
                  /resources/videos/2026-07-23_mind-blowing-quarto-slide-extensions-emil-hvitfeldt-data-science-lab/thumbnail_hu_c7f3360f4b455ac6.jpg 2x"
          alt="Mind-Blowing Quarto Slide Extensions 💬 🖍️ 🤯 | Emil Hvitfeldt | Data Science Lab"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
      
      

      
      
      

      
      
      
      

      <picture class="hidden @grande:block w-full h-full">
        <source
          type="image/webp"
          srcset="/resources/videos/2026-07-23_mind-blowing-quarto-slide-extensions-emil-hvitfeldt-data-science-lab/thumbnail_hu_b4bdbf1c5b2fd5df.webp 1x,
                  /resources/videos/2026-07-23_mind-blowing-quarto-slide-extensions-emil-hvitfeldt-data-science-lab/thumbnail_hu_e2aa1e91b2f546a2.webp 2x" />
        <img
          src="https://opensource.posit.co/resources/videos/2026-07-23_mind-blowing-quarto-slide-extensions-emil-hvitfeldt-data-science-lab/thumbnail_hu_c7f3360f4b455ac6.jpg"
          srcset="/resources/videos/2026-07-23_mind-blowing-quarto-slide-extensions-emil-hvitfeldt-data-science-lab/thumbnail_hu_c7f3360f4b455ac6.jpg 1x,
                  /resources/videos/2026-07-23_mind-blowing-quarto-slide-extensions-emil-hvitfeldt-data-science-lab/thumbnail_hu_d69b5bc0a20d0f5b.jpg 2x"
          alt="Mind-Blowing Quarto Slide Extensions 💬 🖍️ 🤯 | Emil Hvitfeldt | Data Science Lab"
          class="mx-auto h-full w-full object-cover"
          loading="lazy" />
      </picture>
    
  
  


      
    </div>

    
    <div class="hidden gap-y-2 @nip:flex @venti:w-1/2 flex-auto flex-col p-3 @venti:pl-5 text-gray-700 mt-2">

      
      

      
      
      <div class="hidden @short:flex flex-wrap gap-x-2 items-center text-xs font-medium text-gray-500">
        
          <span>Jul 23, 2026</span>
          <span>|</span>
        
        
        
          <span>56 min</span>
          <span>|</span>
        
        
          <span>3.8k views</span>
          
        
        
      </div>
      

      
      <h3 class="m-0! mt-3 font-semibold text-sm @grande:text-lg @venti:text-xl @trenta:text-2xl line-clamp-2 text-sm @tall:text-base @grande:text-lg @venti:text-xl @trenta:text-2xl text-gray-600">Mind-Blowing Quarto Slide Extensions 💬 🖍️ 🤯 | Emil Hvitfeldt | Data Science Lab</h3>
      

      
        <p class="m-0! text-xs @short:text-sm @grande:text-md @venti:text-md @trenta:text-lg line-clamp-2 short@line-clamp-3 @tall:line-clamp-3 font-medium text-gray-600">The Data Science Lab is a live weekly call. Register at pos.it/dslab! Discord invites go out each week on lives calls. We&rsquo;d love to have you!
The Lab is an open, messy space for learning and asking questions. Think of it like pair coding with a friend or two. Learn something new, and share what you know to help others grow.
On this call, Libby Heeren is joined by Emil Hvitfeldt, who walks through the art of &ldquo;slide crafting&rdquo; in Quarto and shows off the collection of Reveal JS extensions he&rsquo;s been building to make slides way more fun and powerful.
Emil starts with the basics of building slides in Quarto with the Reveal JS format (fragments, columns, a little CSS) and explains how Quarto extensions differ from packages. Then he shows off the good stuff: iMessage-style chat bubbles hand-drawn &ldquo;rough notation&rdquo; highlighting and annotations ️ the Flourish extension for animating code chunks, gorgeous reusable themes, and his in-progress &ldquo;editable&rdquo; extension that lets you drag, resize, and restyle slide elements right in the browser and save the changes back to your source. And, spoiler, his &ldquo;editable&rdquo; extension is much further along as of the publishing of this video!! If you&rsquo;ve ever fought with slide layouts, there is definitely something here you can try at home!
Hosting crew from Posit: Libby Heeren, Isabella Velasquez
Emil&rsquo;s socials and urls: website: https://emilhvitfeldt.com/ GitHub: https://github.com/emilhvitfeldt Bluesky: https://bsky.app/profile/emilhvitfeldt.bsky.social LinkedIn: https://www.linkedin.com/in/emilhvitfeldt/
Resources mentioned in the video and chat: Emil&rsquo;s GitHub (home of his Quarto extensions): https://github.com/EmilHvitfeldt quarto-revealjs-editable extension: https://github.com/EmilHvitfeldt/quarto-revealjs-editable quarto-revealjs-rows extension (shared by Emil after the session): https://github.com/EmilHvitfeldt/quarto-revealjs-rows Quarto Reveal JS documentation: https://quarto.org/docs/presentations/revealjs/ Quarto Reveal JS demo slides: https://quarto.org/docs/presentations/revealjs/demo/#/title-slide News! Quarto 2 (Quarto in Rust): https://quarto.org/docs/blog/posts/2026-04-06-whats-next-quarto-2/ Dynamic Tabsets in Quarto (Josh Cowley): https://josh.quarto.pub/posts/2022-11-10-quarto-tabsets-update/2022-11-10-quarto-tabsets-update.html Embedding Reveal JS via iframe (Quarto source example): https://github.com/quarto-dev/quarto-web/blob/main/docs/presentations/revealjs/index.qmd#L12-L14 posit::conf 2026: https://conf.posit.co/2026/ R/Medicine conference: https://rconsortium.github.io/RMedicine_website/ Muntasir Masum&rsquo;s slides repo: https://github.com/muntasirmasum/epi553-data-viz Example Quarto slides — Nicola Rennie (Styling Charts): https://nrennie.rbind.io/MFC-CDT-data-viz/slides/styling-charts-r.html Example Quarto slides — Colin Madland: https://cmadland.github.io/slides/ Colin Madland&rsquo;s slides repo: https://github.com/cmadland/slides Example Quarto slides — Raha Dastgheyb (R/Medicine 2024): https://rdastgh1.quarto.pub/rmedicine-2024-scidatareportr/#/title-slide
► Subscribe to Our Channel Here: https://bit.ly/2TzgcOu Follow Us Here: Website: https://www.posit.co Hangout: https://pos.it/dsh The Lab: https://pos.it/dslab LinkedIn: https://www.linkedin.com/company/posit-software Bluesky: https://bsky.app/profile/posit.co
Thanks for hanging out with us!
Timestamps 00:00 Introduction 01:22 Demoing a primer on Reveal JS slides in Quarto 05:11 &ldquo;Does that PDF export still use Pandoc, or is there an option of exporting via Typst?&rdquo; 08:17 Demoing slide crafting with a new Quarto document 11:24 Demoing how to download and use a Quarto template 14:01 &ldquo;Is this UI you&rsquo;re using for slide generation exclusive to Positron?&rdquo; 16:16 Demoing how to install a Quarto extension from the terminal 18:45 &ldquo;Is there a maximum number of columns you can have?&rdquo; 19:18 Demoing adding custom CSS classes to slide columns 23:03 &ldquo;Do we know if extensions will still be submitted to the OpenVSX marketplace?&rdquo; 25:11 &ldquo;Are these Quarto extensions global, or are they per project?&rdquo; 27:42 &ldquo;When you change the text fonts inside those columns, could you do it for all the columns at once, maybe by changing the top div?&rdquo; 29:31 &ldquo;Do you also get grid logic like you have with CSS Grid?&rdquo; 31:54 Demoing Quarto VS Code extensions for autocomplete and shortcodes 34:32 Demoing the chat bubbles extension 37:38 Demoing the rough notation extension for highlighting text fragments 40:20 Demoing the Flourish extension for highlighting code chunks 43:07 Demoing the editable extension for dragging and adjusting slide elements 49:28 Demoing custom Reveal JS slide themes 52:26 &ldquo;Is there a Quarto extension that can help dynamically create tabs or chunks?&rdquo; (see dynamic tabs link in the resources above!) 53:58 Explaining the default code chunk highlighting feature in Quarto 55:11 &ldquo;How do you embed a Reveal JS site into another Quarto document?&rdquo;
</p>
      

      
        <div class="text-sm @grande:text-md">
          <div class="mt-2 flex flex-row gap-x-4 items-center"><div class="flex flex-row flex-shrink-0"><img 
          src="https://opensource.posit.co/people/emil-hvitfeldt/profile.jpg" 
          alt="Emil Hvitfeldt" 
          class="my-0! w-6 h-6 rounded-full object-cover ring-2 ring-white "
          style="z-index: 10;"
        ></div><div class="line-clamp-2 font-medium text-gray-600">Emil Hvitfeldt</div></div>
        </div>
      

      <div class="grow"></div>
      
        
          <div class="hidden @grande:flex flex-wrap gap-x-1 gap-y-4 py-2 overflow-hidden h-10 items-end">
            
              <span class="pill">
                Positron
              </span>
            
              <span class="pill">
                Quarto
              </span>
            
              <span class="pill">
                revealjs
              </span>
            
          </div>
        
      
    </div>
  </a>
</div>


    </div>
  
</div>


<p>We hope you learn a lot from these resources!</p>
<h2 id="showcases-from-the-community">Showcases from the community
</h2>
<p>We love learning what you are up to. If you have a project using our tools to share, please let us know. In particular, I&rsquo;d love to hear how you&rsquo;re adopting and using Positron! Find me on <a href="https://www.linkedin.com/in/ivelasq/" target="_blank" rel="noopener">LinkedIn</a> and <a href="https://bsky.app/profile/ivelasq.bsky.social" target="_blank" rel="noopener">Bluesky</a>.</p>









  
  
    
  

  
  
    
  





  


<div class="grid gap-12 items-start mt-12 md:grid-cols-2 ">
  
  
    
    
    
      <div class="prose max-w-none "><p>Alfredo H. S. shared a small, opinionated skill for turning papers and other source material into concise Quarto RevealJS presentations with AI coding agents.</p>
<p>Try it out, and let Alfredo know how it goes!</p>
<ul>
<li><a href="https://forum.posit.co/t/a-small-quarto-ai-skill-for-better-slides/215878" target="_blank" rel="noopener">Forum discussion</a></li>
<li><a href="https://alfredohs.com/blog/quarto_talks/" target="_blank" rel="noopener">About the skill</a></li>
<li><a href="http://SKILL.md" target="_blank" rel="noopener">SKILL.md</a></li>
</ul>
</div>
    
  
    
    
    
      <div class="prose max-w-none "><div class="not-prose"><figure>
      <img class="h-auto max-w-full rounded-lg"
        src="https://opensource.posit.co/blog/2026-08-05_glimpse-aug-2026/images/image1.png"
        alt="" 
        loading="lazy"
      >
    </figure></div></div>
    
  
</div>










  
  
    
  

  
  
    
  





  


<div class="grid gap-12 items-start mt-12 md:grid-cols-2 ">
  
  
    
    
    
      <div class="prose max-w-none "><div class="not-prose"><figure>
      <img class="h-auto max-w-full rounded-lg"
        src="https://opensource.posit.co/blog/2026-08-05_glimpse-aug-2026/images/image2.png"
        alt="" 
        loading="lazy"
      >
    </figure></div></div>
    
  
    
    
    
      <div class="prose max-w-none "><p>REYL Intesa Sanpaolo&rsquo;s Risk Management team built their own Incident Management Tool with Shiny and Posit Connect, cutting incident follow-up from days to 30 minutes. Now, they&rsquo;re eyeing the same R-and-Connect pattern for liquidity monitoring, credit risk appetite, and stress testing.</p>
<ul>
<li><a href="https://posit.co/about/customer-stories/reyl-intesa-sanpaolo" target="_blank" rel="noopener">Check out the spotlight</a></li>
</ul></div>
    
  
</div>










  
  
    
  

  
  
    
  





  


<div class="grid gap-12 items-start mt-12 md:grid-cols-2 ">
  
  
    
    
    
      <div class="prose max-w-none "><p>Interested in testing your regression skills? Angela Heberger is looking for feedback on RelationLab, an interactive Shiny app for learning correlation and regression. Give it a shot and let Angela know what you think!</p>
<ul>
<li><a href="https://forum.posit.co/t/feedback-requested-on-relationlab-an-interactive-shiny-app-for-learning-correlation-and-regression/216954" target="_blank" rel="noopener">Forum discussion</a></li>
<li><a href="https://angela-heberger.shinyapps.io/correlation-regression-learning-app/" target="_blank" rel="noopener">Shiny app</a></li>
</ul></div>
    
  
    
    
    
      <div class="prose max-w-none "><div class="not-prose"><figure>
      <img class="h-auto max-w-full rounded-lg"
        src="https://opensource.posit.co/blog/2026-08-05_glimpse-aug-2026/images/image3.png"
        alt="" 
        loading="lazy"
      >
    </figure></div></div>
    
  
</div>

<p>We usually find these projects on social media. If you&rsquo;re on LinkedIn, be sure to follow and tag <a href="https://www.linkedin.com/showcase/posit-open-source/" target="_blank" rel="noopener">Posit Open Source</a> for us to share the amazing things you&rsquo;re working on!</p>
<h2 id="whats-next">What’s next
</h2>
<p>Besides posit::conf (have you <a href="https://conf.posit.co/2026/" target="_blank" rel="noopener">registered for conf</a> yet? You should!), we have lots of goodies on the horizon.</p>
<ul>
<li>Emil Hvidtfelt is leading a 2-hour workshop all about making great slides in Quarto on Aug 12th. <a href="https://events.zoom.us/ev/AhS1wYPFlx9m2sfuhnidFjnvI2UlS3PKv6JRVL_bv4UUydDxeKnK~AtgzvV6vmUoaeLU-9KZrRyj3QUGklZmSV2a1hFRfixtmtlszjyaUoneXVw" target="_blank" rel="noopener">Register here</a>!</li>
<li>Join us for an upcoming <a href="https://pos.it/dsh" target="_blank" rel="noopener">Data Science Hangout</a> or <a href="https://pos.it/dslab" target="_blank" rel="noopener">Data Science Lab</a>.</li>
</ul>
<p>I would love to know how to make the Glimpse newsletter better. Email me at isabella [dot] velasquez [at] posit.co.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-08-05_glimpse-aug-2026/thumbnail.jpg" length="68574" type="image/jpeg" />
    </item>
    <item>
      <title>Shiny updates: R 1.14, Python 1.7, bslib 0.12</title>
      <link>https://opensource.posit.co/blog/2026-08-04_shiny-r-1-14-python-1-7/</link>
      <pubDate>Tue, 04 Aug 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-08-04_shiny-r-1-14-python-1-7/</guid>
      <dc:creator>Barret Schloerke</dc:creator><description><![CDATA[<p>We&rsquo;re happy to announce a trio of releases: <a href="https://cran.r-project.org/package=shiny" target="_blank" rel="noopener">Shiny for R v1.14</a> and <a href="https://cran.r-project.org/package=bslib" target="_blank" rel="noopener">bslib v0.12</a> are now on CRAN, and <a href="https://pypi.org/project/shiny/" target="_blank" rel="noopener">Shiny for Python v1.7</a> is now on PyPI!</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-1" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-1-1">R</a></li>
<li><a href="#tabset-1-2">Python</a></li>
</ul>
<div id="tabset-1-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="nf">c</span><span class="p">(</span><span class="s">&#34;shiny&#34;</span><span class="p">,</span> <span class="s">&#34;bslib&#34;</span><span class="p">))</span></span></span></code></pre></div></div>
</div>
<div id="tabset-1-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install -U shiny</span></span></code></pre></div></div>
</div>
</div>
<p>The highlights: <a href="#agent-skills">bundled agent skills</a> that teach coding agents how to write Shiny for Python apps, <a href="#non-blocking-apps"><code>startApp()</code></a> for running R apps without blocking your console, <a href="#offcanvas-panels">offcanvas panels</a> that slide in from the edge of the viewport, and <a href="#module-cleanup"><code>destroy()</code></a> for cleaning up dynamic modules.</p>
<p>Full details are in the <a href="https://shiny.posit.co/r/reference/shiny/1.14.0/upgrade.html" target="_blank" rel="noopener">Shiny for R release notes</a>, the <a href="https://rstudio.github.io/bslib/news/index.html#bslib-0120" target="_blank" rel="noopener">bslib release notes</a>, and the <a href="https://github.com/posit-dev/py-shiny/blob/main/CHANGELOG.md" target="_blank" rel="noopener">Shiny for Python changelog</a>.</p>
<h2 id="agent-skills">Agent skills
</h2>
<p><em>Shiny for Python only &mdash; coming to R soon.</em></p>
<p>Coding agents are writing more and more Shiny apps &mdash; so we&rsquo;re teaching them how to do it well. Shiny for Python v1.7 ships with bundled <a href="https://agentskills.io" target="_blank" rel="noopener">Agent Skills</a>: a <code>shiny-for-python</code> skill whose <code>SKILL.md</code> routes agents to focused reference files covering each area of Shiny&rsquo;s public API &mdash; reactivity, Express mode, modules, layouts, plots, data frames, chat and streaming, extended tasks, testing, debugging, OpenTelemetry, and more.</p>
<p>To install the bundled skills into your coding agent, use <a href="https://library-skills.io" target="_blank" rel="noopener">library-skills</a>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">uvx library-skills --claude</span></span></code></pre></div></div>
<p>Because the skills ship inside the package, they always match the version of Shiny you have installed &mdash; and <code>shiny skills list</code> shows what&rsquo;s bundled.</p>
<p>Now your agent stops hand-rolling HTML tables and fake tabs, and starts using the framework the way you would.</p>
<h2 id="non-blocking-apps">Non-blocking apps
</h2>
<p><em>Shiny for R only.</em></p>
<p><a href="https://shiny.posit.co/r/reference/shiny/1.14.0/runApp.html" target="_blank" rel="noopener"><code>runApp()</code></a> blocks your R console until the app stops. New in Shiny v1.14, <a href="https://shiny.posit.co/r/reference/shiny/1.14.0/startApp.html" target="_blank" rel="noopener"><code>startApp()</code></a> runs the app in the background and hands control right back to you:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># Start app in the background</span>
</span></span><span class="line"><span class="cl"><span class="n">handle</span> <span class="o">&lt;-</span> <span class="nf">startApp</span><span class="p">(</span><span class="s">&#34;myapp&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># The console remains available</span>
</span></span><span class="line"><span class="cl"><span class="n">handle</span><span class="o">$</span><span class="nf">status</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] &#34;running&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">handle</span><span class="o">$</span><span class="nf">url</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] &#34;http://127.0.0.1:7365&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Stop the app</span>
</span></span><span class="line"><span class="cl"><span class="n">handle</span><span class="o">$</span><span class="nf">stop</span><span class="p">()</span></span></span></code></pre></div></div>
<p>The returned <code>ShinyAppHandle</code> has <code>stop()</code>, <code>status()</code>, <code>url()</code>, and <code>result()</code> methods. Starting a new app automatically stops the previous one, so iterating is as simple as calling <code>startApp()</code> again.</p>
<p>This is handy for interactive development, but it really shines for anything that needs to drive an app <em>and</em> keep working: testing tools, coding agents, or scripts that launch an app, interact with it, and shut it down.</p>
<p>That&rsquo;s the tooling around your app. The next two features are about the app itself &mdash; starting with a new way to keep UI off screen until someone asks for it.</p>
<h2 id="offcanvas-panels">Offcanvas panels
</h2>
<p>An offcanvas is a panel that slides in from an edge of the viewport &mdash; perfect for settings, filters, details-on-demand, or anything else that doesn&rsquo;t need to be on screen all the time. It&rsquo;s built on <a href="https://getbootstrap.com/docs/5.3/components/offcanvas/" target="_blank" rel="noopener">Bootstrap 5&rsquo;s offcanvas component</a> and comes with the full set of server verbs: <code>show_offcanvas()</code>, <code>hide_offcanvas()</code>, and <code>toggle_offcanvas()</code>.</p>
<p>The simplest way to use one is with a <code>trigger</code> element &mdash; no server code required:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-2" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-2-1">R</a></li>
<li><a href="#tabset-2-2">Python</a></li>
</ul>
<div id="tabset-2-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">shiny</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">bslib</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="nf">page_fluid</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="nf">offcanvas</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;Panel content goes here.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">title</span> <span class="o">=</span> <span class="s">&#34;Settings&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">trigger</span> <span class="o">=</span> <span class="nf">actionButton</span><span class="p">(</span><span class="s">&#34;open&#34;</span><span class="p">,</span> <span class="s">&#34;Open settings&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
</div>
<div id="tabset-2-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny.express</span> <span class="kn">import</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ui</span><span class="o">.</span><span class="n">offcanvas</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Panel content goes here.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">title</span><span class="o">=</span><span class="s2">&#34;Settings&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">trigger</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">input_action_button</span><span class="p">(</span><span class="s2">&#34;open&#34;</span><span class="p">,</span> <span class="s2">&#34;Open settings&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
</div>
</div>
<figure>
<img src="https://opensource.posit.co/blog/2026-08-04_shiny-r-1-14-python-1-7/offcanvas-trigger.png" data-fig-alt="A Shiny app with an open offcanvas panel titled Settings sliding in from the right, dimming the page behind it" data-fig-align="center" alt="Clicking the trigger button reveals the offcanvas panel" />
<figcaption aria-hidden="true">Clicking the trigger button reveals the offcanvas panel</figcaption>
</figure>
<p>Give the panel an <code>id</code> and it becomes fully programmable: control it from the server and reactively respond to whether it&rsquo;s open.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-3" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-3-1">R</a></li>
<li><a href="#tabset-3-2">Python</a></li>
</ul>
<div id="tabset-3-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">shiny</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">bslib</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="nf">page_fluid</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="nf">actionButton</span><span class="p">(</span><span class="s">&#34;toggle&#34;</span><span class="p">,</span> <span class="s">&#34;Toggle details&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="nf">offcanvas</span><span class="p">(</span><span class="s">&#34;Panel content&#34;</span><span class="p">,</span> <span class="n">title</span> <span class="o">=</span> <span class="s">&#34;Details&#34;</span><span class="p">,</span> <span class="n">id</span> <span class="o">=</span> <span class="s">&#34;details&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nf">observeEvent</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">toggle</span><span class="p">,</span> <span class="nf">toggle_offcanvas</span><span class="p">(</span><span class="s">&#34;details&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nf">observeEvent</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">details</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">message</span><span class="p">(</span><span class="s">&#34;Panel is open: &#34;</span><span class="p">,</span> <span class="n">input</span><span class="o">$</span><span class="n">details</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span></span></span></code></pre></div></div>
</div>
<div id="tabset-3-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny</span> <span class="kn">import</span> <span class="n">reactive</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny.express</span> <span class="kn">import</span> <span class="nb">input</span><span class="p">,</span> <span class="n">render</span><span class="p">,</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ui</span><span class="o">.</span><span class="n">input_action_button</span><span class="p">(</span><span class="s2">&#34;toggle&#34;</span><span class="p">,</span> <span class="s2">&#34;Toggle details&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ui</span><span class="o">.</span><span class="n">offcanvas</span><span class="p">(</span><span class="s2">&#34;Panel content&#34;</span><span class="p">,</span> <span class="n">title</span><span class="o">=</span><span class="s2">&#34;Details&#34;</span><span class="p">,</span> <span class="nb">id</span><span class="o">=</span><span class="s2">&#34;details&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@reactive.effect</span>
</span></span><span class="line"><span class="cl"><span class="nd">@reactive.event</span><span class="p">(</span><span class="nb">input</span><span class="o">.</span><span class="n">toggle</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">_</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">toggle_offcanvas</span><span class="p">(</span><span class="s2">&#34;details&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@render.text</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">state</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;Panel is </span><span class="si">{</span><span class="s1">&#39;open&#39;</span> <span class="k">if</span> <span class="nb">input</span><span class="o">.</span><span class="n">details</span><span class="p">()</span> <span class="k">else</span> <span class="s1">&#39;closed&#39;</span><span class="si">}</span><span class="s2">&#34;</span></span></span></code></pre></div></div>
</div>
</div>
<p>Panels can slide in from the <code>left</code>, <code>right</code>, <code>top</code>, or <code>bottom</code>, and you can even build one entirely in the server and reveal it with <code>show_offcanvas()</code> &mdash; no UI placement needed.</p>
<p>Offcanvas panels are available now in Shiny for Python v1.7 and in <a href="https://rstudio.github.io/bslib/" target="_blank" rel="noopener">bslib</a> v0.12 for R. See the <code>offcanvas()</code> (<a href="https://rstudio.github.io/bslib/reference/offcanvas.html" target="_blank" rel="noopener">R</a>, <a href="https://shiny.posit.co/py/api/express/express.ui.offcanvas.html" target="_blank" rel="noopener">Python</a>) reference for the panel itself, and <code>show_offcanvas()</code>, <code>hide_offcanvas()</code>, and <code>toggle_offcanvas()</code> (<a href="https://rstudio.github.io/bslib/reference/show_offcanvas.html" target="_blank" rel="noopener">R</a>, <a href="https://shiny.posit.co/py/api/express/express.ui.show_offcanvas.html" target="_blank" rel="noopener">Python</a>) for controlling it from the server.</p>
<h2 id="module-cleanup">Module cleanup
</h2>
<p>An offcanvas hides UI that&rsquo;s already there. Creating and destroying UI on the fly is a different problem, and modules make it easy to <em>add</em> UI and server logic dynamically. Removing them has always been the awkward part: <a href="https://shiny.posit.co/r/reference/shiny/1.14.0/insertUI.html" target="_blank" rel="noopener"><code>removeUI()</code></a> takes the HTML away, but the module&rsquo;s observers, reactive values, and outputs keep running behind the scenes &mdash; leaving behind &ldquo;dangling reactivity&rdquo;.</p>
<p>The session&rsquo;s new <code>destroy()</code> method (<a href="https://shiny.posit.co/r/reference/shiny/1.14.0/session.html" target="_blank" rel="noopener">R</a>, <a href="https://shiny.posit.co/py/api/core/Session.html" target="_blank" rel="noopener">Python</a>) closes that gap. The parent that inserted a module can now clean it up by the same id it used to insert it &mdash; no cleanup handles to pass around:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-4" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-4-1">R</a></li>
<li><a href="#tabset-4-2">Python</a></li>
</ul>
<div id="tabset-4-1">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># In the parent server</span>
</span></span><span class="line"><span class="cl"><span class="nf">observeEvent</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">add</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nf">insertUI</span><span class="p">(</span><span class="s">&#34;#container&#34;</span><span class="p">,</span> <span class="n">ui</span> <span class="o">=</span> <span class="nf">myModuleUI</span><span class="p">(</span><span class="s">&#34;editor&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="nf">myModuleServer</span><span class="p">(</span><span class="s">&#34;editor&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">observeEvent</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">remove</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nf">removeUI</span><span class="p">(</span><span class="n">selector</span> <span class="o">=</span> <span class="s">&#34;#editor&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">session</span><span class="o">$</span><span class="nf">destroy</span><span class="p">(</span><span class="s">&#34;editor&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div>
</div>
<div id="tabset-4-2">
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nd">@reactive.effect</span>
</span></span><span class="line"><span class="cl"><span class="nd">@reactive.event</span><span class="p">(</span><span class="nb">input</span><span class="o">.</span><span class="n">add</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">_</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">insert_ui</span><span class="p">(</span><span class="n">my_module_ui</span><span class="p">(</span><span class="s2">&#34;editor&#34;</span><span class="p">),</span> <span class="n">selector</span><span class="o">=</span><span class="s2">&#34;#container&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">my_module_server</span><span class="p">(</span><span class="s2">&#34;editor&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@reactive.effect</span>
</span></span><span class="line"><span class="cl"><span class="nd">@reactive.event</span><span class="p">(</span><span class="nb">input</span><span class="o">.</span><span class="n">remove</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">_</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">remove_ui</span><span class="p">(</span><span class="n">selector</span><span class="o">=</span><span class="s2">&#34;#editor&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="n">session</span><span class="o">.</span><span class="n">destroy</span><span class="p">(</span><span class="s2">&#34;editor&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
</div>
</div>
<p>Destroying a scope invokes all of its registered <a href="https://shiny.posit.co/r/reference/shiny/1.14.0/session.html" target="_blank" rel="noopener"><code>onDestroy()</code></a> (R) / <a href="https://shiny.posit.co/py/api/core/Session.html" target="_blank" rel="noopener"><code>on_destroy()</code></a> (Python) callbacks, cleaning up reactive values, expressions, observers, inputs, and outputs for that module <em>and</em> its descendant modules. Everything is scoped, so the parent session and sibling modules are untouched. And a module can call <code>destroy()</code> on its own session (no id) to clean up after itself.</p>
<p>If your app inserts and removes modules over a long-lived session, <code>destroy()</code> keeps those removed modules from accumulating as memory and reactivity leaks.</p>
<h2 id="test-mode">Test mode
</h2>
<p><em>Shiny for Python only.</em></p>
<p>Back to agents for a moment. The bundled skills teach them how to <em>write</em> your app; test mode, also new in v1.7, lets them &mdash; and your tests &mdash; <em>see inside</em> it while it runs. Enable it with the <code>SHINY_TESTMODE=1</code> environment variable (or <code>App(test_mode=True)</code>), and each session serves a live JSON snapshot of its <code>input</code>, <code>output</code>, and exported values.</p>
<p>The snapshot is only served when test mode is enabled, and by default it includes only inputs and outputs. To surface an internal reactive &mdash; a <code>reactive.calc</code> or <code>reactive.value</code> that never reaches the UI &mdash; export it with <a href="https://shiny.posit.co/py/api/core/testmode.export_test_values.html" target="_blank" rel="noopener"><code>export_test_values()</code></a>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny</span> <span class="kn">import</span> <span class="n">reactive</span><span class="p">,</span> <span class="n">render</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny.express</span> <span class="kn">import</span> <span class="nb">input</span><span class="p">,</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny.testmode</span> <span class="kn">import</span> <span class="n">export_test_values</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ui</span><span class="o">.</span><span class="n">input_slider</span><span class="p">(</span><span class="s2">&#34;n&#34;</span><span class="p">,</span> <span class="s2">&#34;N&#34;</span><span class="p">,</span> <span class="nb">min</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="nb">max</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="mi">20</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@reactive.calc</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">doubled</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">input</span><span class="o">.</span><span class="n">n</span><span class="p">()</span> <span class="o">*</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@render.text</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">txt</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;n * 2 = </span><span class="si">{</span><span class="n">doubled</span><span class="p">()</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Surface the internal reactive in the test-mode snapshot. This is a no-op</span>
</span></span><span class="line"><span class="cl"><span class="c1"># unless test mode is enabled, so it&#39;s safe to leave in production code.</span>
</span></span><span class="line"><span class="cl"><span class="n">export_test_values</span><span class="p">(</span><span class="n">doubled</span><span class="o">=</span><span class="n">doubled</span><span class="p">)</span></span></span></code></pre></div></div>
<p>The pytest app fixtures (<code>local_app</code>, <code>create_app_fixture</code>) enable test mode automatically, and the new <a href="https://shiny.posit.co/py/api/testing/playwright.controller.AppTestValues.html" target="_blank" rel="noopener"><code>shiny.playwright.controller.AppTestValues</code></a> controller reads the snapshot in end-to-end tests. Expectations accept exact values or predicates:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">is_even</span><span class="p">(</span><span class="n">value</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">value</span> <span class="o">%</span> <span class="mi">2</span> <span class="o">==</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app_values</span> <span class="o">=</span> <span class="n">controller</span><span class="o">.</span><span class="n">AppTestValues</span><span class="p">(</span><span class="n">page</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">app_values</span><span class="o">.</span><span class="n">expect_export</span><span class="p">(</span><span class="s2">&#34;doubled&#34;</span><span class="p">,</span> <span class="mi">40</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">app_values</span><span class="o">.</span><span class="n">expect_export</span><span class="p">(</span><span class="s2">&#34;doubled&#34;</span><span class="p">,</span> <span class="n">is_even</span><span class="p">)</span></span></span></code></pre></div></div>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Scrubbing volatile values</span>
</div>
<div class="callout-body">
<p>Need to remove timestamps or temp paths before they are written to the snapshot? Register a preprocessor with <a href="https://shiny.posit.co/py/api/core/testmode.snapshot_preprocess_input.html" target="_blank" rel="noopener"><code>input.set_snapshot_preprocess()</code></a> or <code>my_output.snapshot_preprocess()</code>, and the snapshot stays stable from run to run.</p>
</div>
</div>
<p>Test mode mirrors R&rsquo;s long-standing <a href="https://shiny.posit.co/r/reference/shiny/1.14.0/exportTestValues.html" target="_blank" rel="noopener"><code>exportTestValues()</code></a> &mdash; and it gives coding agents a structured way to debug a running app directly from the server instead of inferring information from the UI.</p>
<p>New to testing Shiny apps? Start with <a href="https://shiny.posit.co/py/docs/unit-testing.html" target="_blank" rel="noopener">Unit testing</a> and <a href="https://shiny.posit.co/py/docs/end-to-end-testing.html" target="_blank" rel="noopener">End-to-end testing</a> on the Shiny for Python website, then browse the <a href="https://shiny.posit.co/py/api/testing/index.html" target="_blank" rel="noopener">testing API reference</a>.</p>
<h2 id="other-improvements">Other improvements
</h2>
<p>A few more changes worth a quick mention &mdash; the full lists are in the release notes for <a href="https://shiny.posit.co/r/reference/shiny/1.14.0/upgrade.html" target="_blank" rel="noopener">Shiny for R v1.14.0</a>, <a href="https://rstudio.github.io/bslib/news/index.html#bslib-0120" target="_blank" rel="noopener">bslib v0.12.0</a>, and <a href="https://github.com/posit-dev/py-shiny/blob/main/CHANGELOG.md" target="_blank" rel="noopener">Shiny for Python v1.7.0</a>:</p>
<h3 id="r">R
</h3>
<ul>
<li><a href="https://shiny.posit.co/r/reference/shiny/1.14.0/downloadButton.html" target="_blank" rel="noopener"><code>downloadButton()</code></a> and <a href="https://shiny.posit.co/r/reference/shiny/1.14.0/downloadButton.html" target="_blank" rel="noopener"><code>downloadLink()</code></a> gain an <code>enabled</code> argument. The default, <code>&quot;auto&quot;</code>, automatically enables the button once the download is ready.</li>
<li>Output resize and visibility detection now uses native browser observers (<code>ResizeObserver</code>, <code>IntersectionObserver</code>), so plot sizing and hidden-state tracking work in any layout &mdash; including CSS-only show/hide and non-Bootstrap frameworks.</li>
<li><a href="https://shiny.posit.co/r/reference/shiny/1.14.0/conditionalPanel.html" target="_blank" rel="noopener"><code>conditionalPanel()</code></a> no longer briefly flashes its contents on app start when the condition is initially <code>FALSE</code>.</li>
<li>bslib v0.12 also lets you grab the sidebar handle directly to resize a sidebar, and fixes the resize handle indicator for <code>position = &quot;right&quot;</code> sidebars.</li>
</ul>
<h3 id="python">Python
</h3>
<ul>
<li><a href="https://shiny.posit.co/py/api/express/express.render.download_button.html" target="_blank" rel="noopener"><code>@render.download_button</code></a> and <a href="https://shiny.posit.co/py/api/express/express.render.download_link.html" target="_blank" rel="noopener"><code>@render.download_link</code></a> pair 1:1 with <code>ui.download_button()</code> and <code>ui.download_link()</code>, replacing the now-deprecated <code>@render.download</code>.</li>
<li>The <code>shiny[otel]</code> extra now sets up OpenTelemetry zero-code auto-instrumentation out of the box: <code>opentelemetry-instrument shiny run app.py</code>.</li>
</ul>
<h2 id="in-closing">In closing
</h2>
<p>We&rsquo;re excited to see what you build (and clean up) with these releases. As always, if you have questions or feedback, <a href="https://discord.gg/yMGCamUMnS" target="_blank" rel="noopener">join us on Discord</a> or open an issue on <a href="https://github.com/rstudio/shiny/issues/new" target="_blank" rel="noopener">rstudio/shiny</a> or <a href="https://github.com/posit-dev/py-shiny/issues/new" target="_blank" rel="noopener">posit-dev/py-shiny</a>. Happy Shiny-ing!</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thank you to all the folks who helped make these releases happen:</p>
<h4 id="shiny-for-r-v1140">Shiny for R <a href="https://shiny.posit.co/r/reference/shiny/1.14.0/upgrade.html" target="_blank" rel="noopener">v1.14.0</a>
</h4>
<p><a href="https://github.com/adit-0132" target="_blank" rel="noopener">@adit-0132</a>, <a href="https://github.com/ahnungslos-git" target="_blank" rel="noopener">@ahnungslos-git</a>, <a href="https://github.com/byronvickers" target="_blank" rel="noopener">@byronvickers</a>, <a href="https://github.com/cpsievert" target="_blank" rel="noopener">@cpsievert</a>, <a href="https://github.com/cuckooland" target="_blank" rel="noopener">@cuckooland</a>, <a href="https://github.com/djacob65" target="_blank" rel="noopener">@djacob65</a>, <a href="https://github.com/dmurdoch" target="_blank" rel="noopener">@dmurdoch</a>, <a href="https://github.com/elnelson575" target="_blank" rel="noopener">@elnelson575</a>, <a href="https://github.com/FBrockmeyer" target="_blank" rel="noopener">@FBrockmeyer</a>, <a href="https://github.com/gadenbuie" target="_blank" rel="noopener">@gadenbuie</a>, <a href="https://github.com/HenrikBengtsson" target="_blank" rel="noopener">@HenrikBengtsson</a>, <a href="https://github.com/IvanM26" target="_blank" rel="noopener">@IvanM26</a>, <a href="https://github.com/jcheng5" target="_blank" rel="noopener">@jcheng5</a>, <a href="https://github.com/jeffkeller-einc" target="_blank" rel="noopener">@jeffkeller-einc</a>, <a href="https://github.com/jeis4wpi" target="_blank" rel="noopener">@jeis4wpi</a>, <a href="https://github.com/JohnCoene" target="_blank" rel="noopener">@JohnCoene</a>, <a href="https://github.com/JosiahParry" target="_blank" rel="noopener">@JosiahParry</a>, <a href="https://github.com/karangattu" target="_blank" rel="noopener">@karangattu</a>, <a href="https://github.com/klin333" target="_blank" rel="noopener">@klin333</a>, <a href="https://github.com/lachlansimpson" target="_blank" rel="noopener">@lachlansimpson</a>, <a href="https://github.com/lionel-" target="_blank" rel="noopener">@lionel-</a>, <a href="https://github.com/marcosnav" target="_blank" rel="noopener">@marcosnav</a>, <a href="https://github.com/mconflitti-pbc" target="_blank" rel="noopener">@mconflitti-pbc</a>, <a href="https://github.com/ml-ebs-ext" target="_blank" rel="noopener">@ml-ebs-ext</a>, <a href="https://github.com/nbenn" target="_blank" rel="noopener">@nbenn</a>, <a href="https://github.com/Noskario" target="_blank" rel="noopener">@Noskario</a>, <a href="https://github.com/prinjos" target="_blank" rel="noopener">@prinjos</a>, <a href="https://github.com/pyltime" target="_blank" rel="noopener">@pyltime</a>, <a href="https://github.com/rikivillalba" target="_blank" rel="noopener">@rikivillalba</a>, <a href="https://github.com/roberson4627-cpu" target="_blank" rel="noopener">@roberson4627-cpu</a>, <a href="https://github.com/samuelbharti" target="_blank" rel="noopener">@samuelbharti</a>, <a href="https://github.com/schloerke" target="_blank" rel="noopener">@schloerke</a>, <a href="https://github.com/shikokuchuo" target="_blank" rel="noopener">@shikokuchuo</a>, <a href="https://github.com/simon-smart88" target="_blank" rel="noopener">@simon-smart88</a>, <a href="https://github.com/toph-allen" target="_blank" rel="noopener">@toph-allen</a>, and <a href="https://github.com/Upipa" target="_blank" rel="noopener">@Upipa</a>.</p>
<h4 id="bslib-v0120">bslib <a href="https://rstudio.github.io/bslib/news/index.html#bslib-0120" target="_blank" rel="noopener">v0.12.0</a>
</h4>
<p><a href="https://github.com/AleKoure" target="_blank" rel="noopener">@AleKoure</a>, <a href="https://github.com/ArthurAndrews" target="_blank" rel="noopener">@ArthurAndrews</a>, <a href="https://github.com/averissimo" target="_blank" rel="noopener">@averissimo</a>, <a href="https://github.com/elnelson575" target="_blank" rel="noopener">@elnelson575</a>, <a href="https://github.com/etiennebacher" target="_blank" rel="noopener">@etiennebacher</a>, <a href="https://github.com/gadenbuie" target="_blank" rel="noopener">@gadenbuie</a>, <a href="https://github.com/jeis4wpi" target="_blank" rel="noopener">@jeis4wpi</a>, <a href="https://github.com/LeonidasZhak" target="_blank" rel="noopener">@LeonidasZhak</a>, <a href="https://github.com/lgaborini" target="_blank" rel="noopener">@lgaborini</a>, <a href="https://github.com/lgschuck" target="_blank" rel="noopener">@lgschuck</a>, <a href="https://github.com/sawelch-NIVA" target="_blank" rel="noopener">@sawelch-NIVA</a>, <a href="https://github.com/sims1253" target="_blank" rel="noopener">@sims1253</a>, and <a href="https://github.com/willgearty" target="_blank" rel="noopener">@willgearty</a>.</p>
<h4 id="shiny-for-python-v170">Shiny for Python <a href="https://github.com/posit-dev/py-shiny/blob/main/CHANGELOG.md" target="_blank" rel="noopener">v1.7.0</a>
</h4>
<p><a href="https://github.com/chernojagne" target="_blank" rel="noopener">@chernojagne</a>, <a href="https://github.com/cpsievert" target="_blank" rel="noopener">@cpsievert</a>, <a href="https://github.com/eeshsaxena" target="_blank" rel="noopener">@eeshsaxena</a>, <a href="https://github.com/elnelson575" target="_blank" rel="noopener">@elnelson575</a>, <a href="https://github.com/EltonChang1" target="_blank" rel="noopener">@EltonChang1</a>, <a href="https://github.com/FBruzzesi" target="_blank" rel="noopener">@FBruzzesi</a>, <a href="https://github.com/gadenbuie" target="_blank" rel="noopener">@gadenbuie</a>, <a href="https://github.com/hutch3232" target="_blank" rel="noopener">@hutch3232</a>, <a href="https://github.com/JosiahParry" target="_blank" rel="noopener">@JosiahParry</a>, <a href="https://github.com/karangattu" target="_blank" rel="noopener">@karangattu</a>, <a href="https://github.com/kb071216" target="_blank" rel="noopener">@kb071216</a>, <a href="https://github.com/kbzsl" target="_blank" rel="noopener">@kbzsl</a>, <a href="https://github.com/marcosnav" target="_blank" rel="noopener">@marcosnav</a>, <a href="https://github.com/mariameraz" target="_blank" rel="noopener">@mariameraz</a>, <a href="https://github.com/MukundaKatta" target="_blank" rel="noopener">@MukundaKatta</a>, <a href="https://github.com/mvanhorn" target="_blank" rel="noopener">@mvanhorn</a>, <a href="https://github.com/nightcityblade" target="_blank" rel="noopener">@nightcityblade</a>, <a href="https://github.com/pevolution-ahmed" target="_blank" rel="noopener">@pevolution-ahmed</a>, <a href="https://github.com/QuintonBaker-USDA" target="_blank" rel="noopener">@QuintonBaker-USDA</a>, <a href="https://github.com/saisharan0103" target="_blank" rel="noopener">@saisharan0103</a>, <a href="https://github.com/SamEdwardes" target="_blank" rel="noopener">@SamEdwardes</a>, <a href="https://github.com/schloerke" target="_blank" rel="noopener">@schloerke</a>, <a href="https://github.com/slupczynskim" target="_blank" rel="noopener">@slupczynskim</a>, <a href="https://github.com/Steven314" target="_blank" rel="noopener">@Steven314</a>, and <a href="https://github.com/tomaioo" target="_blank" rel="noopener">@tomaioo</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-08-04_shiny-r-1-14-python-1-7/feature.gif" length="449496" type="image/gif" />
    </item>
    <item>
      <title>Quarto 1.10</title>
      <link>https://opensource.posit.co/blog/2026-08-03_quarto-1-10/</link>
      <pubDate>Mon, 03 Aug 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-08-03_quarto-1-10/</guid>
      <dc:creator>Charlotte Wickham</dc:creator><description><![CDATA[<p>Quarto 1.10 is out! You can get the current release from the <a href="https://quarto.org/docs/download/index.html" target="_blank" rel="noopener">download page</a>.</p>
<p>Quarto 1.10 is dominated by fixes, as much of our development effort is now going into <a href="https://opensource.posit.co/blog/2026-04-06_whats-next-quarto-2">Quarto 2</a>, but there are some improvements we wanted to point out: more refined HTML accessibility checks, and access to Quarto&rsquo;s localized strings for template authors.</p>
<p>You can read about these improvements and the most important fixes below. You can find all the changes in this version in the <a href="https://quarto.org/docs/download/changelog/1.10/" target="_blank" rel="noopener">Release Notes</a>.</p>
<h2 id="accessibility-checking-improvements">Accessibility Checking Improvements
</h2>
<p>Since Quarto 1.8, setting the <code>axe</code> option on HTML documents runs <a href="https://github.com/dequelabs/axe-core" target="_blank" rel="noopener">axe-core</a> accessibility checks on your rendered document. Quarto 1.10 makes these checks easier to rely on.</p>
<p><strong>Checking works offline.</strong> Quarto now bundles its own copy of axe-core instead of loading it from a CDN in the reader&rsquo;s browser. Accessibility checking works without a network connection, and viewing a rendered document no longer triggers a request to an external CDN. The axe-core version is unchanged, so results are identical.</p>
<p><strong>Check against a WCAG conformance level.</strong> The new <code>standard</code> option scopes checks to a WCAG conformance level, including rules axe-core keeps off by default for that level. The <code>best-practice</code> option controls whether axe&rsquo;s best-practice rules, recommendations not required by any WCAG success criterion, are also checked:</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-0">
  <div class="code-with-filename-label" id="code-filename-0"><span class="font-mono text-sm">document.qmd</span></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">format</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">html</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">axe</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">standard</span><span class="p">:</span><span class="w"> </span><span class="l">wcag21aa</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">best-practice</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div></div>
<p><strong>A clearer report.</strong> When using <code>output: document</code>, violations are now listed first by impact, then WCAG conformance level. The report overlay also received its own accessibility and styling fixes: it can be scrolled with the keyboard, uses theme and brand independent colors, and no longer inherits page styling like text centering.</p>
<p>Learn more at <a href="https://quarto.org/docs/output-formats/html-accessibility.html" target="_blank" rel="noopener">HTML Accessibility</a>.</p>
<h2 id="localized-strings-in-templates">Localized Strings in Templates
</h2>
<p>If you write custom Pandoc templates or <a href="https://quarto.org/docs/journals/templates.html#template-partials" target="_blank" rel="noopener">template partials</a>, you can now use Quarto&rsquo;s localized strings through the new <code>quarto.language</code> template variable namespace. Quarto resolves each string for the document&rsquo;s language and exposes it as a template variable:</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-1">
  <div class="code-with-filename-label" id="code-filename-1"><span class="font-mono text-sm">template.typ</span></div><pre tabindex="0"><code class="language-default" data-lang="default">$quarto.language.toc-title-document$
$quarto.language.crossref-fig-title$</code></pre></div>
<p>This lets templates honor the document&rsquo;s <code>lang</code> option without hard-coding text. For example, the <code>orange-book</code> Typst book extension now uses these variables in its running headers, so a book with <code>lang: fr</code> gets <em>Chapitre</em> rather than <em>Chapter</em>.</p>
<p>Learn more at <a href="https://quarto.org/docs/authoring/language.html#localized-strings-in-templates" target="_blank" rel="noopener">Localized Strings in Templates</a>.</p>
<h2 id="other-important-fixes">Other Important Fixes
</h2>
<ul>
<li>
<p><code>quarto preview</code> reliability&mdash;a long-standing bug that showed stale HTML for non-index pages is fixed, changes to a document&rsquo;s <code>format:</code> are detected on the first render after the edit, a <code>_brand.yml</code> added or removed during preview is picked up, and transient <code>.quarto_ipynb</code> files no longer accumulate on disk.</p>
</li>
<li>
<p><a href="https://quarto.org/docs/extensions/shortcodes.html" target="_blank" rel="noopener">Shortcodes</a>&mdash;shortcodes are now resolved inside inline and display math expressions.</p>
</li>
<li>
<p><a href="https://quarto.org/docs/output-formats/pdf-basics.html#fonts" target="_blank" rel="noopener">PDF font fallbacks</a>&mdash;<code>mainfontfallback</code>, <code>sansfontfallback</code>, and <code>monofontfallback</code> no longer crash LuaLaTeX on TeX Live 2026, and a missing fallback font is now installed automatically.</p>
</li>
<li>
<p>Typst fonts&mdash;fonts not installed locally are filtered from CSS <code>font-family</code> fallback lists before they reach Typst, suppressing <code>unknown font family</code> warnings, alongside a batch of fixes to Quarto&rsquo;s CSS-to-Typst translation.</p>
</li>
<li>
<p><a href="https://opensource.posit.co/blog/2026-04-14_chrome-headless-shell">Chrome Headless Shell</a>&mdash;the <code>quarto install chromium</code> command, deprecated in 1.9, now transparently redirects to <code>chrome-headless-shell</code>, <code>quarto check install</code> warns about legacy Chromium installs, and arm64 Linux is now supported.</p>
</li>
<li>
<p><a href="https://quarto.org/docs/websites/website-llms.html" target="_blank" rel="noopener">Output for LLMs</a>&mdash;fixes for websites with <code>llms-txt</code> enabled: headings and shortcodes inside conditional content, and clean-URL rewriting of <code>index.html.md</code> links.</p>
</li>
</ul>
<p>Dependency updates:</p>
<ul>
<li><code>pandoc</code> updated to 3.10</li>
<li><code>typst</code> updated to 0.15.1</li>
<li><code>deno</code> updated to 2.7.14</li>
<li><code>dart-sass</code> updated to 1.101.0</li>
<li><code>esbuild</code> updated to 0.28.1</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>We&rsquo;d like to say a huge thank you to everyone who contributed to this release by opening issues and pull requests:</p>
<p><a href="https://github.com/AlexLietard" target="_blank" rel="noopener">AlexLietard</a>,
<a href="https://github.com/ArthurRudolph" target="_blank" rel="noopener">ArthurRudolph</a>,
<a href="https://github.com/Artmann" target="_blank" rel="noopener">Artmann</a>,
<a href="https://github.com/C-Monaghan" target="_blank" rel="noopener">C-Monaghan</a>,
<a href="https://github.com/ChrisJefferson" target="_blank" rel="noopener">ChrisJefferson</a>,
<a href="https://github.com/CoryMcCartan" target="_blank" rel="noopener">CoryMcCartan</a>,
<a href="https://github.com/DCEW" target="_blank" rel="noopener">DCEW</a>,
<a href="https://github.com/DrFloLinke" target="_blank" rel="noopener">DrFloLinke</a>,
<a href="https://github.com/Guest-1013" target="_blank" rel="noopener">Guest-1013</a>,
<a href="https://github.com/MBe-iUS" target="_blank" rel="noopener">MBe-iUS</a>,
<a href="https://github.com/MurzNN" target="_blank" rel="noopener">MurzNN</a>,
<a href="https://github.com/ThierryO" target="_blank" rel="noopener">ThierryO</a>,
<a href="https://github.com/ThomasFaria" target="_blank" rel="noopener">ThomasFaria</a>,
<a href="https://github.com/TinasheMTapera" target="_blank" rel="noopener">TinasheMTapera</a>,
<a href="https://github.com/Voorhoeve" target="_blank" rel="noopener">Voorhoeve</a>,
<a href="https://github.com/alderete" target="_blank" rel="noopener">alderete</a>,
<a href="https://github.com/andrewheiss" target="_blank" rel="noopener">andrewheiss</a>,
<a href="https://github.com/barendgehrels" target="_blank" rel="noopener">barendgehrels</a>,
<a href="https://github.com/basm92" target="_blank" rel="noopener">basm92</a>,
<a href="https://github.com/blauzo" target="_blank" rel="noopener">blauzo</a>,
<a href="https://github.com/chainsawriot" target="_blank" rel="noopener">chainsawriot</a>,
<a href="https://github.com/crisbour" target="_blank" rel="noopener">crisbour</a>,
<a href="https://github.com/cs-res-pub-ser-st" target="_blank" rel="noopener">cs-res-pub-ser-st</a>,
<a href="https://github.com/dustinstoltz" target="_blank" rel="noopener">dustinstoltz</a>,
<a href="https://github.com/eculler" target="_blank" rel="noopener">eculler</a>,
<a href="https://github.com/eneveu" target="_blank" rel="noopener">eneveu</a>,
<a href="https://github.com/apps/github-actions" target="_blank" rel="noopener">github-actions[bot]</a>,
<a href="https://github.com/gregswinehart" target="_blank" rel="noopener">gregswinehart</a>,
<a href="https://github.com/hwine" target="_blank" rel="noopener">hwine</a>,
<a href="https://github.com/ianpittwood" target="_blank" rel="noopener">ianpittwood</a>,
<a href="https://github.com/ihrke" target="_blank" rel="noopener">ihrke</a>,
<a href="https://github.com/jdonaldson" target="_blank" rel="noopener">jdonaldson</a>,
<a href="https://github.com/jiangyun-fun" target="_blank" rel="noopener">jiangyun-fun</a>,
<a href="https://github.com/jidanni" target="_blank" rel="noopener">jidanni</a>,
<a href="https://github.com/jkrumbiegel" target="_blank" rel="noopener">jkrumbiegel</a>,
<a href="https://github.com/jnkatz" target="_blank" rel="noopener">jnkatz</a>,
<a href="https://github.com/jph00" target="_blank" rel="noopener">jph00</a>,
<a href="https://github.com/jtkulas" target="_blank" rel="noopener">jtkulas</a>,
<a href="https://github.com/juleswg23" target="_blank" rel="noopener">juleswg23</a>,
<a href="https://github.com/juliohm" target="_blank" rel="noopener">juliohm</a>,
<a href="https://github.com/kazuyanagimoto" target="_blank" rel="noopener">kazuyanagimoto</a>,
<a href="https://github.com/kelli-rstudio" target="_blank" rel="noopener">kelli-rstudio</a>,
<a href="https://github.com/lsbjordao" target="_blank" rel="noopener">lsbjordao</a>,
<a href="https://github.com/luismmontilla" target="_blank" rel="noopener">luismmontilla</a>,
<a href="https://github.com/maelle" target="_blank" rel="noopener">maelle</a>,
<a href="https://github.com/maucejo" target="_blank" rel="noopener">maucejo</a>,
<a href="https://github.com/memeplex" target="_blank" rel="noopener">memeplex</a>,
<a href="https://github.com/micedre" target="_blank" rel="noopener">micedre</a>,
<a href="https://github.com/multimeric" target="_blank" rel="noopener">multimeric</a>,
<a href="https://github.com/nathant181" target="_blank" rel="noopener">nathant181</a>,
<a href="https://github.com/neilernst" target="_blank" rel="noopener">neilernst</a>,
<a href="https://github.com/nessan" target="_blank" rel="noopener">nessan</a>,
<a href="https://github.com/nickvigilante" target="_blank" rel="noopener">nickvigilante</a>,
<a href="https://github.com/nrennie" target="_blank" rel="noopener">nrennie</a>,
<a href="https://github.com/pbosetti" target="_blank" rel="noopener">pbosetti</a>,
<a href="https://github.com/reckoner" target="_blank" rel="noopener">reckoner</a>,
<a href="https://github.com/rgouveiamendes" target="_blank" rel="noopener">rgouveiamendes</a>,
<a href="https://github.com/robjhyndman" target="_blank" rel="noopener">robjhyndman</a>,
<a href="https://github.com/sbwiecko" target="_blank" rel="noopener">sbwiecko</a>,
<a href="https://github.com/seandavi" target="_blank" rel="noopener">seandavi</a>,
<a href="https://github.com/sebastiansauer" target="_blank" rel="noopener">sebastiansauer</a>,
<a href="https://github.com/skyfroger" target="_blank" rel="noopener">skyfroger</a>,
<a href="https://github.com/songwupei" target="_blank" rel="noopener">songwupei</a>,
<a href="https://github.com/stefkuypers" target="_blank" rel="noopener">stefkuypers</a>,
<a href="https://github.com/stragu" target="_blank" rel="noopener">stragu</a>,
<a href="https://github.com/tiagojct" target="_blank" rel="noopener">tiagojct</a>,
<a href="https://github.com/victorrssx" target="_blank" rel="noopener">victorrssx</a>,
<a href="https://github.com/widlarizer" target="_blank" rel="noopener">widlarizer</a>,
<a href="https://github.com/wlatendresse" target="_blank" rel="noopener">wlatendresse</a>,
<a href="https://github.com/xuefeng-xu" target="_blank" rel="noopener">xuefeng-xu</a>,
<a href="https://github.com/yasyf" target="_blank" rel="noopener">yasyf</a>,
<a href="https://github.com/zinc75" target="_blank" rel="noopener">zinc75</a>.</p>
<p>The hammer and wrench emoji in the <a href="thumbnail.png">listing and social card image</a> for this post comes from <a href="https://openmoji.org/" class="external">OpenMoji</a>&ndash; the open-source emoji and icon project. License: <a href="https://creativecommons.org/licenses/by-sa/4.0/#" class="external">CC BY-SA 4.0</a></p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-08-03_quarto-1-10/thumbnail.png" length="48234" type="image/png" />
    </item>
    <item>
      <title>Positron&#39;s Jupyter Notebook Editor Is Now Generally Available</title>
      <link>https://opensource.posit.co/blog/2026-07-29_positron-jupyter-notebook-editor-ga/</link>
      <pubDate>Wed, 29 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-07-29_positron-jupyter-notebook-editor-ga/</guid>
      <dc:creator>Wasim Lorgat</dc:creator>
      <dc:creator>Cindy Tong</dc:creator><description><![CDATA[<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Note</span>
</div>
<div class="callout-body">
<p><a href="https://positron.posit.co" target="_blank" rel="noopener">Positron</a> is Posit&rsquo;s new, next-generation IDE for data science. Positron is designed to be an extensible, polyglot tool for exploring data and reproducible authoring in Python, R, and more.</p>
</div>
</div>
<p>Data scientists are often forced to choose between the simplicity of the classic Jupyter notebook experience and the advanced capabilities of a general-purpose IDE. With the <a href="https://opensource.posit.co/blog/2026-07-13_positron-2026-07-release/" target="_blank" rel="noopener">2026.07 release</a>, you no longer have to choose: Positron now delivers a first-class Notebook Editor inside an IDE built for data science.</p>
<p>If you&rsquo;re new to Positron, here&rsquo;s what the Notebook Editor is like in your daily work.</p>
<h2 id="environment-management">Environment management
</h2>
<p>You clone your colleague&rsquo;s repo and try to rerun their notebook. Which Python version do you use? What dependencies do you install and which versions? Did you previously set up an environment that you could reuse? You&rsquo;re faced with all of these questions before you&rsquo;ve run a single line of code.</p>
<p>Positron has several <a href="https://opensource.posit.co/blog/2026-07-08_positron-uv/" target="_blank" rel="noopener">built-in workflows</a> to help you manage your environments. When you open a repo, Positron discovers your installed environments, suggests the right one for the repo, and prompts you to set up a new environment if needed. The active environment is displayed front and center; click to restart or switch environments. Positron uses the same environment across your Jupyter and Quarto notebooks, scripts, and consoles. You can see inside your environments, which packages are installed, their versions, whether newer versions are available, and you can upgrade them. Environments are first-class citizens of Positron. <a href="https://positron.posit.co/positron-notebook-editor.html#setting-up-your-environment" target="_blank" rel="noopener">Explore Positron&rsquo;s environment management</a>.</p>
<img src="https://opensource.posit.co/blog/2026-07-29_positron-jupyter-notebook-editor-ga/notebook-from-git.gif" data-fig-align="center" data-fig-alt="Positron helps you resolve environment and package dependencies as you pull down a colleague&#39;s notebook" />
<h2 id="interactive-data-exploration">Interactive data exploration
</h2>
<p>When you run a cell in your notebook, your variables appear in the <a href="https://positron.posit.co/variables-pane.html" target="_blank" rel="noopener">Variables Pane</a>. Filter, sort, and search your data in the <a href="https://positron.posit.co/data-explorer.html" target="_blank" rel="noopener">Data Explorer</a>, and try out plot variations in the Visualize wizard. You can easily convert your point-and-click interactions into code by using the copy-code feature to bring them back into your notebook. If you get stuck, look up library documentation in the <a href="https://positron.posit.co/help-pane.html" target="_blank" rel="noopener">Help Pane</a>. All of this works out of the box.</p>
<img src="https://opensource.posit.co/blog/2026-07-29_positron-jupyter-notebook-editor-ga/notebook-data-exploration.gif" data-fig-align="center" data-fig-alt="Run a notebook and see your variables update live, inspect them in the data explorer and convert filters back to code." />
<h2 id="streamlined-version-control">Streamlined version control
</h2>
<p>Notebooks store outputs and execution metadata alongside code, which is great for sharing but does not work well with version control. Positron includes settings to exclude outputs and execution metadata from the saved file, so the diff is just your code change. <a href="https://positron.posit.co/positron-notebook-editor.html#version-control" target="_blank" rel="noopener">Explore the custom settings</a> to clean up your git diffs.</p>
<p>Your notebooks stay ordinary <code>.ipynb</code> files and the editing experience stays classic Jupyter. When a plain-text workflow suits you better, conversion to and from <code>.ipynb</code> and other formats is built in.</p>
<img src="https://opensource.posit.co/blog/2026-07-29_positron-jupyter-notebook-editor-ga/notebook-save-settings.png" data-fig-align="center" data-fig-alt="Customize what is saved in your notebooks for better version control." />
<h2 id="ai-assistance">AI assistance
</h2>
<p>Posit Assistant sees your notebook as more than just text. It works with the live session behind the notebook: your variables, your data, your plots. Ask it to fix a chart and it can inspect the dataframe, look at the plot itself, edit the cell, and run it again. It does the same things you would do, using the same panes you use. <a href="https://positron.posit.co/positron-notebook-editor.html#ai-integration" target="_blank" rel="noopener">Explore notebook-aware AI assistance in Positron</a>.</p>
<img src="https://opensource.posit.co/blog/2026-07-29_positron-jupyter-notebook-editor-ga/notebook-ai.gif" data-fig-align="center" data-fig-alt="Assistant can fix, explain, run your notebook and suggest next steps." />
<h2 id="get-started">Get started
</h2>
<p><a href="https://positron.posit.co/download" target="_blank" rel="noopener">Download Positron</a> and open a project with a Jupyter notebook. The Notebook Editor is now on by default. If you&rsquo;re coming from another IDE, your existing <code>.ipynb</code> files will open as-is without any conversion needed.</p>
<p>For the full feature reference, check out our <a href="https://positron.posit.co" target="_blank" rel="noopener">documentation</a>.</p>
<p>If you run into any issues or have ideas on how we can improve Positron, reach out on <a href="https://github.com/posit-dev/positron" target="_blank" rel="noopener">GitHub</a>.</p>
<p>Already using Positron? Everything in this release is in the <a href="https://positron.posit.co/release-notes" target="_blank" rel="noopener">release notes</a>.</p>
<p>Thank you so much for helping us build Positron&rsquo;s Jupyter notebook support from the ground up by chatting with us live, sharing your pain points, and giving us feedback on GitHub!</p>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Tip</span>
</div>
<div class="callout-body">
<p><a href="https://positron.posit.co/download" target="_blank" rel="noopener">Download Positron</a> to try out the Notebook Editor and other new features!</p>
</div>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-07-29_positron-jupyter-notebook-editor-ga/featured.png" length="686743" type="image/png" />
    </item>
  </channel>
</rss>
