<?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>R on Posit Open Source</title>
    <link>https://opensource.posit.co/languages/r/</link>
    <description>Recent content in R on Posit Open Source</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Wed, 30 Sep 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://opensource.posit.co/languages/r/index.xml" rel="self" type="application/rss+xml" />
    <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>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[<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>ellmer 0.5.0</title>
      <link>https://opensource.posit.co/blog/2026-09-14_ellmer-0-5-0/</link>
      <pubDate>Mon, 14 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-09-14_ellmer-0-5-0/</guid>
      <dc:creator>Nic Crane</dc:creator><description><![CDATA[<p>We are happy to announce that <a href="https://ellmer.tidyverse.org" target="_blank" rel="noopener">ellmer</a> 0.5.0 is now available on CRAN! ellmer is an R package that makes it easy to work with large language models directly from R. It supports a wide variety of providers (including OpenAI, Anthropic, Google, AWS Bedrock, Azure, Snowflake, Databricks, Posit, and many more), makes it easy to extract structured data, and lets the model call R functions via tool calling.</p>
<p>You can install the latest version from CRAN with</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;ellmer&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>This blog post covers the major changes in this release: a lifecycle update, updates to how you can work with files with ellmer, new ways to update model price data, returning citations when using web search tools, and new hooks for developers building on ellmer&rsquo;s tool loop.</p>
<p>The full list of changes can be found in the <a href="https://github.com/tidyverse/ellmer/releases/tag/v0.5.0" target="_blank" rel="noopener">release notes</a>.</p>
<h2 id="lifecycle">Lifecycle
</h2>
<p><code>chat_github()</code> and <code>models_github()</code> are now defunct, since GitHub Models has been retired.</p>
<p>We&rsquo;ve tightened up what a tool can return: a string, an atomic vector, a JSON string, or a <code>Content</code> object. Returning anything else, like a data frame or a list, now gives a deprecation warning. Previously ellmer converted these to JSON for you, but any problem with the conversion surfaced long after your function had finished, and it was easy to forget that the model can only read the result, not compute with it. For a data frame, convert it yourself:</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">get_weather</span> <span class="o">&lt;-</span> <span class="nf">tool</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="kr">function</span><span class="p">(</span><span class="n">cities</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">df</span> <span class="o">&lt;-</span> <span class="nf">weather_api</span><span class="p">(</span><span class="n">cities</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">jsonlite</span><span class="o">::</span><span class="nf">toJSON</span><span class="p">(</span><span class="n">df</span><span class="p">,</span> <span class="n">dataframe</span> <span class="o">=</span> <span class="s">&#34;columns&#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="kc">...</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<h2 id="new-features">New features
</h2>
<h3 id="sending-files-to-the-model">Sending files to the model
</h3>
<p>There are now two ways to give the model a file. New <code>content_document_file()</code> and <code>content_document_url()</code> send text-based documents like CSV, Markdown, and code files, just as <code>content_pdf_file()</code> and <code>content_image_file()</code> already do for PDFs and images. The contents go inline with your message, so this works with every provider:</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">penguins</span> <span class="o">&lt;-</span> <span class="nf">tempfile</span><span class="p">(</span><span class="n">fileext</span> <span class="o">=</span> <span class="s">&#34;.csv&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">readr</span><span class="o">::</span><span class="nf">write_csv</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="nf">data.frame</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">penguin</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;Waddlesworth&#34;</span><span class="p">,</span> <span class="s">&#34;Flipper McGee&#34;</span><span class="p">,</span> <span class="s">&#34;Turbo Tuxedo&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;Captain Blubber&#34;</span><span class="p">,</span> <span class="s">&#34;Sir Slidesalot&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">race_time_seconds</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="m">43.2</span><span class="p">,</span> <span class="m">38.7</span><span class="p">,</span> <span class="m">31.9</span><span class="p">,</span> <span class="m">45.1</span><span class="p">,</span> <span class="m">36.4</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">penguins</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">&lt;-</span> <span class="nf">chat_anthropic</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 class="s">&#34;Who won the race?&#34;</span><span class="p">,</span> <span class="nf">content_document_file</span><span class="p">(</span><span class="n">penguins</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Based on the race times, **Turbo Tuxedo** won the race with the fastest time of</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; **31.9 seconds**.</span></span></span></code></pre></div></div>
<p>Inline contents are re-sent with every turn, which adds up over a long conversation, especially with a large file. For those cases, <code>chat$file_upload()</code> sends the file to the provider once and returns a reference you pass to <code>$chat()</code> instead. Because the file isn&rsquo;t re-sent with every message, this also reduces your token usage and costs:</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_google_gemini</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">race</span> <span class="o">&lt;-</span> <span class="n">chat</span><span class="o">$</span><span class="nf">file_upload</span><span class="p">(</span><span class="n">penguins</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 class="s">&#34;Who won the race?&#34;</span><span class="p">,</span> <span class="n">race</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; **Turbo Tuxedo** won the race with the fastest time of **31.9 seconds**.</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 class="s">&#34;And who came last?&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; **Captain Blubber** came in last with the slowest time of **45.1 seconds**.</span></span></span></code></pre></div></div>
<p>You can manage your uploads with <code>chat$file_list()</code>, <code>$file_get()</code>, <code>$file_download()</code>, and <code>$file_delete()</code>. File management works with <code>chat_openai()</code>, <code>chat_anthropic()</code>, and <code>chat_google_gemini()</code>, and replaces the now-deprecated <code>claude_file_upload()</code> and <code>google_upload()</code>.</p>
<h3 id="citations">Citations
</h3>
<p>When a model answers using a built-in web search or fetch tool, the provider usually reports which sources back the answer. ellmer 0.5.0 captures citations from Claude, Google, and OpenAI.</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_anthropic</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">claude_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 class="s">&#34;What are the current stable versions of R and Python? Look them up, one line each, no commentary.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; R: R version 4.6.1 (Happy Hop) has been released on 2026-06-24[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; Python: Python 3.14.7 / 5 August 2026[2]</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] R: The R Project for Statistical Computing: https://www.r-project.org/</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [2] Python (programming language):</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; https://en.wikipedia.org/wiki/Python_(programming_language)</span></span></span></code></pre></div></div>
<p>Citations are also kept in the chat history and included in streamed output, so apps built on ellmer can display them as well.</p>
<h3 id="counting-tokens-and-keeping-prices-current">Counting tokens and keeping prices current
</h3>
<p>Two additions make it easier to know what a conversation will cost before you commit to it. <code>Chat$token_count()</code> asks the provider how many tokens some input would use, without actually sending it to the model.</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_anthropic</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="s">&#34;Tell me a joke about an R programmer&#34;</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 class="c1">#&gt; [1] 19</span></span></span></code></pre></div></div>
<p>Token counting is currently supported by <code>chat_anthropic()</code>, <code>chat_openai()</code>, <code>chat_google_gemini()</code>, <code>chat_google_vertex()</code>, and <code>chat_posit()</code>.</p>
<p>Providers change their prices more often than we release ellmer. You can now update in between releases with <code>models_update_prices()</code>, which downloads the latest pricing data from GitHub and caches it locally.</p>
<h2 id="other-improvements">Other improvements
</h2>
<ul>
<li>Default models have been updated across providers: <code>chat_anthropic()</code>, <code>chat_aws_bedrock()</code>, <code>chat_databricks()</code>, <code>chat_posit()</code>, and <code>chat_snowflake()</code> now use Claude Sonnet 5; <code>chat_openai()</code> and <code>chat_openrouter()</code> use GPT 5.6 Terra; and <code>chat_google_gemini()</code> and <code>chat_google_vertex()</code> use Gemini 3.7 Flash. We update the default models regularly, so if you&rsquo;d prefer to pin your code to a specific model, you should specify it using the <code>model</code> parameter.</li>
<li><code>chat_aws_bedrock()</code> now supports Bedrock Mantle, the newer endpoint that serves models like Claude Mythos and the GPT-5 family through the Anthropic Messages and OpenAI Responses APIs, rather than only the Converse API. ellmer picks the right API from the model name, so this should just work. If you&rsquo;re using a model it doesn&rsquo;t recognize, you can set the new <code>api</code> argument yourself.</li>
<li>You can now stream structured output. <code>Chat$stream()</code> and <code>$stream_async()</code> gain a <code>type</code> argument, which works the same way as in <code>$chat_structured()</code>, for providers that support it.</li>
</ul>
<h3 id="developer-updates">Developer updates
</h3>
<p>Two new features help developers building on ellmer&rsquo;s tool loop.</p>
<p>Inside a tool, <code>tool_context()</code> returns the request that triggered it and the conversation so far, so a tool can make decisions, not just the model. Here, a query tool stops after three calls:</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">run_query</span> <span class="o">&lt;-</span> <span class="nf">tool</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="kr">function</span><span class="p">(</span><span class="n">sql</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># count_tool_results() is a stand-in for your own helper</span>
</span></span><span class="line"><span class="cl">    <span class="kr">if</span> <span class="p">(</span><span class="nf">count_tool_results</span><span class="p">(</span><span class="nf">tool_context</span><span class="p">()</span><span class="o">$</span><span class="n">turns</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="m">3</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nf">tool_reject</span><span class="p">(</span><span class="s">&#34;Query budget used up. Answer with what you have.&#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="n">jsonlite</span><span class="o">::</span><span class="nf">toJSON</span><span class="p">(</span><span class="n">DBI</span><span class="o">::</span><span class="nf">dbGetQuery</span><span class="p">(</span><span class="n">con</span><span class="p">,</span> <span class="n">sql</span><span class="p">),</span> <span class="n">dataframe</span> <span class="o">=</span> <span class="s">&#34;columns&#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="n">name</span> <span class="o">=</span> <span class="s">&#34;run_query&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Run a SQL query&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">arguments</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span><span class="n">sql</span> <span class="o">=</span> <span class="nf">type_string</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p><code>Chat</code> also gains <code>$on_request_start()</code> and <code>$on_request_end()</code>, which fire before and after every request to the model, including each round of the tool loop. For example, to time each request:</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_anthropic</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="n">run_query</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">started</span> <span class="o">&lt;-</span> <span class="kc">NULL</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">$</span><span class="nf">on_request_start</span><span class="p">(</span><span class="nf">\</span><span class="p">(</span><span class="n">turns</span><span class="p">)</span> <span class="n">started</span> <span class="o">&lt;&lt;-</span> <span class="nf">Sys.time</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">on_request_end</span><span class="p">(</span><span class="nf">\</span><span class="p">(</span><span class="n">turn</span><span class="p">)</span> <span class="nf">message</span><span class="p">(</span><span class="s">&#34;Request took &#34;</span><span class="p">,</span> <span class="nf">round</span><span class="p">(</span><span class="nf">Sys.time</span><span class="p">()</span> <span class="o">-</span> <span class="n">started</span><span class="p">,</span> <span class="m">1</span><span class="p">),</span> <span class="s">&#34;s&#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">chat</span><span class="o">$</span><span class="nf">chat</span><span class="p">(</span><span class="s">&#34;Find the mean of every column in mtcars, one query at a time.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Request took 3.6s</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ◯ [tool call] run_query(sql = &#34;SELECT * FROM mtcars LIMIT 5&#34;)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ● #&gt; [{&#34;mpg&#34;:21,&#34;cyl&#34;:6,&#34;disp&#34;:160,&#34;hp&#34;:110,&#34;drat&#34;:3.9,&#34;wt&#34;:2.62,&#34;qsec&#34;:16.46,…</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Now I&#39;ll compute the mean of each column one query at a time, as requested.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Request took 2.7s</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ◯ [tool call] run_query(sql = &#34;SELECT AVG(mpg) AS mean_mpg FROM mtcars&#34;)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ● #&gt; [{&#34;mean_mpg&#34;:20.0906}]</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Request took 1.9s</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ◯ [tool call] run_query(sql = &#34;SELECT AVG(cyl) AS mean_cyl FROM mtcars&#34;)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ● #&gt; [{&#34;mean_cyl&#34;:6.1875}]</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Request took 2s</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ◯ [tool call] run_query(sql = &#34;SELECT AVG(disp) AS mean_disp FROM mtcars&#34;)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ■ #&gt; Error: Tool call rejected. Query budget used up. Answer with what you</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; have.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; It looks like the query budget has been used up, so I can only report the means</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; I was able to compute before being cut off:</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; | Column | Mean |</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; | mpg | 20.0906 |</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; | cyl | 6.1875 |</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Request took 3.4s</span></span></span></code></pre></div></div>
<p><code>$on_request_start()</code> also receives the turns about to be sent, so an agent can compact its history with <code>chat$set_turns()</code> before the context window fills up.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thanks to the 73 people who helped make this release possible by filing issues, contributing code, and asking questions: <a href="https://github.com/1beb" target="_blank" rel="noopener">@1beb</a>, <a href="https://github.com/abiyug" target="_blank" rel="noopener">@abiyug</a>, <a href="https://github.com/aclink88" target="_blank" rel="noopener">@aclink88</a>, <a href="https://github.com/AdaemmerP" target="_blank" rel="noopener">@AdaemmerP</a>, <a href="https://github.com/alesanGreat" target="_blank" rel="noopener">@alesanGreat</a>, <a href="https://github.com/Apollo7777777" target="_blank" rel="noopener">@Apollo7777777</a>, <a href="https://github.com/arnavchauhan7" target="_blank" rel="noopener">@arnavchauhan7</a>, <a href="https://github.com/arunrajes" target="_blank" rel="noopener">@arunrajes</a>, <a href="https://github.com/atheriel" target="_blank" rel="noopener">@atheriel</a>, <a href="https://github.com/awunderground" target="_blank" rel="noopener">@awunderground</a>, <a href="https://github.com/bakaburg1" target="_blank" rel="noopener">@bakaburg1</a>, <a href="https://github.com/bastianolea" target="_blank" rel="noopener">@bastianolea</a>, <a href="https://github.com/bshor" target="_blank" rel="noopener">@bshor</a>, <a href="https://github.com/cerebrixos" target="_blank" rel="noopener">@cerebrixos</a>, <a href="https://github.com/CoryMcCartan" target="_blank" rel="noopener">@CoryMcCartan</a>, <a href="https://github.com/cpsievert" target="_blank" rel="noopener">@cpsievert</a>, <a href="https://github.com/D-M4rk" target="_blank" rel="noopener">@D-M4rk</a>, <a href="https://github.com/dareneiri" target="_blank" rel="noopener">@dareneiri</a>, <a href="https://github.com/debruine" target="_blank" rel="noopener">@debruine</a>, <a href="https://github.com/diegomsg" target="_blank" rel="noopener">@diegomsg</a>, <a href="https://github.com/diegoperoni" target="_blank" rel="noopener">@diegoperoni</a>, <a href="https://github.com/dipterix" target="_blank" rel="noopener">@dipterix</a>, <a href="https://github.com/earthcli" target="_blank" rel="noopener">@earthcli</a>, <a href="https://github.com/etiennebacher" target="_blank" rel="noopener">@etiennebacher</a>, <a href="https://github.com/feddelegrand7" target="_blank" rel="noopener">@feddelegrand7</a>, <a href="https://github.com/FrancescoMonti-source" target="_blank" rel="noopener">@FrancescoMonti-source</a>, <a href="https://github.com/frankiethull" target="_blank" rel="noopener">@frankiethull</a>, <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/hectorgray" target="_blank" rel="noopener">@hectorgray</a>, <a href="https://github.com/hopessugar" target="_blank" rel="noopener">@hopessugar</a>, <a href="https://github.com/hswerdfe" target="_blank" rel="noopener">@hswerdfe</a>, <a href="https://github.com/JamesHWade" target="_blank" rel="noopener">@JamesHWade</a>, <a href="https://github.com/jamesinottawa" target="_blank" rel="noopener">@jamesinottawa</a>, <a href="https://github.com/jcheng5" target="_blank" rel="noopener">@jcheng5</a>, <a href="https://github.com/jcrodriguez1989" target="_blank" rel="noopener">@jcrodriguez1989</a>, <a href="https://github.com/jeroenjanssens" target="_blank" rel="noopener">@jeroenjanssens</a>, <a href="https://github.com/JosiahParry" target="_blank" rel="noopener">@JosiahParry</a>, <a href="https://github.com/jrosell" target="_blank" rel="noopener">@jrosell</a>, <a href="https://github.com/kaipingyang" target="_blank" rel="noopener">@kaipingyang</a>, <a href="https://github.com/karawoo" target="_blank" rel="noopener">@karawoo</a>, <a href="https://github.com/kbenoit" target="_blank" rel="noopener">@kbenoit</a>, <a href="https://github.com/kchou496" target="_blank" rel="noopener">@kchou496</a>, <a href="https://github.com/klin333" target="_blank" rel="noopener">@klin333</a>, <a href="https://github.com/kolabearafk" target="_blank" rel="noopener">@kolabearafk</a>, <a href="https://github.com/ksr-zguo" target="_blank" rel="noopener">@ksr-zguo</a>, <a href="https://github.com/lazasaurus-ai" target="_blank" rel="noopener">@lazasaurus-ai</a>, <a href="https://github.com/lionel-" target="_blank" rel="noopener">@lionel-</a>, <a href="https://github.com/MLiedgens" target="_blank" rel="noopener">@MLiedgens</a>, <a href="https://github.com/n8layman" target="_blank" rel="noopener">@n8layman</a>, <a href="https://github.com/nbenn" target="_blank" rel="noopener">@nbenn</a>, <a href="https://github.com/neil-bray" target="_blank" rel="noopener">@neil-bray</a>, <a href="https://github.com/nrineausanofi" target="_blank" rel="noopener">@nrineausanofi</a>, <a href="https://github.com/ntentes" target="_blank" rel="noopener">@ntentes</a>, <a href="https://github.com/omorante" target="_blank" rel="noopener">@omorante</a>, <a href="https://github.com/petzi53" target="_blank" rel="noopener">@petzi53</a>, <a href="https://github.com/rajabzadehalidip" target="_blank" rel="noopener">@rajabzadehalidip</a>, <a href="https://github.com/rempsyc" target="_blank" rel="noopener">@rempsyc</a>, <a href="https://github.com/Sade154" target="_blank" rel="noopener">@Sade154</a>, <a href="https://github.com/sarahsdao" target="_blank" rel="noopener">@sarahsdao</a>, <a href="https://github.com/scjohannes" target="_blank" rel="noopener">@scjohannes</a>, <a href="https://github.com/simonpcouch" target="_blank" rel="noopener">@simonpcouch</a>, <a href="https://github.com/Sirhubi007" target="_blank" rel="noopener">@Sirhubi007</a>, <a href="https://github.com/SokolovAnatoliy" target="_blank" rel="noopener">@SokolovAnatoliy</a>, <a href="https://github.com/sounkou-bioinfo" target="_blank" rel="noopener">@sounkou-bioinfo</a>, <a href="https://github.com/stefanlinner" target="_blank" rel="noopener">@stefanlinner</a>, <a href="https://github.com/t-kalinowski" target="_blank" rel="noopener">@t-kalinowski</a>, <a href="https://github.com/Tazinho" target="_blank" rel="noopener">@Tazinho</a>, <a href="https://github.com/thisisnic" target="_blank" rel="noopener">@thisisnic</a>, <a href="https://github.com/thoov08" target="_blank" rel="noopener">@thoov08</a>, <a href="https://github.com/trangdata" target="_blank" rel="noopener">@trangdata</a>, <a href="https://github.com/WvdH-Novus3" target="_blank" rel="noopener">@WvdH-Novus3</a>, and <a href="https://github.com/xmarquez" target="_blank" rel="noopener">@xmarquez</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-09-14_ellmer-0-5-0/featured.jpg" length="385161" type="image/jpeg" />
    </item>
    <item>
      <title>orbital 0.7.0</title>
      <link>https://opensource.posit.co/blog/2026-09-09_orbital-0-7-0/</link>
      <pubDate>Wed, 09 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-09-09_orbital-0-7-0/</guid>
      <dc:creator>Emil Hvitfeldt</dc:creator><description><![CDATA[<p>We&rsquo;re happy to announce the release of <a href="https://orbital.tidymodels.org/" target="_blank" rel="noopener">R-orbital</a> 0.7.0.
orbital turns a fitted tidymodels workflow into the set of equations that produce its predictions,
so you can run those predictions in a database instead of moving the data to R.
It uses <a href="https://tidypredict.tidymodels.org/" target="_blank" rel="noopener">tidypredict</a> under the hood to translate fitted models.
This post also covers the release of tidypredict 1.2.0.</p>
<p>This post is about the R package.
There is also a <a href="https://posit-dev.github.io/orbital/" target="_blank" rel="noopener">Python version of orbital</a> that works on scikit-learn pipelines.</p>
<p>You can install both from CRAN with:</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="nf">c</span><span class="p">(</span><span class="s">&#34;orbital&#34;</span><span class="p">,</span> <span class="s">&#34;tidypredict&#34;</span><span class="p">))</span></span></span></code></pre></div></div>
<p>This post covers the highlights of these releases.
You can see the full list of changes in the <a href="https://orbital.tidymodels.org/news/index.html" target="_blank" rel="noopener">orbital release notes</a> and the <a href="https://tidypredict.tidymodels.org/news/index.html" target="_blank" rel="noopener">tidypredict release notes</a>.</p>
<h2 id="a-lot-more-models">A lot more models
</h2>
<p>Previously orbital and tidypredict supported a handful of useful models.
This included linear models, decision trees, random forests, and boosted trees models.
This release adds discriminant analysis, naive Bayes, neural networks, support vector machines, partial least squares,
and several more rule and ensemble methods.
This fills in the gaps of most of the known models that can be made to work with orbital.</p>
<p>Newly supported for regression:</p>
<ul>
<li><code>bart(engine = &quot;dbarts&quot;)</code></li>
<li><code>boost_tree(engine = &quot;h2o_gbm&quot;)</code></li>
<li><code>linear_reg(engine = &quot;glm&quot;)</code></li>
<li><code>mlp(engine = &quot;nnet&quot;)</code></li>
<li><code>null_model()</code></li>
<li><code>pls(engine = &quot;mixOmics&quot;)</code></li>
<li><code>rand_forest(engine = &quot;aorsf&quot;)</code></li>
<li><code>rand_forest(engine = &quot;partykit&quot;)</code></li>
<li><code>rule_fit(engine = &quot;h2o&quot;)</code></li>
<li><code>svm_linear(engine = &quot;kernlab&quot;)</code></li>
<li><code>svm_linear(engine = &quot;LiblineaR&quot;)</code></li>
</ul>
<p>Newly supported for classification:</p>
<ul>
<li><code>bag_tree(engine = &quot;rpart&quot;)</code> and <code>bag_tree(engine = &quot;C5.0&quot;)</code></li>
<li><code>boost_tree(engine = &quot;C5.0&quot;)</code></li>
<li><code>boost_tree(engine = &quot;h2o_gbm&quot;)</code></li>
<li><code>C5_rules(engine = &quot;C5.0&quot;)</code></li>
<li><code>decision_tree(engine = &quot;C5.0&quot;)</code></li>
<li><code>discrim_linear(engine = &quot;MASS&quot;)</code>, <code>discrim_quad(engine = &quot;MASS&quot;)</code></li>
<li><code>discrim_linear()</code> with the <code>&quot;mda&quot;</code>, <code>&quot;sda&quot;</code>, and <code>&quot;sparsediscrim&quot;</code> engines</li>
<li><code>logistic_reg(engine = &quot;LiblineaR&quot;)</code></li>
<li><code>mlp(engine = &quot;nnet&quot;)</code></li>
<li><code>multinom_reg(engine = &quot;nnet&quot;)</code></li>
<li><code>naive_Bayes(engine = &quot;klaR&quot;)</code> and <code>naive_Bayes(engine = &quot;naivebayes&quot;)</code></li>
<li><code>null_model()</code></li>
<li><code>pls(engine = &quot;mixOmics&quot;)</code></li>
<li><code>rule_fit(engine = &quot;h2o&quot;)</code></li>
<li><code>rule_fit(engine = &quot;xrf&quot;)</code></li>
<li><code>svm_linear(engine = &quot;kernlab&quot;)</code></li>
<li><code>svm_linear(engine = &quot;LiblineaR&quot;)</code></li>
</ul>
<p>One thing worth noting for models with &ldquo;h2o&rdquo; in their engine value, is that you need a running H2O cluster to build the orbital object.
Prediction and SQL generation from the orbital object stays dependency free.</p>
<p>Here is an example of linear discriminant analysis model,
which was not supported before this release,
fit on the penguins data and then run in DuckDB.</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">library</span><span class="p">(</span><span class="n">tidymodels</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">discrim</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">orbital</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">duckdb</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">data</span><span class="p">(</span><span class="n">penguins</span><span class="p">,</span> <span class="n">package</span> <span class="o">=</span> <span class="s">&#34;modeldata&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">penguins</span> <span class="o">&lt;-</span> <span class="n">tidyr</span><span class="o">::</span><span class="nf">drop_na</span><span class="p">(</span><span class="n">penguins</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">rec_spec</span> <span class="o">&lt;-</span> <span class="nf">recipe</span><span class="p">(</span><span class="n">species</span> <span class="o">~</span> <span class="n">.,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">penguins</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_dummy</span><span class="p">(</span><span class="nf">all_nominal_predictors</span><span class="p">())</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_normalize</span><span class="p">(</span><span class="nf">all_numeric_predictors</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">mod_spec</span> <span class="o">&lt;-</span> <span class="nf">discrim_linear</span><span class="p">(</span><span class="n">engine</span> <span class="o">=</span> <span class="s">&#34;MASS&#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">wf_spec</span> <span class="o">&lt;-</span> <span class="nf">workflow</span><span class="p">(</span><span class="n">rec_spec</span><span class="p">,</span> <span class="n">mod_spec</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">fit</span><span class="p">(</span><span class="n">penguins</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">orb</span> <span class="o">&lt;-</span> <span class="nf">orbital</span><span class="p">(</span><span class="n">wf_spec</span><span class="p">,</span> <span class="n">type</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="s">&#34;class&#34;</span><span class="p">,</span> <span class="s">&#34;prob&#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">con</span> <span class="o">&lt;-</span> <span class="n">DBI</span><span class="o">::</span><span class="nf">dbConnect</span><span class="p">(</span><span class="n">duckdb</span><span class="o">::</span><span class="nf">duckdb</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="n">penguins_db</span> <span class="o">&lt;-</span> <span class="n">dplyr</span><span class="o">::</span><span class="nf">copy_to</span><span class="p">(</span><span class="n">con</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></span><span class="line"><span class="cl"><span class="nf">predict</span><span class="p">(</span><span class="n">orb</span><span class="p">,</span> <span class="n">penguins_db</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code># A query:  ?? x 4
# Database: DuckDB 1.5.5 [root@Darwin 25.6.0:R 4.6.1/:memory:]
   .pred_class .pred_Adelie .pred_Chinstrap .pred_Gentoo
   &lt;chr&gt;              &lt;dbl&gt;           &lt;dbl&gt;        &lt;dbl&gt;
 1 Adelie             1.000        6.43e- 9     4.64e-25
 2 Adelie             1.000        5.25e- 6     8.81e-16
 3 Adelie             1.000        1.97e- 4     2.61e-17
 4 Adelie             1.000        2.99e- 8     1.93e-23
 5 Adelie             1.000        8.34e- 9     4.42e-29
 6 Adelie             1.000        1.60e- 6     4.14e-20
 7 Adelie             1.000        6.03e-10     1.45e-18
 8 Adelie             0.999        6.47e- 4     4.16e-20
 9 Adelie             1.000        9.56e-10     1.97e-30
10 Adelie             1.000        6.73e-14     9.13e-27
# ℹ more rows
</code></pre>
<p>We have documented the <a href="https://orbital.tidymodels.org/articles/supported-models.html" target="_blank" rel="noopener">list of supported models</a>.
If there is a model you need that is still missing,
<a href="https://github.com/tidymodels/tidypredict/issues" target="_blank" rel="noopener">let us know</a> so we can prioritize it.</p>
<h2 id="when-a-model-has-no-probability">When a model has no probability
</h2>
<p>While we added these new methods we ran into a problem we didn&rsquo;t have before.
For many classification models you get both predicted probabilities and hard class predictions.
This release has added models where that is not the case.
A number of newly added models only produce hard class predictions.</p>
<p>Instead of trying to invent class probabilities,
we produce an informative error for the affected models.</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">c5_wf</span> <span class="o">&lt;-</span> <span class="nf">workflow</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">species</span> <span class="o">~</span> <span class="n">.,</span>
</span></span><span class="line"><span class="cl">  <span class="nf">decision_tree</span><span class="p">(</span><span class="n">mode</span> <span class="o">=</span> <span class="s">&#34;classification&#34;</span><span class="p">,</span> <span class="n">engine</span> <span class="o">=</span> <span class="s">&#34;C5.0&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">fit</span><span class="p">(</span><span class="n">penguins</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">orbital</span><span class="p">(</span><span class="n">c5_wf</span><span class="p">,</span> <span class="n">type</span> <span class="o">=</span> <span class="s">&#34;prob&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code>Error in `orbital()`:
! &quot;prob&quot; predictions are not available for this model.
ℹ It predicts a class directly, with no probability behind it.
ℹ Use `type = &quot;class&quot;` instead.
</code></pre>
<h2 id="tidypredict-is-now-a-toolkit-not-just-a-function">tidypredict is now a toolkit, not just a function
</h2>
<p>We have talked a lot about orbital so far.
This is because we think that it is the ideal interface compared to tidypredict,
if your goal is to generate SQL expressions for a fitted model or workflow.</p>
<p>orbital and tidypredict work together to generate the expression that you need.
We have expanded tidypredict with a number of generics,
which are all developer focused.
With the goal that packages other than orbital can benefit from the work we have done.
Three of them describe what a model&rsquo;s fitted expressions compute:</p>
<ul>
<li><code>tidypredict_output_type()</code> returns <code>&quot;numeric&quot;</code>, <code>&quot;prob&quot;</code>, <code>&quot;decision&quot;</code>, or <code>&quot;class&quot;</code></li>
<li><code>tidypredict_outcome_levels()</code> returns the outcome levels in model order</li>
<li><code>tidypredict_normalized()</code> reports whether per-level probabilities already sum to one</li>
</ul>
<p>Below we see two of the generics in action.</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">library</span><span class="p">(</span><span class="n">tidypredict</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">svm_fit</span> <span class="o">&lt;-</span> <span class="nf">svm_linear</span><span class="p">(</span><span class="n">mode</span> <span class="o">=</span> <span class="s">&#34;classification&#34;</span><span class="p">,</span> <span class="n">engine</span> <span class="o">=</span> <span class="s">&#34;LiblineaR&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">fit</span><span class="p">(</span><span class="n">sex</span> <span class="o">~</span> <span class="n">bill_length_mm</span> <span class="o">+</span> <span class="n">body_mass_g</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">penguins</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">tidypredict_output_type</span><span class="p">(</span><span class="n">svm_fit</span><span class="o">$</span><span class="n">fit</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code>[1] &quot;decision&quot;
</code></pre>
<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">tidypredict_outcome_levels</span><span class="p">(</span><span class="n">svm_fit</span><span class="o">$</span><span class="n">fit</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code>[1] &quot;female&quot; &quot;male&quot;  
</code></pre>
<p>The other five expose the per-tree pieces that <code>tidypredict_fit()</code> assembles,
so a package generating its own code can split an ensemble apart and put it back together:</p>
<ul>
<li><code>tidypredict_trees()</code> returns per-tree expressions</li>
<li><code>tidypredict_n_trees()</code> returns the number of trees</li>
<li><code>tidypredict_combine_trees()</code> turns per-tree expressions back into a prediction</li>
<li><code>tidypredict_class_trees()</code> returns per-tree expressions for each outcome level</li>
<li><code>tidypredict_class_exprs()</code> returns one finished expression per outcome level</li>
</ul>
<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">rf</span> <span class="o">&lt;-</span> <span class="nf">rand_forest</span><span class="p">(</span><span class="n">mode</span> <span class="o">=</span> <span class="s">&#34;regression&#34;</span><span class="p">,</span> <span class="n">trees</span> <span class="o">=</span> <span class="m">5</span><span class="p">,</span> <span class="n">engine</span> <span class="o">=</span> <span class="s">&#34;ranger&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">fit</span><span class="p">(</span><span class="n">body_mass_g</span> <span class="o">~</span> <span class="n">bill_length_mm</span> <span class="o">+</span> <span class="n">flipper_length_mm</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">penguins</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">tidypredict_n_trees</span><span class="p">(</span><span class="n">rf</span><span class="o">$</span><span class="n">fit</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code>[1] 5
</code></pre>
<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">tidypredict_combine_trees</span><span class="p">(</span><span class="n">rf</span><span class="o">$</span><span class="n">fit</span><span class="p">,</span> <span class="n">rlang</span><span class="o">::</span><span class="nf">syms</span><span class="p">(</span><span class="nf">paste0</span><span class="p">(</span><span class="s">&#34;tree_&#34;</span><span class="p">,</span> <span class="m">1</span><span class="o">:</span><span class="m">5</span><span class="p">)))</span></span></span></code></pre></div></div>
<pre><code>(tree_1 + tree_2 + tree_3 + tree_4 + tree_5)/5
</code></pre>
<p>A lot of this logic was hardcoded inside orbital on a model by model basis,
it is now formalized in such a way that you can easily use just the bits you need.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>Many thanks to all the people who contributed to orbital and tidypredict since the last release!</p>
<p><a href="https://github.com/EmilHvitfeldt" target="_blank" rel="noopener">@EmilHvitfeldt</a>, <a href="https://github.com/jannikbx" target="_blank" rel="noopener">@jannikbx</a>, and <a href="https://github.com/RAMitchell" target="_blank" rel="noopener">@RAMitchell</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-09-09_orbital-0-7-0/featured.jpg" length="402596" type="image/jpeg" />
    </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>AI Newsletter: You probably don&#39;t want to fine-tune</title>
      <link>https://opensource.posit.co/blog/2026-09-04_ai-newsletter/</link>
      <pubDate>Fri, 04 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-09-04_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>
<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>This week, we are busy preparing for posit::conf, so we&rsquo;re running an abbreviated version of a post Simon published on his personal blog. You can read the full version <a href="https://simonpcouch.com/blog/2026-09-03-fine-tune" target="_blank" rel="noopener">here</a>.</p>
</div>
</div>
<p>Recently, there&rsquo;s been a lot of talk about fine-tuning models. The reasoning usually goes something like this:</p>
<ul>
<li>There&rsquo;s some task that needs to be done regularly.</li>
<li>Today&rsquo;s frontier models can do it quite reliably, but it&rsquo;s expensive and the bills are starting to rack up.</li>
<li>Cheaper models can&rsquo;t quite do the task reliably, but a fine-tuned variant of a small, open-weights model might be able to just as cheaply.</li>
<li>Therefore, you should fine-tune models for tasks that your organization does.</li>
</ul>
<p>I&rsquo;ve seen <a href="https://seldo.com/posts/2026-is-the-year-of-fine-tuned-small-models/" target="_blank" rel="noopener">a</a> <a href="https://fermisense.com/when-machines-take-the-wheel/" target="_blank" rel="noopener">number</a> <a href="https://neon.com/blog/how-castform-neon-beats-frontier-models-on-price-and-efficiency" target="_blank" rel="noopener">of</a> <a href="https://shopify.engineering/sidekicks-continual-learning-loop" target="_blank" rel="noopener">blog</a> <a href="https://turbopuffer.com/blog/reinforcement-learning-sid-ai" target="_blank" rel="noopener">posts</a> cited in support of this idea.</p>
<p>It makes sense that fine-tuning is appealing! AI is getting expensive, many players in the space cannot make the privacy guarantees we&rsquo;d hope, and just as you start to rely on one proprietary model, it&rsquo;s phased out in favor of a newer release. That said, my reaction is that this approach seems more engineering-intensive and, likely, more expensive than alternative approaches. I&rsquo;ll try to make that case here.</p>
<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>When I say you <em>probably</em> don&rsquo;t want to fine-tune, I mean that I totally understand there are some valid use cases here. Some of the linked blog posts are themselves valid cases. I can see fine-tuning making sense for some <em>very</em> high-volume workloads and/or for asynchronous workloads (where it doesn&rsquo;t matter if a response comes back in a second or a day).</p>
</div>
</div>
<h2 id="what-is-fine-tuning">What is fine-tuning?
</h2>
<p>Before we go further, it&rsquo;s probably worth quickly outlining what I mean by fine-tuning. In short, fine-tuning means training an existing model further to improve its performance on a particular task.</p>
<p>Large language models contain huge arrays of parameters. Input, usually in the form of text, is turned into arrays of numbers, and those arrays get multiplied a bunch of times to form the output, which is then typically converted back into text. Those multiplications are quite computationally intensive, and the array of parameters is itself quite large. For example, the smallest models that are <a href="https://simonpcouch.com/blog/2026-09-02-local-agents-3/" target="_blank" rel="noopener">beginning to be able to</a> reliably complete basic agentic work contain 8 billion parameters (in short, &ldquo;8B&rdquo;).</p>
<p>Fine-tuning changes a model&rsquo;s behavior, either by updating the existing parameters, or by training a <a href="https://huggingface.co/learn/llm-course/en/chapter11/4" target="_blank" rel="noopener">smaller set of additional parameters</a> that work alongside the originals. The goal of fine-tuning is, broadly, to increase the performance of a model on a given task while minimizing the impact on the model&rsquo;s ability to do other tasks.</p>
<h2 id="fine-tuning-is-hard">Fine-tuning is hard
</h2>
<p>It is very, very difficult to successfully fine-tune a model. I don&rsquo;t claim that it&rsquo;s impossible, but it is a substantial engineering effort, requiring the careful attention of dedicated scientists with access to today&rsquo;s frontier models across several weeks, just for the first edition of the model. To show why, let&rsquo;s revisit each step of that process.</p>
<p><strong>What model should you start with?</strong> In short, you want the smallest possible model that has the ability to learn to do the task reliably while retaining sufficient general intelligence. It is very hard to predict when or how capabilities will emerge during fine-tuning without first fine-tuning, meaning that the choice will need to be revisited several times once you&rsquo;ve made a first go at the remaining steps.</p>
<p><strong>What data gets used for training?</strong> Once you&rsquo;ve chosen a model, you need to find some training data to fine-tune it with. What should you use? Ideally, you would use the real data that represents the task and its inputs. However, in order to train on that real data, you typically need the user&rsquo;s consent, and it&rsquo;s likely that you won&rsquo;t have that consent.</p>
<p>And then, when you do have user consent and choose to train on that data, you now have a mandate not to overfit. This is because if the model you&rsquo;re fine-tuning internalizes the data you&rsquo;re training on and is able to (even hazily) recollect it, you&rsquo;ve now exposed that data to any users of the model.</p>
<p>The other approach, then, is synthetic data, created by asking a model to generate a bunch of scenarios that resemble the real inputs and then demonstrate how to carry out the task in those scenarios. But this type of synthetic data has its own problems: notably, the models creating the data have their own set of tics that make the data unrepresentative of real-world data. The <a href="https://www.404media.co/elias-thorne-chatbots-llms-chatgpt-lighthouse-keeper-story/" target="_blank" rel="noopener">Elias in the Lighthouse</a> effect is a notable example of this phenomenon. A wide variety of models will, when asked to write a story, write about a lighthouse keeper named Elias Thorne. On its own, the story of Elias Thorne might be informative training data. However, 1,000 stories about a lighthouse keeper Elias Thorne are not.</p>
<p><strong>How do you nudge the weights?</strong> Let&rsquo;s assume you do have a diverse, representative set of training data to work with. You&rsquo;ll now need to decide how, mechanistically, to change the behavior of the model&mdash;do you need a full fine-tune, or will a <a href="https://huggingface.co/learn/llm-course/en/chapter11/4" target="_blank" rel="noopener">LoRA</a> be sufficient?</p>
<p>Next, you&rsquo;ll need to tune a set of 5-10 hyperparameters. Notably, because they are hyperparameters, there is no generally good &ldquo;magic number&rdquo; for them, and instead you&rsquo;ll need to try a bunch of values and see what works. You&rsquo;d make some guesses, see what happens, then generate some hypotheses on what a given change to those parameters might do, then try out a different number and see if it has the desired effect. Every time you&rsquo;re figuring out what to try next, you&rsquo;ll be reading a <em>ton</em> of test cases and trying to observe general failure modes exhibited in this intermediate draft of your fine-tuned model. That might change the initial choice of model you&rsquo;re fine-tuning, and it might change which subsets of the training data you&rsquo;re exposing to the training process.</p>
<p><strong>How do you measure success?</strong> You need a reliable way to tell whether each version of your model is actually improving. That is especially tricky for free-text tasks, because two answers can be equally good even when they differ syntactically. Because of this, you&rsquo;ll probably want an LLM-as-a-judge system, where another model compares the responses to a target and grades according to a rubric you&rsquo;ve supplied. LLM-as-a-judge systems are themselves quite hard to design correctly, and you&rsquo;ll be reading a bunch of that judge&rsquo;s grading transcripts, too.</p>
<p>It&rsquo;s also worth mentioning that <strong>getting your fine-tuned model to score well on your own benchmark is not the hard part</strong>. Many of these fine-tuning writeups include a plot along these lines:</p>
<img src="https://opensource.posit.co/blog/2026-09-04_ai-newsletter/index.markdown_strict_files/figure-markdown_strict/plot-finetune-benchmark-1.png" style="width:100.0%" data-fig-align="center" data-fig-alt="A scatter plot of evaluation performance against cost per million tokens (log scale). Unlabeled grey synthetic points loosely follow a rising trend. Labeled model points include Fable 5.1, GPT 5.6 Sol, Sonnet 5, Gemma 4 26B, Llama 4 8B, and Phi 5 mini. A red point labeled &#39;Our fine-tune&#39; sits in the upper left, cheap yet scoring above the frontier models." />
<p>When I see that a single-digit-billion-parameter fine-tuned model scores better than Fable 5.1 or GPT 5.6 Sol on an evaluation, I interpret that as evidence that the evaluation is not meaningful. In my own experience, it is not (comparatively) hard to get a small model to score very well on any given benchmark. What&rsquo;s much more difficult is preserving the broad intelligence of a model while driving the evaluation score up. A meaningful evaluation can do both at once, measuring task performance under a realistic, broad distribution of possible task configurations. It is very hard to author meaningful evaluations, especially for models as capable as those that exist today.</p>
<p>But let&rsquo;s say you&rsquo;ve made it this far! You found a good model to start from, a diverse set of training data that you&rsquo;ve obtained consent to train on, and a meaningful way to measure progress. Now, it&rsquo;s time to put it in production.</p>
<h2 id="fine-tuning-is-expensive">Fine-tuning is expensive
</h2>
<p>Fine-tuning can seem appealing because the actual fine-tuning part of the process can be relatively inexpensive, likely on the order of a few dollars to a few hundred dollars.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>However, actually deploying the model to do the task you trained it to do is likely to be substantially more expensive.</p>
<h3 id="because-hosting-is-expensive">&hellip;because hosting is expensive
</h3>
<p>Hosting a model yourself is more expensive than using a similarly capable model hosted by someone else unless you have extraordinary volume. Frontier-model providers like Anthropic and OpenAI serve extraordinarily large volumes, keeping their compute almost always near max capacity. It&rsquo;s likely that the same won&rsquo;t be true for your fine-tuned, self-hosted model.</p>
<p>As an example, let&rsquo;s say I host Gemma 4 26B A4B on a single H100. I can rent a high-availability <a href="https://lambda.ai/instances" target="_blank" rel="noopener">H100</a> <a href="https://fireworks.ai/pricing" target="_blank" rel="noopener">GPU</a> <a href="https://www.baseten.co/pricing/" target="_blank" rel="noopener">instance</a> at $0.0833 a minute, or $44,000 a year. Assuming a coding-agent-like workload,<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> that would buy about 96 billion <a href="https://platform.claude.com/docs/en/about-claude/pricing#:~:text=%2475%20/%20MTok-,Claude%20Sonnet%205,%2410%20/%20MTok,-Claude%20Sonnet%204.6" target="_blank" rel="noopener">Sonnet 5</a> tokens, and Sonnet 5 is a much more capable model. To break even, this fine-tuned model would need to serve about 182,000 tokens per minute, 24/7, throughout the year.</p>
<p>It might be possible to serve that many tokens. The hard part is finding enough demand for one task to keep the model busy, and that demand needs to be relatively smooth. If the model sits idle, you&rsquo;re still paying for it. If demand spikes, you either rent a second GPU or accept worse performance for users.</p>
<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>I&rsquo;m not arguing against self-hosting in general. Self-hosting (either literally on your organization&rsquo;s own hardware or by renting hardware and serving open-weights models on it with open source software) can be quite cost-effective at sufficient scale. If you know that a <em>lot</em> of traffic will go through that endpoint, do the same napkin math shown above and see if you can save some money. What I&rsquo;m particularly arguing against is that it&rsquo;s a good idea to self-host a model <em>that can only do one thing</em>. Unless you serve an extraordinary amount of traffic that does that task specifically, the payoff is not there.</p>
</div>
</div>
<h3 id="and-you-must-host">&hellip;and you must host
</h3>
<p>The other suggestion here is that, if the model is small enough, users who need to do the task can just download the weights and run it on their laptop or a dedicated workstation.</p>
<p>The effectiveness of this argument depends on the kind of task to be done. However, it&rsquo;s worth considering the potential for this to be a very unpleasant user experience compared to just using a model that someone else hosts. For example, let&rsquo;s say my colleague does some task for an hour every week and there&rsquo;s some model small enough to run on my laptop that&rsquo;s capable of doing the task. In order to use the model for that task, my colleague would need to download the 4GB or 8GB or 100GB or whatever of weights. That user would also need to be capable of configuring the serving of the model, and they&rsquo;d need to make that happen every time they started the task. If that model was accessed through a tool that the user was using regularly anyway&mdash;Claude Code pointed at a local ollama model, for instance&mdash;they&rsquo;d need to remember to switch the model over in the application&rsquo;s settings. If that model was accessed through a different interface, the user would need to use a different interface than they normally use LLMs with for that task specifically. Neither of these are good UX.</p>
<p>Even if the software for locally serving models got <em>much</em> more pleasant than it currently is, it&rsquo;s hard to compete with the experience of pay-as-you-go for the Everything Tool that wraps the Everything Model.</p>
<h2 id="your-fine-tune-will-quickly-fall-behind">Your fine-tune will quickly fall behind
</h2>
<p>Let&rsquo;s say you successfully fine-tune a model based on some fictional model Kuen 3. Then, the following week, Kuen 3.5 is released. 3 of the 100 most capable LLM scientists on this planet worked on it. It&rsquo;s almost as good as your fine-tune on that specific task, and it&rsquo;s also broadly capable at a very broad array of tasks.</p>
<p>Fine-tuning is not a boat that is lifted by the rising tide of broader AI progress&mdash;in order to take advantage of the Kuen 3.5 release with your fine-tune, you&rsquo;d need to restart that process of choosing parameters and observing fine-tuning runs. The underlying architecture of the model may have changed, and thus the approaches that you used to fine-tune Kuen 3 might not work for Kuen 3.5 on your first try.</p>
<h2 id="what-you-should-do-instead">What you should do instead
</h2>
<p>Instead, the boat that is lifted by a rising tide in this context is plain old prompt engineering. (POPE, as <a href="https://github.com/jcheng5" target="_blank" rel="noopener">Joe Cheng</a> calls it.) Choose a model that&rsquo;s available to you, that fits your price point, and seems broadly capable across a wide variety of public benchmarks. (Extra points if the vibes on the model from people you trust are good, and extra points if someone else is serving it.)</p>
<p>Then, put together a short prompt telling the model how to do the task, perhaps inside of some coding agent harness like Claude Code (or, hey, <a href="https://assistant.posit.co/" target="_blank" rel="noopener">Posit Assistant</a>), and see what it does. Adjust the prompt to tell it how to do things correctly that it tends to trip up on, and iterate from there. If you&rsquo;ve already put together an evaluation as part of your fine-tuning process, you could even reuse that! My colleague Sara and I have written about prompt engineering in the past if you&rsquo;re interested in learning more, once about the more specific task of POPE for <a href="https://posit.co/blog/custom-chat-app" target="_blank" rel="noopener">teaching LLMs about R packages</a> but in relatively generalizable ways, and once focused on <a href="https://opensource.posit.co/blog/2026-07-03_ai-newsletter/" target="_blank" rel="noopener">deciding between the popular ways to deliver context to coding agents</a>.</p>
<h2 id="recent-past-newsletters">Recent past newsletters
</h2>
<ul>
<li><a href="https://opensource.posit.co/blog/2026-08-14_ai-newsletter">How to choose a model</a></li>
<li><a href="https://opensource.posit.co/blog/2026-07-31_ai-newsletter">Keep track of your data exploration with Posit Assistant&rsquo;s EDA log</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>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Various technical details might mean this is an order of magnitude or two off. Regardless, the larger point stands that a single fine-tuning run is not the expensive part of deploying a fine-tuned model.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Meaning around 90% of tokens are cached input, 9% of tokens are uncached input, and 1% of tokens are output.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-09-04_ai-newsletter/images/hero.png" length="182577" type="image/png" />
    </item>
    <item>
      <title>vitals 0.4.0</title>
      <link>https://opensource.posit.co/blog/2026-09-03_vitals-0-4-0/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-09-03_vitals-0-4-0/</guid>
      <dc:creator>Simon Couch</dc:creator><description><![CDATA[<p>I&rsquo;m as amped as Ella Langley&rsquo;s Gibson to share that <a href="https://vitals.tidyverse.org/" target="_blank" rel="noopener">vitals</a> 0.4.0 is now on CRAN! vitals implements a large language model evaluation toolkit for R, and this release contains several exciting features.</p>
<p>To install the newest release, run the following in R:</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;vitals&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>The package includes two new helpers, <a href="https://vitals.tidyverse.org/reference/agent_solvers.html" target="_blank" rel="noopener"><code>claude_code()</code></a> and <a href="https://vitals.tidyverse.org/reference/agent_solvers.html" target="_blank" rel="noopener"><code>codex()</code></a>, which allow
you to compare your own ellmer-built agents with leading coding agents. This release also ships another new helper, <a href="https://vitals.tidyverse.org/reference/vitals_log_read.html" target="_blank" rel="noopener"><code>vitals_log_read()</code></a>, which supports reading log files back into tibbles, including columns of resumable ellmer Chats. Finally, the release includes several performance improvements; log files are much smaller, and the log viewer that reads them is now substantively faster.</p>
<p>To read the full list of changes, see the <a href="https://vitals.tidyverse.org/news/index.html#vitals-040" target="_blank" rel="noopener">changelog</a>.</p>
<h2 id="agent-solvers">Agent solvers
</h2>
<p>vitals is a port of <a href="https://inspect.aisi.org.uk/" target="_blank" rel="noopener">Inspect</a>, a well-adopted Python framework for LLM eval from Posit&rsquo;s own JJ Allaire. One of the concepts that vitals borrows from Inspect is the concept of a &ldquo;solver,&rdquo; or the LLM-powered system that sets out to solve some task. The simplest solver is just the LLM itself, with no system prompt or tools, like what you&rsquo;d get from running <code>chat_anthropic()</code> from ellmer. Solvers can gain all sorts of prompts and tools, which allows vitals users to test the effect of a change in their prompt or the addition of a new tool.</p>
<p>In the last year or so, the dominant interface to solvers in Inspect has become &ldquo;agent solvers&rdquo;: interfaces to the popular coding agents Claude Code and Codex. You call the helper <code>claude_code()</code> or <code>codex()</code>, and Inspect will proxy traffic through the real coding agent harness.</p>
<p>vitals now has first-class support for these two helpers, allowing users to compare their own agents built with ellmer to popular coding agents like Claude Code and Codex. You provide the set of tasks and grading guidance, and vitals will take care of the communication with Inspect.</p>
<h2 id="read-eval-logs-back-into-ellmer-chats">Read eval logs back into ellmer Chats
</h2>
<p>One of the big annoyances I&rsquo;ve had in my own usage of vitals is log storage. So that users can use Inspect&rsquo;s log viewer directly, we write evaluation logs to a JSON format that Inspect can read.^[1] However, I often want to write R code against the original R objects—ellmer Chats especially—that the logs were generated from. Loading in the ellmer Chats would especially be helpful for inspecting (ha!) the conversation histories in the same interface that users of the ellmer application would see.</p>
<p>Because of this, I&rsquo;ve often saved <em>both</em> the JSON logs and <code>.rda</code> logs, the latter of which contain the ellmer Chats. These files are large on their own, and it feels even more silly passing around duplicates of them.</p>
<p>The new release of vitals introduces <code>vitals_log_read()</code>, which reads an eval log file back into a tibble of samples. (It&rsquo;s almost exactly what you&rsquo;d get if you ran the <code>get_samples()</code> method on a vitals Task object.) That tibble includes reconstructed solver (and, for model-graded scorers, scorer) chats as ellmer Chat objects. For some providers, the chats will even be resumable; you can load in a solver from a JSON file into an R session and ask that solver a question yourself.</p>
<h2 id="performance-improvements">Performance improvements
</h2>
<p>The long and the short of this section is just to say that:</p>
<ol>
<li>Logs will take up less storage space than they did before. Roughly, logs written with the new vitals version will be 4x smaller than before, and the magnitude of savings increases with the complexity of the log.</li>
<li>We now display logs (with <code>vitals_view()</code>) <em>much</em> more quickly. The log viewer should feel very snappy for almost all uses of the package.</li>
</ol>
<p>I&rsquo;m really excited to have this release on CRAN! Take it for a spin and let me know if you run into issues on the <a href="https://github.com/tidyverse/vitals" target="_blank" rel="noopener">package repository</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-09-03_vitals-0-4-0/featured.png" length="414699" type="image/png" />
    </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>recipes 1.4.0</title>
      <link>https://opensource.posit.co/blog/2026-08-26_recipes-1-4-0/</link>
      <pubDate>Wed, 26 Aug 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-08-26_recipes-1-4-0/</guid>
      <dc:creator>Emil Hvitfeldt</dc:creator><description><![CDATA[<p>We&rsquo;re pleased to announce that <a href="https://recipes.tidymodels.org" target="_blank" rel="noopener">recipes 1.4.0</a> is now on CRAN.
recipes lets you create a pipeable sequence of feature engineering steps.</p>
<p>This release brings 3 main improvements that are worth your attention:
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>
<p>You can install it with:</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;recipes&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<h2 id="seeing-a-recipe-partway-through">Seeing a recipe partway through
</h2>
<p>A recipe is constructed using a series of pipes.
Since it is prepped as a unit,
it can be a little hard to look into the internals and see what happens at each stage of the recipe.
If you wanted to know what the data looked like after the second recipe step you would need to write out a separate recipe with just those steps.</p>
<p>This is no longer the case as <code>bake()</code> now takes a <code>stop_at</code> argument,
which applies only the steps up to and including the one you specify.
Below we write out a small recipe on the ames data set.</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">data</span><span class="p">(</span><span class="n">ames</span><span class="p">,</span> <span class="n">package</span> <span class="o">=</span> <span class="s">&#34;modeldata&#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">ames</span> <span class="o">&lt;-</span> <span class="n">ames</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">select</span><span class="p">(</span><span class="n">Sale_Price</span><span class="p">,</span> <span class="n">Neighborhood</span><span class="p">,</span> <span class="n">Bldg_Type</span><span class="p">,</span> <span class="n">Year_Built</span><span class="p">,</span> <span class="n">Gr_Liv_Area</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">mutate</span><span class="p">(</span><span class="n">Sale_Price</span> <span class="o">=</span> <span class="nf">log10</span><span class="p">(</span><span class="n">Sale_Price</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ames_rec</span> <span class="o">&lt;-</span> <span class="nf">recipe</span><span class="p">(</span><span class="n">Sale_Price</span> <span class="o">~</span> <span class="n">.,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">ames</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_other</span><span class="p">(</span><span class="n">Neighborhood</span><span class="p">,</span> <span class="n">threshold</span> <span class="o">=</span> <span class="m">0.05</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_dummy</span><span class="p">(</span><span class="nf">all_nominal_predictors</span><span class="p">())</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_normalize</span><span class="p">(</span><span class="nf">all_numeric_predictors</span><span class="p">())</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">prep</span><span class="p">()</span></span></span></code></pre></div></div>
<p>The three steps each do a job that the next one depends on.
<code>Neighborhood</code> has 29 levels, many of them rare,
so <code>step_other()</code> pools every level accounting for less than 5% of the data into a single <code>&quot;other&quot;</code> level,
which keeps the next step from producing near-empty indicator columns.
<code>step_dummy()</code> then turns the remaining factors (<code>Neighborhood</code> and <code>Bldg_Type</code>) into 0/1 indicator columns,
since most models need numeric predictors.
Finally, <code>step_normalize()</code> puts every predictor on the same scale (mean 0, standard deviation 1),
which matters for regularized and distance-based models.</p>
<p>Now let&rsquo;s first see the data before we apply the recipe on it.</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">tail</span><span class="p">(</span><span class="n">ames</span><span class="p">)</span> <span class="o">|&gt;</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">glimpse</span><span class="p">()</span></span></span></code></pre></div></div>
<pre><code>Rows: 6
Columns: 5
$ Sale_Price   &lt;dbl&gt; 5.117271, 5.153815, 5.117271, 5.120574, 5.230449, 5.274158
$ Neighborhood &lt;fct&gt; Mitchell, Mitchell, Mitchell, Mitchell, Mitchell, Mitchell
$ Bldg_Type    &lt;fct&gt; OneFam, OneFam, OneFam, OneFam, OneFam, OneFam
$ Year_Built   &lt;int&gt; 1960, 1984, 1983, 1992, 1974, 1993
$ Gr_Liv_Area  &lt;int&gt; 1224, 1003, 902, 970, 1389, 2000
</code></pre>
<p>If we now wanted to see what happened after the first step,
we pass in <code>stop_at = 1</code> to <code>bake()</code>.</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">bake</span><span class="p">(</span><span class="n">ames_rec</span><span class="p">,</span> <span class="n">new_data</span> <span class="o">=</span> <span class="nf">tail</span><span class="p">(</span><span class="n">ames</span><span class="p">),</span> <span class="n">stop_at</span> <span class="o">=</span> <span class="m">1</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">glimpse</span><span class="p">()</span></span></span></code></pre></div></div>
<pre><code>Rows: 6
Columns: 5
$ Neighborhood &lt;fct&gt; other, other, other, other, other, other
$ Bldg_Type    &lt;fct&gt; OneFam, OneFam, OneFam, OneFam, OneFam, OneFam
$ Year_Built   &lt;int&gt; 1960, 1984, 1983, 1992, 1974, 1993
$ Gr_Liv_Area  &lt;int&gt; 1224, 1003, 902, 970, 1389, 2000
$ Sale_Price   &lt;dbl&gt; 5.117271, 5.153815, 5.117271, 5.120574, 5.230449, 5.274158
</code></pre>
<p>And we see that <code>Mitchell</code> was collapsed into <code>&quot;other&quot;</code> by <code>step_other()</code>.
Next we set <code>stop_at = 2</code> to see the result of <code>step_other()</code> followed by <code>step_dummy()</code>.</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">bake</span><span class="p">(</span><span class="n">ames_rec</span><span class="p">,</span> <span class="n">new_data</span> <span class="o">=</span> <span class="nf">tail</span><span class="p">(</span><span class="n">ames</span><span class="p">),</span> <span class="n">stop_at</span> <span class="o">=</span> <span class="m">2</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">glimpse</span><span class="p">()</span></span></span></code></pre></div></div>
<pre><code>Rows: 6
Columns: 15
$ Year_Built                      &lt;int&gt; 1960, 1984, 1983, 1992, 1974, 1993
$ Gr_Liv_Area                     &lt;int&gt; 1224, 1003, 902, 970, 1389, 2000
$ Sale_Price                      &lt;dbl&gt; 5.117271, 5.153815, 5.117271, 5.120574, 5.…
$ Neighborhood_College_Creek      &lt;dbl&gt; 0, 0, 0, 0, 0, 0
$ Neighborhood_Old_Town           &lt;dbl&gt; 0, 0, 0, 0, 0, 0
$ Neighborhood_Edwards            &lt;dbl&gt; 0, 0, 0, 0, 0, 0
$ Neighborhood_Somerset           &lt;dbl&gt; 0, 0, 0, 0, 0, 0
$ Neighborhood_Northridge_Heights &lt;dbl&gt; 0, 0, 0, 0, 0, 0
$ Neighborhood_Gilbert            &lt;dbl&gt; 0, 0, 0, 0, 0, 0
$ Neighborhood_Sawyer             &lt;dbl&gt; 0, 0, 0, 0, 0, 0
$ Neighborhood_other              &lt;dbl&gt; 1, 1, 1, 1, 1, 1
$ Bldg_Type_TwoFmCon              &lt;dbl&gt; 0, 0, 0, 0, 0, 0
$ Bldg_Type_Duplex                &lt;dbl&gt; 0, 0, 0, 0, 0, 0
$ Bldg_Type_Twnhs                 &lt;dbl&gt; 0, 0, 0, 0, 0, 0
$ Bldg_Type_TwnhsE                &lt;dbl&gt; 0, 0, 0, 0, 0, 0
</code></pre>
<p>And we can see the dummies created on <code>Neighborhood</code> and <code>Bldg_Type</code>.</p>
<h2 id="much-faster-on-wide-data">Much faster on wide data
</h2>
<p>Many of the steps such as <code>step_normalize()</code>, <code>step_unknown()</code>, and <code>step_impute_mean()</code> work by modifying the data frame in place.
No new columns are created and no columns are removed.
This type of step is quite common throughout recipes.
It was found that these steps could have their performance improved,
especially if applied to a lot of columns.
Regardless of how many columns you have, you will get the same results as before,
at the same speed or faster depending on the number of columns.</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">library</span><span class="p">(</span><span class="n">recipes</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">set.seed</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">&lt;-</span> <span class="nf">as.data.frame</span><span class="p">(</span><span class="nf">matrix</span><span class="p">(</span><span class="nf">rnorm</span><span class="p">(</span><span class="m">200</span> <span class="o">*</span> <span class="m">5000</span><span class="p">),</span> <span class="n">nrow</span> <span class="o">=</span> <span class="m">200</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">rec</span> <span class="o">&lt;-</span> <span class="nf">recipe</span><span class="p">(</span><span class="o">~</span> <span class="n">.,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">df</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_normalize</span><span class="p">(</span><span class="nf">all_numeric_predictors</span><span class="p">())</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_YeoJohnson</span><span class="p">(</span><span class="nf">all_numeric_predictors</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">system.time</span><span class="p">(</span><span class="n">prepped</span> <span class="o">&lt;-</span> <span class="nf">prep</span><span class="p">(</span><span class="n">rec</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="nf">system.time</span><span class="p">(</span><span class="nf">bake</span><span class="p">(</span><span class="n">prepped</span><span class="p">,</span> <span class="n">new_data</span> <span class="o">=</span> <span class="n">df</span><span class="p">))</span></span></span></code></pre></div></div>
<p>On my machine I get roughly these numbers.</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>recipes 1.3.3</th>
          <th>recipes 1.4.0</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>prep()</code></td>
          <td>2.00s</td>
          <td>0.72s</td>
      </tr>
      <tr>
          <td><code>bake()</code></td>
          <td>1.39s</td>
          <td>0.06s</td>
      </tr>
  </tbody>
</table>
<p><code>prep()</code> got about 3× faster here and <code>bake()</code> over 20×, and the gap widens as you add columns.</p>
<p>Over 40 steps benefited from this change.
See the <a href="https://recipes.tidymodels.org/news/index.html" target="_blank" rel="noopener">changelog</a> for the complete list.
You don&rsquo;t have to change any code to get this.
Existing recipes are just faster.</p>
<p>If you write your own steps,
the helper behind this is exported as <code>recipes_map_cols()</code>.</p>
<h2 id="step_count-and-step_regex-take-multiple-columns"><code>step_count()</code> and <code>step_regex()</code> take multiple columns
</h2>
<p>The two steps <code>step_count()</code> and <code>step_regex()</code> were always the odd ones out as they could not be applied to more than one column.
This has been fixed in this release, where the steps can now be applied to any number of columns,
putting them in line with the rest of the steps in the tidymodels ecosystem.</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">ice_cream</span> <span class="o">&lt;-</span> <span class="nf">tibble</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">flavor</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="s">&#34;rocky road&#34;</span><span class="p">,</span> <span class="s">&#34;stony brook&#34;</span><span class="p">,</span> <span class="s">&#34;vanilla&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">notes</span>  <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="s">&#34;a rock in there&#34;</span><span class="p">,</span> <span class="s">&#34;no match&#34;</span><span class="p">,</span> <span class="s">&#34;rock and stone&#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">recipe</span><span class="p">(</span><span class="o">~</span> <span class="n">.,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">ice_cream</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_count</span><span class="p">(</span><span class="n">flavor</span><span class="p">,</span> <span class="n">notes</span><span class="p">,</span> <span class="n">pattern</span> <span class="o">=</span> <span class="s">&#34;rock&#34;</span><span class="p">,</span> <span class="n">result</span> <span class="o">=</span> <span class="s">&#34;rocks&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">prep</span><span class="p">()</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">bake</span><span class="p">(</span><span class="n">new_data</span> <span class="o">=</span> <span class="kc">NULL</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code># A tibble: 3 × 4
  flavor      notes           flavor_rocks notes_rocks
  &lt;fct&gt;       &lt;fct&gt;                  &lt;int&gt;       &lt;int&gt;
1 rocky road  a rock in there            1           1
2 stony brook no match                   0           0
3 vanilla     rock and stone             0           1
</code></pre>
<p>When more than one column is selected, the new columns are named <code>{column}_{result}</code> rather than just <code>{result}</code>,
so they stay distinguishable.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>Many thanks to all the people who contributed to recipes since the last release!</p>
<p><a href="https://github.com/EmilHvitfeldt" target="_blank" rel="noopener">@EmilHvitfeldt</a>, <a href="https://github.com/instantkaffee" target="_blank" rel="noopener">@instantkaffee</a>, <a href="https://github.com/LeonidasZhak" target="_blank" rel="noopener">@LeonidasZhak</a>, and <a href="https://github.com/topepo" target="_blank" rel="noopener">@topepo</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-08-26_recipes-1-4-0/featured.jpg" length="279577" type="image/jpeg" />
    </item>
    <item>
      <title>cuda.ml 0.4.0: GPU-accelerated machine learning from R</title>
      <link>https://opensource.posit.co/blog/2026-08-21_cuda-ml-0-4-0/</link>
      <pubDate>Fri, 21 Aug 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-08-21_cuda-ml-0-4-0/</guid>
      <dc:creator>Tomasz Kalinowski</dc:creator><description><![CDATA[<p><a href="https://mlverse.github.io/cuda.ml/" target="_blank" rel="noopener">cuda.ml</a> is an R package for
running common data science and machine-learning operations on NVIDIA
GPUs. It provides high-level interfaces for fitting regression and
classification models, finding nearest neighbors, clustering
observations, reducing dimensions, and running predictions from tree
ensembles. You can use its direct R functions or work through parsnip
and tidymodels.</p>
<p>cuda.ml is for data scientists who work primarily in R and want to use a
GPU without moving their modeling workflow to Python or learning
low-level GPU APIs. Version 0.4.0 is a substantial update to the
package. It makes installation much simpler, expands tidymodels support,
adds more ways to run tree-ensemble models, and makes it straightforward
to save and restore supported fitted models.</p>
<h2 id="install-from-cran">Install from CRAN
</h2>
<p>For most users, setup is two commands:</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;cuda.ml&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">cuda.ml</span><span class="o">::</span><span class="nf">cuda_ml_install</span><span class="p">()</span></span></span></code></pre></div></div>
<p>The package from CRAN is a regular, portable R package.
<code>cuda_ml_install()</code> downloads and verifies the matching compiled backend
and GPU libraries, then keeps them in a cache for later R sessions. On a
supported system, you do not need to compile cuda.ml from source,
configure a Python environment, or assemble the GPU libraries yourself.</p>
<p>The result is a familiar R package workflow: install the package,
prepare its supporting libraries once, and start an analysis. The extra
installation call is explicit because the GPU libraries are much larger
than the R package. Repeated calls reuse the completed cache, and
loading cuda.ml itself is quiet and does not initialize CUDA.</p>
<p>Prebuilt support is available for Linux x86_64 with glibc 2.28 or newer.
GPU operations require a supported NVIDIA GPU and driver 580 or newer.
On Windows, install and run R inside a compatible WSL2 Linux
distribution. See the <a href="https://mlverse.github.io/cuda.ml/articles/install-manage.html" target="_blank" rel="noopener">installation guide</a> for the complete system requirements and source-build options.</p>
<h2 id="use-familiar-modeling-interfaces">Use familiar modeling interfaces
</h2>
<p>cuda.ml registers parsnip engines for linear, logistic, and multinomial
regression, random forests, nearest neighbors, and radial, polynomial,
and linear support-vector machines. For example, this fits a
random-forest classifier on the GPU using the standard parsnip
interface:</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">library</span><span class="p">(</span><span class="n">cuda.ml</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">parsnip</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">forest_spec</span> <span class="o">&lt;-</span> <span class="nf">rand_forest</span><span class="p">()</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">set_mode</span><span class="p">(</span><span class="s">&#34;classification&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">set_engine</span><span class="p">(</span><span class="s">&#34;cuda.ml&#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">forest_fit</span> <span class="o">&lt;-</span> <span class="n">forest_spec</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">fit</span><span class="p">(</span><span class="n">class</span> <span class="o">~</span> <span class="n">.,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">modeldata</span><span class="o">::</span><span class="n">hpc_data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">predict</span><span class="p">(</span><span class="n">forest_fit</span><span class="p">,</span> <span class="n">modeldata</span><span class="o">::</span><span class="n">hpc_data[1</span><span class="o">:</span><span class="m">5</span><span class="p">,</span> <span class="n">]</span><span class="p">,</span> <span class="n">type</span> <span class="o">=</span> <span class="s">&#34;prob&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># A tibble: 5 × 4
</span></span><span class="line"><span class="cl">  .pred_VF .pred_F .pred_M  .pred_L
</span></span><span class="line"><span class="cl">     &lt;dbl&gt;   &lt;dbl&gt;   &lt;dbl&gt;    &lt;dbl&gt;
</span></span><span class="line"><span class="cl">1    0.309  0.597  0.0666  0.0275
</span></span><span class="line"><span class="cl">2    0.899  0.0838 0.0138  0.00335
</span></span><span class="line"><span class="cl">3    0.965  0.0261 0.00850 0.000883
</span></span><span class="line"><span class="cl">4    0.973  0.0228 0.00416 0.000352
</span></span><span class="line"><span class="cl">5    0.966  0.0298 0.00416 0.000352</span></span></code></pre></div></div>
<p><code>set_engine(&quot;cuda.ml&quot;)</code> selects the GPU-backed cuda.ml engine; the rest
is a standard parsnip workflow. Recipes can learn preprocessing on the
training data and carry it into resampling and prediction.</p>
<h2 id="work-directly-with-cudaml">Work directly with cuda.ml
</h2>
<p>cuda.ml also provides a direct R interface. This is useful when you
prefer a function-oriented workflow or want to use the package on its
own.</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">library</span><span class="p">(</span><span class="n">cuda.ml</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">ggplot2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">clusters</span> <span class="o">&lt;-</span> <span class="nf">cuda_ml_kmeans</span><span class="p">(</span><span class="nf">scale</span><span class="p">(</span><span class="n">faithful</span><span class="p">),</span> <span class="n">k</span> <span class="o">=</span> <span class="m">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">faithful</span><span class="o">$</span><span class="n">cluster</span> <span class="o">&lt;-</span> <span class="nf">factor</span><span class="p">(</span><span class="n">clusters</span><span class="o">$</span><span class="n">labels</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">ggplot</span><span class="p">(</span><span class="n">faithful</span><span class="p">,</span> <span class="nf">aes</span><span class="p">(</span><span class="n">eruptions</span><span class="p">,</span> <span class="n">waiting</span><span class="p">,</span> <span class="n">color</span> <span class="o">=</span> <span class="n">cluster</span><span class="p">))</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">  <span class="nf">geom_point</span><span class="p">(</span><span class="n">size</span> <span class="o">=</span> <span class="m">2.5</span><span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">  <span class="nf">labs</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">x</span> <span class="o">=</span> <span class="s">&#34;Eruption duration (minutes)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">y</span> <span class="o">=</span> <span class="s">&#34;Waiting time (minutes)&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">  <span class="nf">theme_minimal</span><span class="p">()</span></span></span></code></pre></div></div>
<img src="https://opensource.posit.co/blog/2026-08-21_cuda-ml-0-4-0/index.markdown_strict_files/figure-markdown_strict/faithful-clusters-1.png" data-fig-align="center" data-fig-alt="Scatterplot of Old Faithful eruption durations and waiting times, colored by two clusters. Shorter eruptions have shorter waits, while longer eruptions have longer waits." width="768" />
<p>The direct API covers supervised models as well as clustering and
dimensionality reduction, including DBSCAN, k-means, PCA, tSVD, UMAP,
and t-SNE. It also includes stochastic-gradient-descent regression, the
hyperbolic-tangent SVM kernel, and external tree-ensemble inference.</p>
<h2 id="run-tree-ensembles-on-a-gpu-or-cpu">Run tree ensembles on a GPU or CPU
</h2>
<p>This release expands where and how you can make predictions with tree
ensembles. The new nvForest support powers prediction for random forests
trained with <code>cuda_ml_rand_forest()</code> and can load trained XGBoost
models, LightGBM text models, and Treelite checkpoints. Once a model is
loaded, the API provides standard prediction along with model
information, leaf identifiers, individual-tree predictions, and
checkpoint import and export.</p>
<p>GPU inference uses the complete cuda.ml installation. A deployment that
only needs CPU inference can prepare a smaller, CUDA-free backend:</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">cuda.ml</span><span class="o">::</span><span class="nf">cuda_ml_install</span><span class="p">(</span><span class="n">device</span> <span class="o">=</span> <span class="s">&#34;cpu&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>The CPU backend can run a cuda.ml random forest trained on a GPU as well
as a supported external tree ensemble. The complete installation can
also run these models on a CPU, so the smaller backend is an optional
deployment choice.</p>
<h2 id="save-and-deploy-fitted-models">Save and deploy fitted models
</h2>
<p>cuda.ml 0.4.0 expands model persistence for training, analysis, and
deployment workflows. Supported fitted models can be saved to a
compressed file, restored in another R process, stored as raw bytes, or
wrapped with the bundle package.</p>
<p>The simplest file workflow passes a path directly:</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">model</span> <span class="o">&lt;-</span> <span class="nf">cuda_ml_rand_forest</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">class</span> <span class="o">~</span> <span class="n">.,</span>
</span></span><span class="line"><span class="cl">  <span class="n">data</span> <span class="o">=</span> <span class="n">modeldata</span><span class="o">::</span><span class="n">hpc_data</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">trees</span> <span class="o">=</span> <span class="m">100</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">cuda_ml_serialize</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="s">&#34;hpc-runtime-forest.cuda-ml&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>In another R process or deployment environment, prepare cuda.ml and
restore the fitted model:</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">library</span><span class="p">(</span><span class="n">cuda.ml</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">cuda_ml_install</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">model</span> <span class="o">&lt;-</span> <span class="nf">cuda_ml_unserialize</span><span class="p">(</span><span class="s">&#34;hpc-runtime-forest.cuda-ml&#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">predict</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">modeldata</span><span class="o">::</span><span class="n">hpc_data[1</span><span class="o">:</span><span class="m">5</span><span class="p">,</span> <span class="n">]</span><span class="p">,</span> <span class="n">type</span> <span class="o">=</span> <span class="s">&#34;class&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>File paths use gzip compression. Passing <code>connection = NULL</code> instead
returns the state as an uncompressed raw vector of bytes, which is
convenient for object stores and database BLOB columns. The blob package
can represent raw vectors for database workflows, and the bundle package
is supported for teams that already use bundled model artifacts.</p>
<p>Persistence is available for linear models, logistic and multinomial
regression, PCA, SVC and SVR models, UMAP, random forests, and nvForest
models. cuda.ml checks the saved state and required backend before
restoring it. For an nvForest-backed model, the restore call can select
CPU or GPU inference.</p>
<h2 id="highlights-for-users-upgrading-from-cudaml-03">Highlights for users upgrading from cuda.ml 0.3
</h2>
<p>This is a breaking update to the earlier package. The most visible
changes are:</p>
<ul>
<li>The random-forest API now uses <code>mtry</code> for predictor sampling and
<code>sample_fraction</code> for row sampling. <code>trees</code> defaults to 100, and an
omitted <code>seed</code> draws from R&rsquo;s random-number generator, so <code>set.seed()</code>
controls the fit.</li>
<li>Linear, logistic, and multinomial regression now use numeric <code>penalty</code>
and <code>mixture</code> arguments that match parsnip. Logistic and multinomial
regression are unregularized by default.</li>
<li><code>normalize_input</code> was removed from the linear-model functions. It
previously requested GPU-side L2 normalization. Use explicit
preprocessing such as <code>recipes::step_normalize()</code> when centering and
scaling are appropriate; but please note, the two operations are not
numerically identical.</li>
<li><code>cuda_ml_sgd()</code> now fits squared-loss regression only. Its <code>loss</code>
argument was removed, and <code>n_iters_no_change</code> is now
<code>n_iter_no_change</code>.</li>
<li>The former FIL interface has been replaced by nvForest. Random
projection and the KNN IVFSQ index have no replacement in the pinned
upstream API.</li>
</ul>
<p>See the
<a href="https://mlverse.github.io/cuda.ml/news/index.html" target="_blank" rel="noopener">full changelog</a> for
the complete list of API changes.</p>
<p>The guides cover first steps and more complete examples:</p>
<ul>
<li><a href="https://mlverse.github.io/cuda.ml/articles/cuda-ml.html" target="_blank" rel="noopener">Get started with cuda.ml</a></li>
<li><a href="https://mlverse.github.io/cuda.ml/articles/tidymodels.html" target="_blank" rel="noopener">Use cuda.ml with tidymodels</a></li>
<li><a href="https://mlverse.github.io/cuda.ml/articles/model-persistence.html" target="_blank" rel="noopener">Save and restore models</a></li>
<li><a href="https://mlverse.github.io/cuda.ml/articles/nvforest.html" target="_blank" rel="noopener">nvForest inference and deployment</a></li>
</ul>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-08-21_cuda-ml-0-4-0/featured-alt.png" length="2702295" 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>themis 1.1.0</title>
      <link>https://opensource.posit.co/blog/2026-08-13_themis-1-1-0/</link>
      <pubDate>Thu, 13 Aug 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-08-13_themis-1-1-0/</guid>
      <dc:creator>Emil Hvitfeldt</dc:creator><description><![CDATA[<p>I&rsquo;m very happy to announce that <a href="https://themis.tidymodels.org/" target="_blank" rel="noopener">themis 1.1.0</a> is now on CRAN.
themis provides extra recipes steps for dealing with unbalanced data.
You can install it with:</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;themis&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>This release provides a substantial amount of new features.
We will cover the highlights in this blog post, which include:
11 new steps, setting sampling targets per class, and more distance metrics.
See the <a href="https://themis.tidymodels.org/news/index.html#themis-110" target="_blank" rel="noopener">news file</a> for a complete list of changes in this release.</p>
<p>To get started,
load the tidymodels and themis packages:</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">library</span><span class="p">(</span><span class="n">tidymodels</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">themis</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">set.seed</span><span class="p">(</span><span class="m">1234</span><span class="p">)</span></span></span></code></pre></div></div>
<h2 id="new-sampling-steps">New sampling steps
</h2>
<p>This release adds support for 11 new steps for under- and over-sampling of data.
Making for a more complete picture of the commonly discussed methods in this category.
Each step also ships a direct-implementation counterpart (<code>enn()</code>, <code>smogn()</code>, and so on) for use outside a recipe.</p>
<p>The new steps fall into four groups.</p>
<p><strong>Cleaning-based under-sampling</strong> removes observations that sit in the wrong neighborhood:</p>
<ul>
<li><code>step_enn()</code> applies the <a href="https://scholar.google.com/scholar?&amp;as_sdt=0%2C7&amp;q=%22Asymptotic&#43;Properties&#43;of&#43;Nearest&#43;Neighbor&#43;Rules&#43;Using&#43;Edited&#43;Data%22&amp;btnG=" target="_blank" rel="noopener">Edited Nearest Neighbors rule</a>.</li>
<li><code>step_ncl()</code> applies the <a href="https://scholar.google.com/scholar?&amp;as_sdt=0%2C7&amp;q=%22Improving&#43;identification&#43;of&#43;difficult&#43;small&#43;classes&#43;by&#43;balancing&#43;class&#43;distribution%22&amp;btnG=" target="_blank" rel="noopener">Neighborhood Cleaning Rule</a>.</li>
<li><code>step_cnn()</code> uses <a href="https://scholar.google.com/scholar?&amp;as_sdt=0%2C7&amp;q=%22The&#43;condensed&#43;nearest&#43;neighbor&#43;rule%22&amp;btnG=" target="_blank" rel="noopener">Condensed Nearest Neighbors</a>.</li>
<li><code>step_oss()</code> uses <a href="https://scholar.google.com/scholar?&amp;as_sdt=0%2C7&amp;q=%22An&#43;Effective&#43;Over-sampling&#43;Method&#43;for&#43;Imbalanced&#43;Data&#43;Sets&#43;Classification%22&amp;btnG=" target="_blank" rel="noopener">One-Sided Selection</a>.</li>
</ul>
<p><strong>Selection-based under-sampling</strong> picks a smaller set of representatives instead:</p>
<ul>
<li><code>step_cluster_centroids()</code> replaces each class with <a href="https://scholar.google.com/scholar?&amp;as_sdt=0%2C7&amp;q=%22Clustering-based&#43;undersampling&#43;in&#43;class-imbalanced&#43;data%22&amp;btnG=" target="_blank" rel="noopener">one representative per k-means cluster</a>.</li>
<li><code>step_instance_hardness()</code> removes the <a href="https://scholar.google.com/scholar?&amp;as_sdt=0%2C7&amp;q=%22An&#43;instance&#43;level&#43;analysis&#43;of&#43;data&#43;complexity%22&amp;btnG=" target="_blank" rel="noopener">observations that are hardest to classify</a>.</li>
</ul>
<p><strong>Over-sampling</strong> gains three new variants:</p>
<ul>
<li><code>step_kmeans_smote()</code> generates new examples only inside <a href="https://scholar.google.com/scholar?&amp;as_sdt=0%2C7&amp;q=%22Improving&#43;imbalanced&#43;learning&#43;through&#43;a&#43;heuristic&#43;oversampling&#43;method&#43;based&#43;on&#43;k-means&#43;and&#43;SMOTE%22&amp;btnG=" target="_blank" rel="noopener">clusters where the minority class dominates</a>.</li>
<li><code>step_svmsmote()</code> concentrates them near the <a href="https://scholar.google.com/scholar?&amp;as_sdt=0%2C7&amp;q=%22Borderline&#43;over-sampling&#43;for&#43;imbalanced&#43;data&#43;classification%22&amp;btnG=" target="_blank" rel="noopener">decision boundary found by a support vector machine</a>.</li>
<li><code>step_smoten()</code> handles data where <a href="https://scholar.google.com/scholar?&amp;as_sdt=0%2C7&amp;q=%22Smote%3A&#43;Synthetic&#43;minority&#43;over-sampling&#43;technique%22&amp;btnG=" target="_blank" rel="noopener">every predictor is categorical</a>.</li>
</ul>
<p>Lastly we have a step that applies the SMOTE idea to regression:</p>
<ul>
<li><code>step_smogn()</code> <a href="https://scholar.google.com/scholar?&amp;as_sdt=0%2C7&amp;q=%22SMOGN%3A&#43;a&#43;pre-processing&#43;approach&#43;for&#43;imbalanced&#43;regression%22&amp;btnG=" target="_blank" rel="noopener">over-samples rare regions of a numeric outcome and under-samples the common ones</a>.</li>
</ul>
<p>The new <a href="https://themis.tidymodels.org/articles/" target="_blank" rel="noopener">&ldquo;Methods overview&rdquo;</a> article lays out the full taxonomy.</p>
<h2 id="sampling-targets-per-class">Sampling targets per class
</h2>
<p>The <code>over_ratio</code> and <code>under_ratio</code> arguments used by many of the steps in themis previously only took a single value.
We now accept a named vector,
such that you can specify the value for each level of the <code>outcome</code>.</p>
<p>The <code>penguins</code> data set has three species of unequal size.
A few penguins have missing measurements,
which we drop up front so the counts below are easier to follow:</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">data</span><span class="p">(</span><span class="n">penguins</span><span class="p">,</span> <span class="n">package</span> <span class="o">=</span> <span class="s">&#34;modeldata&#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">penguins</span> <span class="o">&lt;-</span> <span class="n">penguins</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">drop_na</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">count</span><span class="p">(</span><span class="n">penguins</span><span class="p">,</span> <span class="n">species</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code># A tibble: 3 × 2
  species       n
  &lt;fct&gt;     &lt;int&gt;
1 Adelie      146
2 Chinstrap    68
3 Gentoo      119
</code></pre>
<p>The names of the vector should correspond to the levels of the outcome.
So <code>over_ratio = c(Chinstrap = 0.8, Gentoo = 1)</code> brings Gentoo up to the size of the majority level,
and Chinstrap up to 80% of it:</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">recipe</span><span class="p">(</span><span class="n">species</span> <span class="o">~</span> <span class="n">bill_length_mm</span> <span class="o">+</span> <span class="n">bill_depth_mm</span> <span class="o">+</span> <span class="n">flipper_length_mm</span> <span class="o">+</span> <span class="n">body_mass_g</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">penguins</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_smote</span><span class="p">(</span><span class="n">species</span><span class="p">,</span> <span class="n">over_ratio</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="n">Chinstrap</span> <span class="o">=</span> <span class="m">0.8</span><span class="p">,</span> <span class="n">Gentoo</span> <span class="o">=</span> <span class="m">1</span><span class="p">))</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">prep</span><span class="p">()</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">bake</span><span class="p">(</span><span class="n">new_data</span> <span class="o">=</span> <span class="kc">NULL</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">count</span><span class="p">(</span><span class="n">species</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code># A tibble: 3 × 2
  species       n
  &lt;fct&gt;     &lt;int&gt;
1 Adelie      146
2 Chinstrap   117
3 Gentoo      146
</code></pre>
<p>Levels you don&rsquo;t name are left untouched,
as are rows with a missing outcome.
Two things to keep in mind:
supplying a vector means the argument can no longer be tuned,
and <code>step_rose()</code> still requires a single number,
because its <code>over_ratio</code> scales the size of the total generated sample rather than setting a per-class target.
The new article on <a href="https://themis.tidymodels.org/articles/" target="_blank" rel="noopener"><code>over_ratio</code> and <code>under_ratio</code></a> walks through both arguments in more detail.</p>
<h2 id="tracking-synthetic-rows">Tracking synthetic rows
</h2>
<p>Every up-sampling step gains an <code>indicator_column</code> argument.
Give it a name and the final data gets a logical column marking which rows the step added:</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">smote_res</span> <span class="o">&lt;-</span> <span class="nf">recipe</span><span class="p">(</span><span class="n">class</span> <span class="o">~</span> <span class="n">x</span> <span class="o">+</span> <span class="n">y</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">circle_example</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_smote</span><span class="p">(</span><span class="n">class</span><span class="p">,</span> <span class="n">indicator_column</span> <span class="o">=</span> <span class="s">&#34;synthetic&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">prep</span><span class="p">()</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">bake</span><span class="p">(</span><span class="n">new_data</span> <span class="o">=</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="nf">count</span><span class="p">(</span><span class="n">smote_res</span><span class="p">,</span> <span class="n">class</span><span class="p">,</span> <span class="n">synthetic</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code># A tibble: 3 × 3
  class  synthetic     n
  &lt;fct&gt;  &lt;lgl&gt;     &lt;int&gt;
1 Circle FALSE        58
2 Circle TRUE        284
3 Rest   FALSE       342
</code></pre>
<p>This will mostly be useful as a diagnostic tool,
or to help visualize how these methods work in practice.</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">smote_res</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">ggplot</span><span class="p">(</span><span class="nf">aes</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">color</span> <span class="o">=</span> <span class="n">synthetic</span><span class="p">))</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">  <span class="nf">geom_point</span><span class="p">(</span><span class="n">alpha</span> <span class="o">=</span> <span class="m">0.7</span><span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">  <span class="nf">labs</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;Synthetic minority observations created by step_smote()&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">color</span> <span class="o">=</span> <span class="s">&#34;Synthetic&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">  <span class="nf">theme_minimal</span><span class="p">()</span></span></span></code></pre></div></div>
<img src="https://opensource.posit.co/blog/2026-08-13_themis-1-1-0/index.markdown_strict_files/figure-markdown_strict/unnamed-chunk-6-1.png" width="768" />
<h2 id="more-distance-metrics">More distance metrics
</h2>
<p>Most of the steps in this package are built on some calculation that has to do with nearest neighbors.
And so far all of them had been using Euclidean distances.
We have added a <code>distance</code> argument to every step that deals with neighbors, letting you choose a different distance metric.</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">manhattan_res</span> <span class="o">&lt;-</span> <span class="nf">recipe</span><span class="p">(</span><span class="n">class</span> <span class="o">~</span> <span class="n">x</span> <span class="o">+</span> <span class="n">y</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">circle_example</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_smote</span><span class="p">(</span><span class="n">class</span><span class="p">,</span> <span class="n">distance</span> <span class="o">=</span> <span class="s">&#34;manhattan&#34;</span><span class="p">,</span> <span class="n">indicator_column</span> <span class="o">=</span> <span class="s">&#34;synthetic&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">prep</span><span class="p">()</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">bake</span><span class="p">(</span><span class="n">new_data</span> <span class="o">=</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="nf">count</span><span class="p">(</span><span class="n">manhattan_res</span><span class="p">,</span> <span class="n">class</span><span class="p">,</span> <span class="n">synthetic</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code># A tibble: 3 × 3
  class  synthetic     n
  &lt;fct&gt;  &lt;lgl&gt;     &lt;int&gt;
1 Circle FALSE        58
2 Circle TRUE        284
3 Rest   FALSE       342
</code></pre>
<p>The class counts are unchanged,
since those are set by <code>over_ratio</code> rather than by the metric.
What changes is where the synthetic observations land,
because a different metric picks different nearest neighbors to interpolate between.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thank you to everyone who has contributed issues, pull requests, and discussion since the last release!
<a href="https://github.com/3styleJam" target="_blank" rel="noopener">@3styleJam</a>, <a href="https://github.com/Dodothereal" target="_blank" rel="noopener">@Dodothereal</a>, <a href="https://github.com/EmilHvitfeldt" target="_blank" rel="noopener">@EmilHvitfeldt</a>, <a href="https://github.com/FvD" target="_blank" rel="noopener">@FvD</a>, <a href="https://github.com/jeroenjanssens" target="_blank" rel="noopener">@jeroenjanssens</a>, <a href="https://github.com/SAY-5" target="_blank" rel="noopener">@SAY-5</a>, and <a href="https://github.com/topepo" target="_blank" rel="noopener">@topepo</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-08-13_themis-1-1-0/thumbnail.jpg" length="186394" type="image/jpeg" />
    </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>roxygen2 8.1.0</title>
      <link>https://opensource.posit.co/blog/2026-08-04_roxygen2-8-1-0/</link>
      <pubDate>Tue, 04 Aug 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-08-04_roxygen2-8-1-0/</guid>
      <dc:creator>Hadley Wickham</dc:creator><description><![CDATA[<p>I&rsquo;m well chuffed to announce that <a href="https://roxygen2.r-lib.org" target="_blank" rel="noopener">roxygen2 8.1.0</a> is now on CRAN.
roxygen2 turns specially formatted comments in your R code into the <code>.Rd</code> files that power R&rsquo;s help system.
You can install it with:</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;roxygen2&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>This is a small release, and the two main changes are about performance: one makes your package load faster for your users, and one makes documenting faster for you.
You can see the full list of changes in the <a href="https://github.com/r-lib/roxygen2/releases/tag/v8.1.0" target="_blank" rel="noopener">release notes</a>.</p>
<h2 id="merged-importfrom-directives">Merged <code>importFrom()</code> directives
</h2>
<p><code>@importFrom</code> used to generate one <code>importFrom()</code> directive per symbol.
Now it generates a single multi-line directive per package:</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"># before</span>
</span></span><span class="line"><span class="cl"><span class="nf">importFrom</span><span class="p">(</span><span class="n">utils</span><span class="p">,</span><span class="n">URLdecode</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">importFrom</span><span class="p">(</span><span class="n">utils</span><span class="p">,</span><span class="n">URLencode</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">importFrom</span><span class="p">(</span><span class="n">utils</span><span class="p">,</span><span class="n">head</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">importFrom</span><span class="p">(</span><span class="n">utils</span><span class="p">,</span><span class="n">tail</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="nf">importFrom</span><span class="p">(</span><span class="n">utils</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">URLdecode</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">URLencode</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">head</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">tail</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p>We made this change because we discovered that the number of <code>importFrom()</code> directives in your <code>NAMESPACE</code> has a surprisingly large effect on loading time.
<code>loadNamespace()</code> does a chunk of per-directive work, so the cost grows with the number of directives, not the number of symbols.
Importing 1,000 functions with 1,000 separate directives costs around 120ms; importing the same 1,000 functions with one directive per package costs around 9ms, the same as importing 10.</p>
<p>Of course, most packages import far fewer than 1,000 objects.
But <code>library()</code> costs are paid by every user, on every session, and they&rsquo;re cumulative: loading a package also loads its dependencies.
Merging the directives removes the cost for the user entirely, at little cost to you as the developer: just one noisy <code>NAMESPACE</code> diff.</p>
<p>My colleague Kevin Ushey also fixed a related annoyance: <code>@importFrom</code>, <code>@importClassesFrom</code>, and <code>@importMethodsFrom</code> once again accept multi-line input, so you can spread a long list of imports over several lines for readability.
You&rsquo;ll just need a hanging indent on the continuation lines.</p>
<h2 id="faster-documenting-with-rdtools">Faster documenting with rdtools
</h2>
<p>The other change helps developers, rather than users: documenting a package with lots of cross-references is now considerably faster.</p>
<p>The bottleneck was topic resolution: every time roxygen2 sees a link like <code>[dplyr::filter()]</code>, or needs to work out which package an unqualified <code>[filter()]</code> refers to, it has to ask &ldquo;does this package have a topic with this alias?&rdquo;.
Previously roxygen2 answered that question by calling <code>utils::help()</code>, which takes about 1.5ms.
That doesn&rsquo;t sound like much, but to resolve an unqualified link roxygen2 has to check every dependency plus the base packages, which adds up.
On a link-heavy package like testthat, that meant roughly half of the total time spent documenting was just looking up cross-references.</p>
<p>The fix is to stop asking one question at a time, and to cache each lookup.
Installed packages already ship an alias index (<code>help/aliases.rds</code>), and packages loaded from source have one maintained by pkgload, so all the information needed is available in bulk &mdash; it just needs to be read once and kept in a hash table, rather than rediscovered on every query.</p>
<p>That logic now lives in a new package, <a href="https://rdtools.r-lib.org" target="_blank" rel="noopener">rdtools</a>, which provides cached lookup of help topics and aliases across installed, source, and in-development packages, along with efficient retrieval of parsed <code>Rd</code> objects.
Pulling it out of roxygen2 deleted a decent amount of fiddly code that I&rsquo;d also implemented (to varying levels of quality) in pkgdown and downlit.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thanks to everyone who helped make this release possible! <a href="https://github.com/aphalo" target="_blank" rel="noopener">@aphalo</a>, <a href="https://github.com/DavisVaughan" target="_blank" rel="noopener">@DavisVaughan</a>, <a href="https://github.com/dragosmg" target="_blank" rel="noopener">@dragosmg</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/hadley" target="_blank" rel="noopener">@hadley</a>, <a href="https://github.com/jan-swissre" target="_blank" rel="noopener">@jan-swissre</a>, <a href="https://github.com/jemus42" target="_blank" rel="noopener">@jemus42</a>, <a href="https://github.com/jranke" target="_blank" rel="noopener">@jranke</a>, <a href="https://github.com/kevinushey" target="_blank" rel="noopener">@kevinushey</a>, <a href="https://github.com/lionel-" target="_blank" rel="noopener">@lionel-</a>, <a href="https://github.com/mcol" target="_blank" rel="noopener">@mcol</a>, <a href="https://github.com/MichaelChirico" target="_blank" rel="noopener">@MichaelChirico</a>, <a href="https://github.com/peterdesmet" target="_blank" rel="noopener">@peterdesmet</a>, and <a href="https://github.com/thisisnic" target="_blank" rel="noopener">@thisisnic</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-08-04_roxygen2-8-1-0/thumbnail.jpg" length="158270" 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>
