<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Erik Hanchett</title>
    <description>The latest articles on DEV Community by Erik Hanchett (@erikch).</description>
    <link>https://dev.to/erikch</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1994%2F0j84YwMs.jpeg</url>
      <title>DEV Community: Erik Hanchett</title>
      <link>https://dev.to/erikch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/erikch"/>
    <language>en</language>
    <item>
      <title>Nuxt CLI v4 Is Great for AI Coding Agents</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Wed, 04 Nov 2026 18:59:04 +0000</pubDate>
      <link>https://dev.to/erikch/nuxt-cli-v4-is-great-for-ai-coding-agents-5dhg</link>
      <guid>https://dev.to/erikch/nuxt-cli-v4-is-great-for-ai-coding-agents-5dhg</guid>
      <description>&lt;p&gt;Have you ever had a dev server running, and then asked your coding agent to test something in that app? It starts its own server on port 3001. Then it restarts that one, and you end up with another on 3002. Pretty soon you've lost track of what's running on which port and what you need to stop...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nuxt/cli/releases/tag/v4.0.0" rel="noopener noreferrer"&gt;Nuxt CLI v4&lt;/a&gt; shipped alongside &lt;a href="https://nuxt.com/blog/v4-6" rel="noopener noreferrer"&gt;Nuxt 4.6&lt;/a&gt; in early October, and a few of its new features fix that exact problem. The Nuxt team pitched this release as a speed and dev experience update. Nuxt's own numbers put the CLI install at 3.5 MB (down from 13.1 MB) and first paint for &lt;code&gt;nuxt dev&lt;/code&gt; at 50 ms (down from 330 ms). However, what I'm really interested in is some of the agentic features.&lt;/p&gt;

&lt;p&gt;I tried them on a small Nuxt 4.6.4 todo app. Here's the video if you'd rather watch:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/XdWNtDoLBaM" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 22.21+, 24.11+, or 26+. CLI v4 won't run on older versions.&lt;/li&gt;
&lt;li&gt;A Nuxt project on 4.6 or later. CLI v4 is a dependency of &lt;code&gt;nuxt&lt;/code&gt;, so upgrading Nuxt gets you the new CLI. I tested &lt;code&gt;nuxt&lt;/code&gt; 4.6.4 with &lt;code&gt;@nuxt/cli&lt;/code&gt; 4.0.3.&lt;/li&gt;
&lt;li&gt;Optional, a coding agent that can run shell commands.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Meet the new nuxt dev
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;npm run dev&lt;/code&gt; and the first thing you'll notice is the panel pinned to the bottom of your terminal. It shows your Nuxt version, the local URL, a request counter, and a row of single-key shortcuts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F00krytzh5ddo4jmqobn7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F00krytzh5ddo4jmqobn7.jpg" alt="The new nuxt dev terminal UI showing Nuxt 4.6.4, the local URL, 36 requests with an 11ms median, a READY badge, and shortcuts for restart, open, info, logs, network, routes, help, and quit" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The new nuxt dev panel. Each letter at the bottom is a shortcut.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;r&lt;/code&gt; restarts the server, &lt;code&gt;o&lt;/code&gt; opens it in the browser, &lt;code&gt;i&lt;/code&gt; shows info about your setup, and &lt;code&gt;n&lt;/code&gt; lists every request with its status and timing. Press &lt;code&gt;p&lt;/code&gt; for all your pages and server routes. If you'd rather have plain log output, &lt;code&gt;nuxt dev --no-tui&lt;/code&gt; turns the panel off.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Check the lock file
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;nuxt dev&lt;/code&gt; starts, it writes &lt;code&gt;.nuxt/nuxt.lock&lt;/code&gt;. When you stop the server, the file goes away. Here's mine:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh0y1wpip0pv8rfg6zp84.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh0y1wpip0pv8rfg6zp84.jpg" alt="The .nuxt/nuxt.lock file open in the editor with pid, startedAt, interactive, command, cwd, parentPid, port 8231, hostname 127.0.0.1, and url fields" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;.nuxt/nuxt.lock has everything an agent needs to find the running server.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The PID, port, and URL tell an agent that a server is already up and where to find it. It doesn't have to probe ports or guess at &lt;code&gt;localhost:3000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;interactive&lt;/code&gt; field records whether the server was started from an interactive terminal. Nuxt uses it to decide what happens when a second &lt;code&gt;nuxt dev&lt;/code&gt; shows up. In my testing, a server started from a real terminal wouldn't hand itself over to a non-interactive &lt;code&gt;nuxt dev&lt;/code&gt; (the kind an agent runs). The agent gets a message pointing it at &lt;code&gt;--takeover&lt;/code&gt; instead. A server that was started non-interactively gets taken over on the same port, so an agent that restarts its own server doesn't leave the old one running.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Run nuxt dev a second time
&lt;/h2&gt;

&lt;p&gt;If you run &lt;code&gt;npm run dev&lt;/code&gt; again while a server is up, Nuxt stops and asks what you want:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgkg5udeo7dvku6b65mu.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgkg5udeo7dvku6b65mu.jpg" alt="Terminal output reading &amp;quot;A Nuxt dev server is already running here (PID 107817, http://127.0.0.1:8231)&amp;quot; followed by three options, Take over port 8231, Do not start, and Start anyway" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Taking over stops the running server. Start anyway gives you a second one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Take over stops the running server and starts yours on the same port. That's the &lt;code&gt;--takeover&lt;/code&gt; flag. Start anyway runs a second server, which you can also force with &lt;code&gt;NUXT_IGNORE_LOCK=1&lt;/code&gt;. Nuxt calls that unsupported.&lt;/p&gt;

&lt;p&gt;The CLI already warns an agent about all of this, but I still wanted the rules written down. So I added a &lt;a href="https://kiro.dev/docs/steering/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;steering file&lt;/a&gt; at &lt;code&gt;.kiro/steering/nuxt-dev-server.md&lt;/code&gt;. If you use a different agent, put the same thing in &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;inclusion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Nuxt dev server rules&lt;/span&gt;

This project uses Nuxt 4.6 with Nuxt CLI v4.
&lt;span class="p"&gt;
-&lt;/span&gt; Never start a second &lt;span class="sb"&gt;`nuxt dev`&lt;/span&gt;. A dev server is usually already running. Check &lt;span class="sb"&gt;`.nuxt/nuxt.lock`&lt;/span&gt; for its URL.
&lt;span class="p"&gt;-&lt;/span&gt; Never pass &lt;span class="sb"&gt;`--takeover`&lt;/span&gt; or set &lt;span class="sb"&gt;`NUXT_IGNORE_LOCK=1`&lt;/span&gt; unless I ask.
&lt;span class="p"&gt;-&lt;/span&gt; To test API routes, use &lt;span class="sb"&gt;`npx nuxt curl &amp;lt;path&amp;gt;`&lt;/span&gt; (add &lt;span class="sb"&gt;`-X`&lt;/span&gt;, &lt;span class="sb"&gt;`-d`&lt;/span&gt;, &lt;span class="sb"&gt;`-i`&lt;/span&gt; as needed). It finds the running server without a port.
&lt;span class="p"&gt;-&lt;/span&gt; To run Nitro tasks, use &lt;span class="sb"&gt;`npx nuxt task list`&lt;/span&gt; and &lt;span class="sb"&gt;`npx nuxt task run &amp;lt;name&amp;gt; --payload.&amp;lt;key&amp;gt;=&amp;lt;value&amp;gt;`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Before using a Nuxt API you are unsure about, run &lt;span class="sb"&gt;`npx nuxt docs "&amp;lt;query&amp;gt;"`&lt;/span&gt; to read the docs for the installed version.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Test routes with nuxt curl
&lt;/h2&gt;

&lt;p&gt;If the agent can't start its own server, it still needs a way to test things. That's what &lt;code&gt;nuxt curl&lt;/code&gt; is for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nuxt curl /api/todos
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs2bb8wckawryec4igfn9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs2bb8wckawryec4igfn9.jpg" alt="Terminal running npx nuxt curl /api/todos and printing a formatted JSON array with two todos, Upgrade to Nuxt 4.6 and Let Kiro find my dev server" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;No host, no port. nuxt curl reads the lock file.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You pass a path, not a URL. &lt;code&gt;nuxt curl&lt;/code&gt; reads the lock file, finds the running server, and pretty-prints the JSON it gets back. It takes the usual curl flags too, so a POST looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nuxt curl /api/todos &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"title":"Added by nuxt curl"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;-i&lt;/code&gt; to see the response headers. If no server is running, it tells you to start one with &lt;code&gt;nuxt dev&lt;/code&gt; or pass an absolute URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Run Nitro tasks with nuxt task
&lt;/h2&gt;

&lt;p&gt;The other command I liked is &lt;code&gt;nuxt task&lt;/code&gt;, which runs &lt;a href="https://nitro.build/guide/tasks" rel="noopener noreferrer"&gt;Nitro tasks&lt;/a&gt; against the running server. Tasks are still experimental in Nitro, so turn them on in &lt;code&gt;nuxt.config.ts&lt;/code&gt; first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;nitro&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My app has a seed task in &lt;code&gt;server/tasks/db/seed.ts&lt;/code&gt; that resets the in-memory todos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineTask&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;db:seed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Replace the in-memory todos with N seeded todos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;todos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resetTodos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Seeded &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; todos`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then list and run it from the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nuxt task list
npx nuxt task run db:seed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F14h42of8d1eyjbyl89ii.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F14h42of8d1eyjbyl89ii.jpg" alt="Terminal showing npx nuxt task list returning db:seed, a failed npx nuxt task db:seed with an Unknown command error and usage help, then npx nuxt task run db:seed printing Seeded 5 todos" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;I forgot the run subcommand the first time. The help output got me there.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can see I typed &lt;code&gt;npx nuxt task db:seed&lt;/code&gt; first and got an error. It needs &lt;code&gt;run&lt;/code&gt;. Pass a payload with &lt;code&gt;--payload.count=10&lt;/code&gt; and you get 10 todos instead of the default 5.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. Hand errors to your agent
&lt;/h2&gt;

&lt;p&gt;I really like this the most, because I run into this all the time! In my example my app has a &lt;code&gt;/profile&lt;/code&gt; page that's broken on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Erik&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="nx"&gt;as&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load it and Nuxt shows the new error overlay. You get the error, the exact line in &lt;code&gt;profile.vue&lt;/code&gt;, a toggle between source and compiled code, and a call stack with the 9 framework frames hidden by default.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fym36xuxi42y8qsm5szt4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fym36xuxi42y8qsm5szt4.jpg" alt="The Nuxt error overlay showing TypeError Cannot read properties of null (reading 'handle') at app/pages/profile.vue:5:30, with the Copy error menu open listing Copy as Markdown, Copy prompt for an agent, and Copy structured JSON" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Copy error menu has an option written for agents.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The new part is the Copy error menu. Copy as Markdown and Copy structured JSON do what they sound like. Copy prompt for an agent wraps the error, source location, Node version, and runtime details in instructions to diagnose the error, trace the root cause, and explain the failure.&lt;/p&gt;

&lt;p&gt;I pasted that prompt into Kiro, running Claude Opus 5.5, and let it go. Because of the steering file, it didn't start a server. It ran &lt;code&gt;npx nuxt curl /profile&lt;/code&gt; to confirm the 500, changed the non-null assertion to optional chaining and added a &lt;code&gt;v-if&lt;/code&gt; so the template skips the missing handle, then ran &lt;code&gt;nuxt curl&lt;/code&gt; again to confirm a 200.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8m4ip7h6fp9y3otua91d.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8m4ip7h6fp9y3otua91d.jpg" alt="Kiro chat panel showing the verification steps. Before, npx nuxt curl /profile returned 500 with the profile.vue:5:30 stack trace. After, it returns 200 and renders the section. The file has no type errors" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Kiro checked its own fix with nuxt curl against my running server.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The whole thing took about 36 seconds and one credit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;I'm really liking this new version of Nuxt CLI! I'd like to see more frameworks do this. Have you had an agent take over your dev server, or start five of them? Let me know in the comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nuxt.com/blog/v4-6" rel="noopener noreferrer"&gt;Nuxt 4.6 release post&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nuxt/cli/releases/tag/v4.0.0" rel="noopener noreferrer"&gt;Nuxt CLI v4.0.0 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nitro.build/guide/tasks" rel="noopener noreferrer"&gt;Nitro tasks docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kiro.dev/docs/steering/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro steering docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kiro.dev/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=XdWNtDoLBaM" rel="noopener noreferrer"&gt;The video on YouTube&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nuxt</category>
      <category>vue</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Vite+ 1.0 Is Out. You Don't Need ESLint or Prettier Anymore</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Wed, 28 Oct 2026 14:48:04 +0000</pubDate>
      <link>https://dev.to/erikch/vite-10-is-out-you-dont-need-eslint-or-prettier-anymore-3iko</link>
      <guid>https://dev.to/erikch/vite-10-is-out-you-dont-need-eslint-or-prettier-anymore-3iko</guid>
      <description>&lt;p&gt;Do you still need ESLint and Prettier? The team behind Vite+ says no. &lt;a href="https://voidzero.dev/posts/announcing-vite-plus-1-0" rel="noopener noreferrer"&gt;Vite+ 1.0&lt;/a&gt; shipped on September 28, and it puts Vite, Vitest, linting, formatting, Git hooks, and Node.js version management behind one CLI called &lt;code&gt;vp&lt;/code&gt; and one &lt;code&gt;vite.config.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I &lt;a href="https://dev.to/erikch/i-tried-vite-and-replaced-my-entire-frontend-toolchain-4cgb"&gt;wrote about Vite+&lt;/a&gt; back in March when it was still in alpha. It's grown up a lot since then. It's MIT licensed, it's stable, and it's close to 2 million weekly downloads. So I took a small Vue app that uses the usual pile of tools and migrated it to see how much of it Vite+ could swallow.&lt;/p&gt;

&lt;p&gt;If you'd rather watch, here's the video:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/gGM3UTiL5Gc" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;(Full disclosure: I'm a Developer Advocate at AWS, and &lt;a href="https://kiro.dev/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; is an AWS product. I use it to finish the migration below, but Claude Code or Codex would do the same job.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What Vite+ replaces
&lt;/h2&gt;

&lt;p&gt;Vite+ comes from &lt;a href="https://voidzero.dev/" rel="noopener noreferrer"&gt;VoidZero&lt;/a&gt;, the team behind Vite, Vitest, Rolldown, and Oxc. It isn't a new framework and it doesn't replace Vite. It ties a set of tools together and tests them as one stack, so a single &lt;code&gt;vite-plus&lt;/code&gt; dependency takes the place of packages like &lt;code&gt;eslint&lt;/code&gt;, &lt;code&gt;prettier&lt;/code&gt;, &lt;code&gt;vitest&lt;/code&gt;, &lt;code&gt;tsup&lt;/code&gt;, and &lt;code&gt;turbo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The commands map to those tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;vp check&lt;/code&gt; formats, lints, and type checks in one pass with &lt;a href="https://oxc.rs/docs/guide/usage/formatter.html" rel="noopener noreferrer"&gt;Oxfmt&lt;/a&gt; and &lt;a href="https://oxc.rs/docs/guide/usage/linter.html" rel="noopener noreferrer"&gt;Oxlint&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vp test&lt;/code&gt; runs &lt;a href="https://vitest.dev/" rel="noopener noreferrer"&gt;Vitest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vp dev&lt;/code&gt; and &lt;code&gt;vp build&lt;/code&gt; run Vite 8 with &lt;a href="https://rolldown.rs/" rel="noopener noreferrer"&gt;Rolldown&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vp env&lt;/code&gt; manages your Node.js version&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vp migrate&lt;/code&gt; moves an existing project over&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Oxc tools are written in Rust. VoidZero says Oxlint runs 50x to 100x faster than ESLint and Oxfmt is up to 30x faster than Prettier. I didn't benchmark those claims, but I'll share the times I saw later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;macOS, Linux, or Windows. I'm on Windows Subsystem for Linux (WSL) with Ubuntu 24.04.&lt;/li&gt;
&lt;li&gt;An existing Vite project. Mine is a Vue 3 app, but React and other frameworks work too.&lt;/li&gt;
&lt;li&gt;A clean Git working tree. &lt;code&gt;vp migrate&lt;/code&gt; edits a lot of files, so commit first.&lt;/li&gt;
&lt;li&gt;Optional, a coding agent to clean up whatever the migration misses.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The project before Vite+
&lt;/h2&gt;

&lt;p&gt;My test app is a vibe-coded park planner where you save trips and the parks you want to visit. It started out with a pretty typical setup. ESLint for linting, Prettier for formatting, &lt;code&gt;vue-tsc&lt;/code&gt; for type checks, Husky and lint-staged for the pre-commit hook, pnpm as the package manager, and separate &lt;code&gt;vite.config.ts&lt;/code&gt; and &lt;code&gt;vitest.config.ts&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;That's a lot of config to keep in sync. Here's the &lt;code&gt;scripts&lt;/code&gt; block from &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"run-p type-check &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;build-only {@}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; --"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"preview"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite preview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"test:unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vitest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build-only"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type-check"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vue-tsc --build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eslint . --fix --cache"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lint:check"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eslint ."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prettier --write src/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format:check"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prettier --check src/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prepare"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"husky"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of those tools has its own release cadence and its own upgrade guide. I also use NVM to switch Node versions, since I jump between a lot of projects for videos.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Install Vite+
&lt;/h2&gt;

&lt;p&gt;On macOS or Linux, run the install script from &lt;a href="https://viteplus.dev/" rel="noopener noreferrer"&gt;viteplus.dev&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://vite.plus | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows, use PowerShell instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;irm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://vite.plus/ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer asks if you want Vite+ to manage Node.js for you. I said no to this during the alpha because it fought with my other tools. This time I took the default and said yes. After that, open a new shell (or source your &lt;code&gt;.zshrc&lt;/code&gt;) so &lt;code&gt;vp&lt;/code&gt; is on your path.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Run vp migrate
&lt;/h2&gt;

&lt;p&gt;From the project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vp migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It walks you through a short list of questions before it changes anything. Do you want pre-commit hooks for formatting and linting? Which agent instructions file should it write? Which editor config? Then it asks whether to migrate ESLint rules to Oxlint (it uses &lt;code&gt;@oxlint/migrate&lt;/code&gt; for this) and Prettier to Oxfmt.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2q8dzgueo8m8k1k30eix.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2q8dzgueo8m8k1k30eix.jpg" alt="The vp migrate prompts in the terminal, asking to migrate ESLint rules to Oxlint and Prettier to Oxfmt" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;vp migrate asks before it converts your ESLint and Prettier configs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I said yes to the hooks, picked &lt;code&gt;AGENTS.md&lt;/code&gt; (Kiro reads it), chose VS Code since the Kiro IDE is built on it, and let it migrate Prettier, ESLint, and the TypeScript shims.&lt;/p&gt;

&lt;p&gt;When it finished, I ran the check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vp check
vp check &lt;span class="nt"&gt;--fix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;vp check&lt;/code&gt; found some formatting issues, and &lt;code&gt;--fix&lt;/code&gt; cleaned up most of them. A few things were left over. My ESLint rules were now in &lt;code&gt;vite.config.ts&lt;/code&gt; under a &lt;code&gt;lint&lt;/code&gt; block, and &lt;code&gt;eslint.config.ts&lt;/code&gt; was gone. But &lt;code&gt;vitest.config.ts&lt;/code&gt; was still around, and there was one lint error it couldn't fix on its own.&lt;/p&gt;

&lt;p&gt;VoidZero says up front that larger projects might need a follow-up after &lt;code&gt;vp migrate&lt;/code&gt;, so I wasn't surprised.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Let your agent finish the migration
&lt;/h2&gt;

&lt;p&gt;Instead of fixing those by hand, I asked Kiro, running Claude Opus 5.5:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I just used Vite+ to migrate this project. Can you complete the migration for Vitest and this error?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have Kiro, Claude Code, or Codex open already, I don't see a reason to do this part manually. The &lt;code&gt;AGENTS.md&lt;/code&gt; file that &lt;code&gt;vp migrate&lt;/code&gt; writes explains the &lt;code&gt;vp&lt;/code&gt; commands, so the agent doesn't have to guess. VoidZero also publishes a migration prompt for coding agents, linked from the 1.0 announcement.&lt;/p&gt;

&lt;p&gt;It finished in under a minute. It moved the &lt;code&gt;test&lt;/code&gt; block from &lt;code&gt;vitest.config.ts&lt;/code&gt; into &lt;code&gt;vite.config.ts&lt;/code&gt; and deleted the old file. It removed &lt;code&gt;vitest.config.*&lt;/code&gt; from the &lt;code&gt;include&lt;/code&gt; list in &lt;code&gt;tsconfig.node.json&lt;/code&gt;. The lint error was the &lt;code&gt;typescript/no-empty-object-type&lt;/code&gt; rule in &lt;code&gt;env.d.ts&lt;/code&gt;. It fixed it by changing &lt;code&gt;DefineComponent&amp;lt;{}, {}, unknown&amp;gt;&lt;/code&gt; to &lt;code&gt;DefineComponent&amp;lt;object, object, unknown&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then I ran the check again:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fux1plss6ubps7ybp02vo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fux1plss6ubps7ybp02vo.jpg" alt="vp check output reporting all 39 files correctly formatted in 731ms and no warnings, lint errors, or type errors in 27 files" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Formatting, linting, and type checks in one command.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;All 39 files were formatted correctly in 731ms, and it found no warnings, lint errors, or type errors in 27 files in 782ms. All 16 tests passed with &lt;code&gt;vp test&lt;/code&gt;, and the dev server still ran after a restart.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Replace Husky with Vite+ hooks
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;package.json&lt;/code&gt; still had &lt;code&gt;"prepare": "husky"&lt;/code&gt;, so I asked Kiro to move that over too. Vite+ has its own hooks now. &lt;code&gt;vp hooks&lt;/code&gt; sets up Git hooks, and &lt;code&gt;vp staged&lt;/code&gt; runs your checks on staged files only.&lt;/p&gt;

&lt;p&gt;Kiro removed the &lt;code&gt;.husky/&lt;/code&gt; folder, added a &lt;code&gt;staged&lt;/code&gt; block to &lt;code&gt;vite.config.ts&lt;/code&gt;, and ran &lt;code&gt;vp hooks enable&lt;/code&gt;. The hook script lives in &lt;code&gt;.vite-hooks/pre-commit&lt;/code&gt; now, and you commit it with the rest of the project.&lt;/p&gt;

&lt;p&gt;It also flagged something I would have missed. Husky sets &lt;code&gt;core.hooksPath&lt;/code&gt; to &lt;code&gt;.husky/&lt;/code&gt; in your local Git config. Anyone with an existing clone still has that setting, so the new hooks won't install for them until they run this once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config &lt;span class="nt"&gt;--unset&lt;/span&gt; core.hooksPath &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; vp hooks &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put that in your PR description if you're migrating a team project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's left
&lt;/h2&gt;

&lt;p&gt;After all that, one &lt;code&gt;vite.config.ts&lt;/code&gt; holds the linting, formatting, testing, and staged-file config:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjyg1hg6ml6dd6cqoqbdx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjyg1hg6ml6dd6cqoqbdx.png" alt="Before and after diagram. Before lists eslint.config.ts, .prettierrc.json, vitest.config.ts, .husky plus lint-staged, vue-tsc, and nvm. After shows one vite.config.ts with lint, fmt, test, and staged blocks, plus the vp check, vp test, and vp env commands" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;vp toolchain&lt;/code&gt; to see exactly what you're getting. Mine showed Vite 8.3.5, Rolldown 1.2.15, Vitest 5.0.5, Oxlint 1.89.0, Oxfmt 0.74.0, and tsdown 0.23.1, all pinned by the one &lt;code&gt;vite-plus&lt;/code&gt; package. That's the upgrade story I like. You bump one version, and VoidZero has already tested those tools together.&lt;/p&gt;

&lt;p&gt;For Node, &lt;code&gt;vp env current&lt;/code&gt; shows the Node.js and pnpm versions the project resolves to. If something looks off, &lt;code&gt;vp env doctor&lt;/code&gt; explains why it picked them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rolling it back
&lt;/h2&gt;

&lt;p&gt;This is why you commit before running &lt;code&gt;vp migrate&lt;/code&gt;. If it doesn't work out, go back to that commit, delete the new files like &lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;.vite-hooks/&lt;/code&gt;, and reinstall your dependencies. If you let Vite+ manage Node.js, you can switch back to NVM in your shell config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;The migration was pretty painless. &lt;code&gt;vp migrate&lt;/code&gt; did most of the work, and the agent cleaned up the Vitest config, one lint error, and the Husky hooks in a couple of minutes. I'm going to start using Vite+ on most of my projects, and I'm happy to drop NVM.&lt;/p&gt;

&lt;p&gt;It isn't feature complete. Remote caching, &lt;code&gt;vp release&lt;/code&gt;, and &lt;code&gt;vp docs&lt;/code&gt; are still on the roadmap. If you're migrating something in production, read the migration guide on &lt;a href="https://viteplus.dev/" rel="noopener noreferrer"&gt;viteplus.dev&lt;/a&gt; first.&lt;/p&gt;

&lt;p&gt;Are you switching? Let me know in the comments, especially if you've tried it on a big monorepo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://viteplus.dev/" rel="noopener noreferrer"&gt;Vite+ docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://voidzero.dev/posts/announcing-vite-plus-1-0" rel="noopener noreferrer"&gt;Announcing Vite+ 1.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/voidzero-dev/vite-plus" rel="noopener noreferrer"&gt;Vite+ on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/erikch/i-tried-vite-and-replaced-my-entire-frontend-toolchain-4cgb"&gt;My first Vite+ post from the alpha&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kiro.dev/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>vite</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>vue</category>
    </item>
    <item>
      <title>2 Agent Skills Every Frontend Developer Should Try</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 20 Oct 2026 18:15:41 +0000</pubDate>
      <link>https://dev.to/erikch/2-agent-skills-every-frontend-developer-should-try-16pl</link>
      <guid>https://dev.to/erikch/2-agent-skills-every-frontend-developer-should-try-16pl</guid>
      <description>&lt;p&gt;I love to try out new agent skills. Anything that can help me be more productive I'm all for. Today I found two new ones I want to share with you. &lt;/p&gt;

&lt;p&gt;First one is Impeccable. Impeccable took a plain, AI-built website and gave it a design. It wasn't bad at all! Diagram Design took that same site and drew a diagram of how it works! Both install with one command, and both work with basically any coding harness. I used &lt;a href="https://kiro.dev/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; in my demos but you can use whatever you like.&lt;/p&gt;

&lt;p&gt;I made a video walking through both if you'd rather watch:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/JpZctil7tjE" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An agent that supports &lt;a href="https://kiro.dev/docs/skills/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Agent Skills&lt;/a&gt;. I use Kiro, but Claude Code, Cursor, and Codex work too.&lt;/li&gt;
&lt;li&gt;Node.js, so you can run &lt;code&gt;npx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A frontend project to try them on. Mine is a small website about cats.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Finding skills
&lt;/h2&gt;

&lt;p&gt;I found both skills on &lt;a href="https://www.skillleaderboard.com" rel="noopener noreferrer"&gt;Skill Leaderboard&lt;/a&gt;, which ranks skill repos by GitHub stars. You can filter by compatible agent, so I set it to Kiro. Skills are like recipes that work across most coding harnesses, so you'll usually be fine without the filter. Some repos are easier to install in one agent than another, though, and the filter saves you from finding that out the hard way.&lt;/p&gt;

&lt;p&gt;Stars measure popularity. I cared more about what fits my own projects, so I picked the two that match what I build most, which is websites.&lt;/p&gt;

&lt;p&gt;Before you install anything, read the &lt;code&gt;SKILL.md&lt;/code&gt; and any scripts it pulls in. A skill is instructions and scripts that run with your agent's permissions, so check it the same way you'd check an npm package.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Impeccable
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/pbakaus/impeccable" rel="noopener noreferrer"&gt;Impeccable&lt;/a&gt; is a design skill that started from &lt;a href="https://github.com/anthropics/skills/tree/main/skills/frontend-design" rel="noopener noreferrer"&gt;Anthropic's frontend-design skill&lt;/a&gt; and builds on top of it. It has UX planning, a set of design commands like &lt;code&gt;critique&lt;/code&gt;, &lt;code&gt;polish&lt;/code&gt;, and &lt;code&gt;audit&lt;/code&gt;, and it asks you a lot of questions.&lt;/p&gt;

&lt;p&gt;To test it, I had Claude Opus 5.5 vibe code a site called the Great Cat Color Census. You spot a cat in the wild, click "Spotted one," and the counts and leaderboard update. Out of the box it came with a blocky card style and some hover animations. It looked okay. I don't think it looked bad, but it was pretty generic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjd863hdlisjw6sm1975b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjd863hdlisjw6sm1975b.png" alt="The Great Cat Color Census before Impeccable, with blocky pastel stat cards for total cats, shared brain cells, fur on the couch, and a chaos index above a grid of cat cards" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The default design Claude Opus 5.5 came up with.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Install it from your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx impeccable &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run init inside your agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/impeccable init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Init looks through the project, asks about anything it can't work out on its own, and writes a &lt;code&gt;PRODUCT.md&lt;/code&gt; with what it learned. After that I asked it to make the site much nicer using its styles. It came back with five or six design directions. I picked the sticker one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdnqsbo5cuzfmqsf0bvp8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdnqsbo5cuzfmqsf0bvp8.png" alt="The Great Cat Color Census after Impeccable, redesigned as a sticker book with a cat sticker grid, a pink Summon a random cat button, and die-cut cat stickers next to each count" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The sticker direction from Impeccable. Same features, new design.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It has basically the same features as before, and you can still summon a random cat. It looks a lot more polished, with extra animation and cats that look like peel-off stickers. The font gives it a Cat in the Hat feel. I don't have a very particular taste for frontend design, and I still like this version better. If you're a designer, tell me in the comments whether you agree.&lt;/p&gt;

&lt;p&gt;A good way to use Impeccable is to have it build several of its directions on separate Git branches. Then you can run each one and compare them side by side before you commit to a look.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Diagram Design
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/cathrynlavery/diagram-design" rel="noopener noreferrer"&gt;Diagram Design&lt;/a&gt; draws diagrams as one self-contained HTML and SVG file. It supports 45 diagram types, from architecture diagrams and flow charts to radar charts and Venn diagrams. The screenshots in the repo don't do it much justice.&lt;/p&gt;

&lt;p&gt;Install it with the skills CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add cathrynlavery/diagram-design
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I asked it for a diagram of the same cat site and didn't say what to diagram. There's no backend, so I wasn't expecting much. It picked the visitor flow. A visitor clicks, an action runs &lt;code&gt;change()&lt;/code&gt; or &lt;code&gt;reset()&lt;/code&gt;, the counts update, and the derived stats feed the left and right pages. It also shows the save and load path to &lt;code&gt;localStorage&lt;/code&gt;, and it even carried over the pink styling from the site.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj2b8xva66o1uhzz7vj67.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj2b8xva66o1uhzz7vj67.png" alt="A Diagram Design architecture diagram titled How one click becomes a counted cat, showing Visitor to Actions to counts to Derived stats, which feed the left and right pages, with localStorage, sticker flight, and feedback branches" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The diagram Diagram Design made from the cat site, open in Kiro.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It's a simple diagram, but I like the style, and I can see myself using it on my next project. It's also handy when you open a code base with little documentation. Ask it for a few diagrams before you start reading the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of the list
&lt;/h2&gt;

&lt;p&gt;I haven't spent much time with the other popular skills yet. These are my notes so far.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/DietrichGebert/ponytail" rel="noopener noreferrer"&gt;Ponytail&lt;/a&gt; gives your agent the personality of the laziest senior dev in the room. In its README, you show him fifty lines, and he says nothing and replaces them with one. The goal is less code and fewer tokens. In Kiro it loads as an always-on ruleset, so you don't get its slash commands.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/JuliusBrussee/caveman" rel="noopener noreferrer"&gt;Caveman&lt;/a&gt; makes the agent answer tersely to cut token usage. I haven't seen much savings myself. Someone in a &lt;a href="https://www.reddit.com/r/kiroIDE/comments/1x3i5ab/top_10_kiro_skill_repos/" rel="noopener noreferrer"&gt;Reddit thread on the top Kiro skill repos&lt;/a&gt; said it raised their token usage, even though the research the repo cites says otherwise.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/addyosmani/agent-skills" rel="noopener noreferrer"&gt;Addy Osmani's agent skills&lt;/a&gt; add a spec, plan, build, and test workflow to your agent. Kiro has specs built in, but if your tool doesn't, this or &lt;a href="https://github.com/obra/superpowers" rel="noopener noreferrer"&gt;Superpowers&lt;/a&gt; is how I'd add spec-driven development.&lt;/p&gt;

&lt;p&gt;I haven't tried &lt;a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill" rel="noopener noreferrer"&gt;UI UX Pro Max&lt;/a&gt; or &lt;a href="https://github.com/Graphify-Labs/graphify" rel="noopener noreferrer"&gt;Graphify&lt;/a&gt; yet, even though they're near the top of the list.&lt;/p&gt;

&lt;p&gt;If you build on AWS, look at the &lt;a href="https://docs.aws.amazon.com/agent-toolkit/latest/userguide/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Agent Toolkit for AWS&lt;/a&gt;. It's our set of skills and MCP servers for working with AWS from your agent, and it's free on &lt;a href="https://github.com/aws/agent-toolkit-for-aws" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;If you build websites, Impeccable is the one I'd install first. Diagram Design is right behind it, especially when you're new to a code base.&lt;/p&gt;

&lt;p&gt;I'm thinking about testing whether Ponytail and Caveman save tokens in Kiro. Leave a comment if you want to see that, and tell me which skills you're using.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>kiro</category>
    </item>
    <item>
      <title>Is This How Frontend Developers Work Now? My 2026 Coding Setup</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Wed, 14 Oct 2026 18:09:55 +0000</pubDate>
      <link>https://dev.to/erikch/is-this-how-frontend-developers-work-now-my-2026-coding-setup-13ma</link>
      <guid>https://dev.to/erikch/is-this-how-frontend-developers-work-now-my-2026-coding-setup-13ma</guid>
      <description>&lt;p&gt;Software development is moving fast! The same setup I had six weeks ago, is not the same I have now. In fact, I've been doing software development on my phone!&lt;/p&gt;

&lt;p&gt;Just last week I was on vacation at Yellowstone, and I built an app from my phone. I chatted with an agent, it pushed the code to GitHub, and I reviewed the PRs. It was wild!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu4ic5e5xea5pkn9lvrnq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu4ic5e5xea5pkn9lvrnq.png" alt="My game" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post walks through the setup that makes that work. It runs on a server in my house, and I can reach it from anywhere.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/AQm9lbvQahA" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Full disclosure, I'm a developer advocate at AWS, and a couple of the tools here are Kiro tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  The box: a TrueNAS home lab
&lt;/h2&gt;

&lt;p&gt;Years ago I bought an old Intel Xeon server and loaded it with hard drives. It's been my home lab ever since, running TrueNAS. For this setup I created a new Ubuntu VM on it called Kiro Crew and do all my coding there.&lt;/p&gt;

&lt;p&gt;I went with Ubuntu 24.04 for the long-term support. 26 was an option, but 24 does everything I need.&lt;/p&gt;

&lt;p&gt;You don't need a TrueNAS server for this. I've seen people buy a Mac mini or reuse an old machine, and an EC2 instance or any cloud VM works too. I like keeping it local so everything sits on my own network.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I installed
&lt;/h2&gt;

&lt;p&gt;After the VM was up, I SSH'd in and installed four things.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kiro CLI.&lt;/strong&gt; Any coding harness that runs on a remote server and lets you authenticate there will work. Most of them do now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kiro Crew.&lt;/strong&gt; This is the agent I actually talk to. It works like a personal assistant for developers, with sessions, artifacts, memories, skills, and scheduled tasks. OpenClaw, Pi, and OpenCode are other options people run on remote servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The GitHub CLI&lt;/strong&gt;, so the agent can push code and open PRs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailscale&lt;/strong&gt;, so I can reach the server from anywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Connecting from my phone with Tailscale
&lt;/h2&gt;

&lt;p&gt;Tailscale puts the server, my laptop, and my phone on the same private network. Wherever I am, I open Tailscale on my phone, SSH into the box with a mobile SSH app, and port forward the Kiro Crew dashboard. I use port 5477 for mine.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://kiro.dev/docs/crew/" rel="noopener noreferrer"&gt;Kiro Crew docs&lt;/a&gt; cover remote setup, and the &lt;a href="https://github.com/kirodotdev/KiroCrew/blob/main/docs/guides/remote-and-mobile.md" rel="noopener noreferrer"&gt;remote and mobile guide&lt;/a&gt; goes through SSH tunnels and phone access step by step. You could use something like CloudFlare tunnels for this as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting from my laptop with SSH port forwarding
&lt;/h2&gt;

&lt;p&gt;From my laptop it's a single SSH command with local port forwarding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-L&lt;/span&gt; 5477:localhost:5477 YOUR_USER@YOUR_SERVER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I open a new connection window and point it at that port. (I also installed Kiro Crew locally on my Mac Laptop, so I can connect to Kiro Crew locally or to a remote instance)&lt;/p&gt;

&lt;p&gt;For security, Kiro Crew wants a token before it lets you in. On the server, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kirocrew token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prints a link with a token in it. Open that, and you're talking to your Crew instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working in Kiro Crew
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2688bwxeedc9zyncrkod.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2688bwxeedc9zyncrkod.png" alt="Kiro Crew Dashboard" width="799" height="304"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sessions live along the side of the dashboard. When I start a new one, I usually turn on trust mode so the agent stops asking for approval after every step. That isn't a good idea for everything. For the personal projects I'm building, I'm comfortable with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsq315rhbvhm7w7bxbmhq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsq315rhbvhm7w7bxbmhq.png" alt="Trust mode" width="738" height="662"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I tell it what to do. In the video I had it pull down a repo I already had. As we go back and forth, it sends me screenshots of what it's building, and it can spin up a dev server so I can look at the running app.&lt;/p&gt;

&lt;p&gt;By default it put that project in a scratch folder. Normally I pick the working folder I want before I start. The artifacts tab collects the screenshots and files from all my other projects too, so I can check on things I'm not actively working on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memories and skills
&lt;/h2&gt;

&lt;p&gt;Memories are new in Kiro Crew. I told it to always use Vue.js, because I'm a huge Vue fan. I also installed a set of skills, so every new project starts with the same defaults.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reviewing stacked PRs on GitHub
&lt;/h2&gt;

&lt;p&gt;I always tell the agent to push to GitHub. When I want to be careful, I ask for a stacked PR.&lt;/p&gt;

&lt;p&gt;GitHub's stacked PR interface works well with coding agents. It breaks a large update into smaller layers, and you review each one on its own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0x66ptco3dgmzdbodo0f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0x66ptco3dgmzdbodo0f.png" alt=" " width="800" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a fun project I'll accept the whole stack. For anything else I go through it layer by layer, make changes, and go back to Kiro Crew with something like "please update the PR, this code isn't what I want." Then we go back and forth until it's right.&lt;/p&gt;

&lt;p&gt;The next thing I'd add is &lt;a href="https://www.coderabbit.ai" rel="noopener noreferrer"&gt;CodeRabbit&lt;/a&gt;, which reviews PRs automatically before you merge. I don't have it on this project yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building from my phone on vacation
&lt;/h2&gt;

&lt;p&gt;When I was at Yellowstone National Park last week, I wanted to try an ASCII library I'd seen on Hacker News. I asked Kiro Crew to build an app with it, and it created everything.&lt;/p&gt;

&lt;p&gt;I did all of it from my phone. Tailscale got me onto my network, I chatted with the agent, it pushed to GitHub, and I checked the PRs. It worked really well.&lt;/p&gt;

&lt;p&gt;I can also give it one prompt, turn on trust mode, walk away, and come back half an hour later to see everything it's done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Models and effort levels
&lt;/h2&gt;

&lt;p&gt;I switch between Claude Opus 5.5 and GPT 5.6 Sol. Opus 5.5 works better for me and what I'm building, so that's my default.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flqg4fulzeaneg1ok305n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flqg4fulzeaneg1ok305n.png" alt="Models" width="744" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've also been testing different effort levels, including high. I don't have a clear answer there yet, so your mileage may vary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Specs and parallel sessions
&lt;/h2&gt;

&lt;p&gt;When I want to be very specific, I use spec-driven development mode. Kiro Crew has a spec builder built in. (You can enable it in the "Discover" apps section)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ca604ojbldt8pjxllt7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ca604ojbldt8pjxllt7.png" alt="Spec Builder" width="800" height="679"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most of the time, though, I open three or four sessions and let them run in the background while I do other things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scheduled tasks and Discord
&lt;/h2&gt;

&lt;p&gt;I'm trying to do more speaking, so I set up a weekly CFP finder. It runs on a schedule and sends me a message with what it found.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F676dg55n0pq15b1bf4gv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F676dg55n0pq15b1bf4gv.png" alt="Scheduled tasks" width="800" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kiro Crew is also connected to my own Discord channel and posts there. So I can either use Tailscale and port forwarding to open Crew on my phone, or I just open Discord and read the messages as they come in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does your setup look like?
&lt;/h2&gt;

&lt;p&gt;This is how I'm building apps right now. A server at home runs the agent, my phone and laptop connect to it, and code reaches me as stacked PRs I can review.&lt;/p&gt;

&lt;p&gt;Is this what your future looks like? Have you been running agents remotely, or is everything still happening in your editor? Let me know in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>frontend</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Are Frontend Developers Wasting Tokens? 5 Ways to Cut AI Coding Costs</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Thu, 08 Oct 2026 03:27:41 +0000</pubDate>
      <link>https://dev.to/erikch/are-frontend-developers-wasting-tokens-5-ways-to-cut-ai-coding-costs-2eoa</link>
      <guid>https://dev.to/erikch/are-frontend-developers-wasting-tokens-5-ways-to-cut-ai-coding-costs-2eoa</guid>
      <description>&lt;p&gt;When you have an AI agent open all day, it's really easy to burn through a lot of tokens quickly. And for a while that's how everyone was using our agentic harnesses. Unfortunately, the costs add up quick.&lt;/p&gt;

&lt;p&gt;So token maxing is dead. Whether your coding harness bills by tokens or by credits, it adds up. Let me show you a few ways to help lower you token costs! I'm using Kiro in my examples, but you can use whatever you'd like. &lt;/p&gt;

&lt;p&gt;A lot of this builds on Laura Salinas's article &lt;a href="https://dev.to/aws/pushing-kiros-free-tier-to-its-limits-5e34"&gt;Pushing Kiro's Free Tier to Its Limits&lt;/a&gt;. She built a project on Kiro's free tier and tracked where her credits went. It's worth a read.&lt;/p&gt;

&lt;p&gt;I also made a video covering all five. Check it out if you'd rather watch:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/8c9s-mfF_aY" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;(Full disclosure: I'm a Developer Advocate at AWS, and &lt;a href="https://kiro.dev/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; is owned by AWS. It's what I use in the video, so the examples below use it.)&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Use a cheaper model for simple tasks
&lt;/h2&gt;

&lt;p&gt;This one's a little obvious, but you shouldn't use the same frontier model for everything. Fixing a quick bug or renaming a prop shouldn't need the latest and greatest model.&lt;/p&gt;

&lt;p&gt;In Kiro, the model picker shows a credit multiplier for each model, and everything is relative to Auto at 1.0x. The open weight models sit at the cheap end. Qwen3 Coder Next is 0.05x, MiniMax M2.1 is 0.15x, and DeepSeek 3.2 is 0.25x. Opus 5.5, a frontier model, is 2.0x, and a few models go higher than that.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://kiro.dev/docs/models/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;models docs&lt;/a&gt; spell out what that means. A task that costs 10 credits on Auto would cost 20 on Opus 5.5, 4 on Haiku 4.5, and 0.5 on Qwen3 Coder Next. That's a 40x spread for the same task.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Firj6g3219h1u7k3rtrwh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Firj6g3219h1u7k3rtrwh.png" alt="Models" width="746" height="814"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your model supports it, you can also turn down the reasoning effort. The docs say lower effort levels give faster, shorter responses and use fewer credits.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up&lt;/strong&gt;: Multipliers change as new models show up, so check the models page before you settle on one. Two models with the same multiplier can still use different amounts of credits on the same task, since token counts and thinking depth vary.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Keep spec-driven development in mind here. I'll come back to it in tip 3, because that's where mixing models comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Put token rules in your steering file
&lt;/h2&gt;

&lt;p&gt;Steering files are markdown files in &lt;code&gt;.kiro/steering/&lt;/code&gt; that Kiro loads as persistent context, so you don't have to repeat yourself every session. Here are a few token rules I have in mine. You can write them exactly like this, and Kiro will try to follow them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;inclusion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Token rules&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Don't create unnecessary files.
&lt;span class="p"&gt;-&lt;/span&gt; Don't create tests unless I ask you to.
&lt;span class="p"&gt;-&lt;/span&gt; Try to save on token usage.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save that as &lt;code&gt;.kiro/steering/token-rules.md&lt;/code&gt;. &lt;code&gt;inclusion: always&lt;/code&gt; is the default mode, so it loads into every interaction. The front matter has to be the very first thing in the file, with no blank lines above it. With these rules in place, I know I'm not getting a bunch of files or tests I never asked for.&lt;/p&gt;

&lt;p&gt;Laura ran into exactly this in her build. With no steering file, DeepSeek generated validation files and summary markdowns she didn't need, and those cost credits. Her suggested steering rules include "Don't create summary markdown files" and "Use minimal code, no boilerplate."&lt;/p&gt;

&lt;p&gt;Always-on steering rides along with every request too, so keep that file short. Anything framework-specific can use &lt;code&gt;fileMatch&lt;/code&gt;, which only loads the file when you're working with matching files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;inclusion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fileMatch&lt;/span&gt;
&lt;span class="na"&gt;fileMatchPattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;components/**/*.tsx"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# React component rules&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Use function components and hooks.
&lt;span class="p"&gt;-&lt;/span&gt; Type props with an interface in the same file.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two more modes. &lt;code&gt;manual&lt;/code&gt; loads a file only when you reference it with &lt;code&gt;#file-name&lt;/code&gt; in chat, and &lt;code&gt;auto&lt;/code&gt; loads it when your request matches its description. The &lt;a href="https://kiro.dev/docs/steering/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;steering docs&lt;/a&gt; cover all four.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Use spec-driven development with two models
&lt;/h2&gt;

&lt;p&gt;If you're building a feature that takes some forethought, and not just a quick bug fix, spec-driven development is the way to go. A Kiro spec moves through three phases (requirements, design, and tasks) and writes &lt;code&gt;requirements.md&lt;/code&gt;, &lt;code&gt;design.md&lt;/code&gt;, and &lt;code&gt;tasks.md&lt;/code&gt; into &lt;code&gt;.kiro/specs/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The requirements and design phases are where the hard thinking happens, so that's where a stronger model earns its multiplier. Once you have a solid requirements and design document, you don't need the latest frontier model for the implementation phase. The cheaper models do pretty well working through a clear task list. You may want to try it out.&lt;/p&gt;

&lt;p&gt;To make the switch, change the model picker before you start running tasks. The models docs say your selection applies to all later messages in the conversation. Tasks can run one at a time or all at once, so you can check the cheaper model's work on the first task before you let it run the rest.&lt;/p&gt;

&lt;p&gt;More on the phases is in the &lt;a href="https://kiro.dev/docs/specs/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;specs docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Let ESLint, Prettier, and hooks do the deterministic work
&lt;/h2&gt;

&lt;p&gt;If a job has a deterministic answer, don't pay a large language model to work it out. Formatting and lint fixes are that kind of job. Prettier gives you the same output every time, and so does &lt;code&gt;eslint --fix&lt;/code&gt;. When you ask the agent to do it, you pay for it to read the file, reason about it, and write a diff a tool would have made instantly.&lt;/p&gt;

&lt;p&gt;Laura's article makes the same point. She suggests hooks that auto-lint or auto-format on save, so you never spend credits asking the agent to fix style.&lt;/p&gt;

&lt;p&gt;Start with scripts in &lt;code&gt;package.json&lt;/code&gt;. This also includes a &lt;code&gt;lint-staged&lt;/code&gt; block for the commit hook further down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eslint ."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint:fix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eslint . --fix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prettier --write ."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lint-staged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"*.{js,jsx,ts,tsx,vue}"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"eslint --fix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prettier --write"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have a hook that runs Prettier or my Husky rule every time I'm done doing something, so I'm not wasting tokens on it. In Kiro, hooks are JSON files in &lt;code&gt;.kiro/hooks/&lt;/code&gt;. This one lint-fixes and formats when the agent finishes its turn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lint and format when the agent finishes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"trigger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Stop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run lint:fix &amp;amp;&amp;amp; npm run format"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save it as &lt;code&gt;.kiro/hooks/format-on-stop.json&lt;/code&gt;. The &lt;code&gt;Stop&lt;/code&gt; trigger fires when the agent completes its turn, and a &lt;code&gt;command&lt;/code&gt; action runs a shell command in your project root. The &lt;a href="https://kiro.dev/docs/hooks/actions/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;hook actions docs&lt;/a&gt; say shell command actions don't consume credits, while agent prompt actions do because they start a new agent loop. So for formatting, pick &lt;code&gt;command&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you'd rather run on every save, the &lt;a href="https://kiro.dev/docs/hooks/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;hooks docs&lt;/a&gt; show a &lt;code&gt;PostFileSave&lt;/code&gt; trigger with a matcher, something like &lt;code&gt;"matcher": "\\.(ts|tsx|vue)$"&lt;/code&gt;. File triggers only fire on changes the agent makes, not on your own edits.&lt;/p&gt;

&lt;p&gt;For commits, Husky plus lint-staged runs the same tools on staged files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; husky lint-staged
npx husky init
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"npx lint-staged"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .husky/pre-commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;husky init&lt;/code&gt; creates &lt;code&gt;.husky/pre-commit&lt;/code&gt; and adds a &lt;code&gt;prepare&lt;/code&gt; script to &lt;code&gt;package.json&lt;/code&gt;. The &lt;code&gt;echo&lt;/code&gt; line swaps the default &lt;code&gt;npm test&lt;/code&gt; for &lt;code&gt;npx lint-staged&lt;/code&gt;, which reads the config from the &lt;code&gt;package.json&lt;/code&gt; above.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Watch your context window
&lt;/h2&gt;

&lt;p&gt;The last tip is to double check your context usage. In Kiro, the context usage meter in the chat panel shows how close you are to the model's limit.&lt;/p&gt;

&lt;p&gt;This matters because on every turn, every time you go back and forth with the model, the previous chat history gets sent again. The longer the session, the more context each message carries.&lt;/p&gt;

&lt;p&gt;MCP servers add to this, since their tool definitions take up context too. The Kiro &lt;a href="https://kiro.dev/docs/powers/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Powers docs&lt;/a&gt; put a number on it. Five connected MCP servers can mean 100+ tool definitions and 50,000+ tokens, about 40% of your context window, before your first prompt. &lt;em&gt;Different harnesses may handle this differently, some use search tools to help prevent context bloat with MCP servers&lt;/em&gt; Kiro Powers, for example, only load their tools when your conversation mentions something relevant. Your mileage may vary, so turn off MCP servers you aren't using for the project in front of you.&lt;/p&gt;

&lt;p&gt;Then start a new session when you move on to a new task. History from the last task doesn't help with the next one, and it gets sent along on every turn. In Claude Code, &lt;code&gt;/clear&lt;/code&gt; does this. It starts a new session and keeps the previous one so you can resume it later. Kiro also compacts long sessions automatically, but a fresh session for unrelated work keeps things small from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;Thanks again to Laura Salinas for &lt;a href="https://dev.to/aws/pushing-kiros-free-tier-to-its-limits-5e34"&gt;Pushing Kiro's Free Tier to Its Limits&lt;/a&gt;. It has more tips I didn't cover here, like writing a specific first prompt and pointing the agent at files with &lt;code&gt;#File&lt;/code&gt;. The video version is up top if you want to see these in action.&lt;/p&gt;

&lt;p&gt;Let me know in the comments if I missed one.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>kiro</category>
    </item>
    <item>
      <title>Are Frontend Developers Cooked? Is Frontend design safe?</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Wed, 30 Sep 2026 20:38:21 +0000</pubDate>
      <link>https://dev.to/erikch/are-frontend-developers-cooked-is-frontend-design-safe-nn8</link>
      <guid>https://dev.to/erikch/are-frontend-developers-cooked-is-frontend-design-safe-nn8</guid>
      <description>&lt;p&gt;I recently put out a video arguing that frontend development is changing. I don't think the work is going away. The frontend developer title is what's changing, and a lot of us are turning into full stack developers.&lt;/p&gt;

&lt;p&gt;The comments had opinions, so I recorded a follow-up.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/hkejlSRGf94" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick a stack and get really good at it
&lt;/h2&gt;

&lt;p&gt;One comment that I found really interesting was from a 62 year old developer with almost 30 years in the field. In the comment he described how Claude handles about half his workload now. His advice was to pick a couple of good frontend frameworks like Vue or Angular, pair them with a solid backend like C#, and become an absolute master at them.&lt;/p&gt;

&lt;p&gt;I agree with him, and this was something I've always said. It's always good to be at least a specialist in one category. I use Kiro as my agent harness for most of my apps now, and I still want deep knowledge of the stack underneath. That way I can really understand what's happening and help steer the agent when things go wrong. The agents are good. They aren't good enough that I'd stop checking their work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is frontend design the safer part of web dev?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxnho1zwts121upy87gga.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxnho1zwts121upy87gga.png" alt="YouTube comment arguing frontend design is the safer part of web dev" width="800" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This comment is where the title comes from. CRUD apps are the easy wins for LLMs, the argument goes, and attractive, non-generic UI with great UX is where they still struggle.&lt;/p&gt;

&lt;p&gt;I sort of agree. With the older Anthropic models you could tell someone one-shotted a design at a glance. It had that very generic Tailwind template look. However, with every new model it's becoming harder and harder to tell. People who aren't looking at this stuff every day mostly think the designs look fine.&lt;/p&gt;

&lt;p&gt;I didn't talk much about taste in my last video, but it is important. Looking at a screen and knowing whether the UX works, catching an accessibility problem, noticing that a flow is going to bog users down. That judgment still comes from people with experience. Still, LLMs are getting closer all the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  I rebuilt Winamp with an AI agent
&lt;/h2&gt;

&lt;p&gt;To test this I picked something with a lot of small UI pieces. I had &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro Crew&lt;/a&gt; recreate the old Winamp MP3 player, and it came back as ErikAmp.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1bl573rlyixqszjq0so5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1bl573rlyixqszjq0so5.png" alt="ErikAmp, a Winamp-style MP3 player with an equalizer and playlist" width="800" height="1274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was really impressed with everything it did with the UI. An equalizer with a row of sliders, a scrolling playlist, play, pause, stop, next and previous, shuffle, repeat, balance, volume. Getting every one of those working by hand would take me a couple of days, maybe less if I did nothing else. The agent basically one-shot it last Saturday. I had it fix a few bugs afterward, and it wrote a whole test suite along the way.&lt;/p&gt;

&lt;p&gt;The source code is really simple. I never asked for Vue or React, so it skipped frameworks entirely. It's an &lt;code&gt;index.html&lt;/code&gt;, some CSS, a handful of JavaScript files, and a bit of Python.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8a370idwej5iz6mytqf3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8a370idwej5iz6mytqf3.png" alt="ErikAmp index.html source with data-transport attributes and ARIA labels" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You could argue that this design was already made, but still. I had it create three brand new themes, and it looks great. &lt;/p&gt;

&lt;h2&gt;
  
  
  Where it starts to look like AI
&lt;/h2&gt;

&lt;p&gt;The second app I showed is a 5K coach I built to get myself in shape for a race. It tells me what to do each day.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmzv60e460ttv09dp37eg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmzv60e460ttv09dp37eg.png" alt="5K training coach app showing a recovery day and the weekly plan" width="800" height="652"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is about as close as I get to the default Anthropic look. Dark cards, rounded corners, pill badges. It isn't flashy and I wouldn't call it amazing. For my own training plan it's fine, and it took a few minutes with my agent. Building it by hand would have eaten a lot more of my time.&lt;/p&gt;

&lt;h2&gt;
  
  
  So are we cooked?
&lt;/h2&gt;

&lt;p&gt;I don't think so. Building a frontend is the easiest it has ever been, and I got two working apps out of an agent with very little effort. Deciding whether those apps are any good is still the developer's job, and that takes knowing your stack and having some taste.&lt;/p&gt;

&lt;p&gt;I'm also figuring out what to cover next. Do you want more AI workflow content, or more classic Vue and fundamentals tutorials? Tell me in the comments, and tell me where you think frontend is headed.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>frontend</category>
      <category>ai</category>
      <category>career</category>
    </item>
    <item>
      <title>Jev 101: How I'm Using It Today in My Apps</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 22 Sep 2026 17:32:20 +0000</pubDate>
      <link>https://dev.to/erikch/jev-doesnt-write-text-it-returns-probabilities-5gno</link>
      <guid>https://dev.to/erikch/jev-doesnt-write-text-it-returns-probabilities-5gno</guid>
      <description>&lt;p&gt;TypeSafe released Jev on September 15th, 2026 and called it a System One model. A System One model is a specialized type of AI designed to make fast, deterministic judgments and structured classifications in a single parallel pass rather than generating conversational text. &lt;/p&gt;

&lt;p&gt;You don't chat with it. You send it some state plus a list of narrow questions, and it sends back probabilities. &lt;/p&gt;

&lt;p&gt;What I really like about it is it's cost. It's $42 per billion input tokens, and output tokens free. In my testing I was seeing answers in 70 to 500 milliseconds.&lt;/p&gt;

&lt;p&gt;Here is what I built with it!&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/0zK7uT1ToCs" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What a request actually looks like
&lt;/h2&gt;

&lt;p&gt;This is the interface. It has a POST, a state object, and a map of questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://openrouter.ai/api/alpha/decisions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;openRouterApiKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~typesafe/jev-latest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildGateState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pullRequest&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;matches_request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;noul&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Does the pull request directly address the stated issue and acceptance criteria?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;protects_credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;noul&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Does the changed frontend code keep credentials and secret tokens out of browser-visible runtime configuration?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What comes back is shaped like the questions you asked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~typesafe/jev-latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"answers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"matches_request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"noul"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"noul"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.59&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"protects_credentials"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"noul"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"noul"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.11&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4211&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.00067&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every answer is a number between 0 and 1. If you ask six questions, you get six probabilities, and you can then decide what to do with them. &lt;/p&gt;

&lt;h2&gt;
  
  
  Three question types
&lt;/h2&gt;

&lt;p&gt;Jev has three primitives.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;noul&lt;/strong&gt; is a yes-or-no probability. It doesn't have any other criteria, just questions.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;choice&lt;/strong&gt; picks one category from a set you define. Its criteria are an object, because the categories are unordered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;traffic_shape&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;choice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Using only the described usage pattern, choose how this workload receives traffic over a typical month.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;criteria&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;steady_24_7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Traffic arrives continuously at a roughly similar rate at all hours.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;business_hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Traffic happens during working hours on weekdays and falls to almost nothing outside them.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;spiky_bursty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Traffic is uneven and unpredictable, with quiet periods followed by much heavier bursts.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;batch_scheduled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Work runs on a schedule as discrete jobs rather than as a stream of user requests.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;score&lt;/strong&gt; is an ordered rubric, so its criteria are an array instead. The API enforces that distinction.&lt;/p&gt;

&lt;h2&gt;
  
  
  App one: gating a pull request
&lt;/h2&gt;

&lt;p&gt;The first app loads a pull request and asks six questions about it at once. In my case I asked: Does the diff match the stated issue? Does it keep credentials out of browser-visible config? Does it implement the failure states the acceptance criteria asked for? Is the changed interactive UI keyboard operable? Is the layer safe to review on its own given its dependencies?&lt;/p&gt;

&lt;p&gt;All six run in the same request, and it's super fast!&lt;/p&gt;

&lt;p&gt;Then my code, not the model, turns those probabilities into a verdict:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Probability&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;80 to 100%&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Enough support to continue through normal review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;55 to 79%&lt;/td&gt;
&lt;td&gt;Review&lt;/td&gt;
&lt;td&gt;A person should verify the uncertain evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Below 55%&lt;/td&gt;
&lt;td&gt;Block&lt;/td&gt;
&lt;td&gt;Do not merge this layer yet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In my scenario I used &lt;code&gt;keyboard_accessible&lt;/code&gt; to only count if &lt;code&gt;changes_interactive_ui&lt;/code&gt; came back above 0.5. A backend-only PR shouldn't get dinged for missing keyboard behavior, and that conditional lives in my code where I can read it and change it.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenRouter versus calling TypeSafe directly
&lt;/h3&gt;

&lt;p&gt;I started on OpenRouter because I was still on the TypeSafe waitlist.  I noticed that OpenRouter was quite a bit slower.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Through OpenRouter: 683 ms, cost $0.00067&lt;/li&gt;
&lt;li&gt;Straight to TypeSafe: 318 ms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Roughly half the time for the same work. If you're going to build on this, get on the waitlist and use their API. I got in the next day.&lt;/p&gt;

&lt;h2&gt;
  
  
  App two: AWS workload
&lt;/h2&gt;

&lt;p&gt;For the second app you describe an AWS workload, something like "public checkout API for retail storefronts," and it gives you back a cost estimate.&lt;/p&gt;

&lt;p&gt;Here is what it does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Owns&lt;/th&gt;
&lt;th&gt;Never does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jev&lt;/td&gt;
&lt;td&gt;Semantic judgment about the prose description&lt;/td&gt;
&lt;td&gt;See a number, emit a number, compare two numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;My code&lt;/td&gt;
&lt;td&gt;Sizing, platform limits, thresholds, ranking&lt;/td&gt;
&lt;td&gt;Guess at intent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Price List&lt;/td&gt;
&lt;td&gt;Every unit rate&lt;/td&gt;
&lt;td&gt;Nothing, it is just data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Jev answers questions like whether the described data access needs relational queries across multiple tables, whether any single unit of work runs longer than fifteen minutes, and whether the description implies a bounded population or open-ended public demand. Then the &lt;a href="https://github.com/aws/agent-toolkit-for-aws" rel="noopener noreferrer"&gt;Agent Toolkit for AWS&lt;/a&gt; and the AWS pricing MCP server produces the dollar figure.&lt;/p&gt;

&lt;p&gt;Once the model has classified the output, the arithmetic is deterministic. &lt;/p&gt;

&lt;h2&gt;
  
  
  Schema validity
&lt;/h2&gt;

&lt;p&gt;Schema validity is not correctness. Jev cannot return an answer outside the questions and criteria you defined, so you'll never parse a broken response. Although, it can return a confidently wrong probability that validates perfectly.&lt;/p&gt;

&lt;p&gt;That's why both apps put a threshold in front of any consequence. Low confidence routes to a person or to a stronger model. If you wire probabilities straight into an irreversible action, the guaranteed output shape will not save you.&lt;/p&gt;

&lt;p&gt;TypeSafe publishes &lt;a href="https://docs.typesafe.ai/confidence" rel="noopener noreferrer"&gt;guidance on confidence&lt;/a&gt;, but you need to measure it against your own labeled data and set thresholds based on what a mistake costs you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What other people are doing with it
&lt;/h2&gt;

&lt;p&gt;The community projects are where the speed becomes obvious. Someone has Jev playing Super Mario Brothers by asking "should I jump here" over and over while the game runs. Somebody else built a real time outfit picker that updates suggestions as you change the inputs.&lt;/p&gt;

&lt;p&gt;The possibilities are endless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Would I use it
&lt;/h2&gt;

&lt;p&gt;Frontier models aren't going anywhere, and this doesn't compete with them. Nothing here writes code, explains a decision, or produces a plan.&lt;/p&gt;

&lt;p&gt;However, I have a decent number of workflows that need a lot of small semantic decisions with deterministic code waiting on the other side. Routing, classification, gating, cheap verification before an expensive step. For those, sending narrow questions and getting probabilities back is a better fit than asking a chat model for JSON and hoping.&lt;/p&gt;

&lt;p&gt;Try it on something you already have labels for, measure the calibration yourself, and keep anything expensive behind a threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" rel="noopener noreferrer"&gt;Jev announcement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.typesafe.ai/introduction" rel="noopener noreferrer"&gt;TypeSafe docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.typesafe.ai/confidence" rel="noopener noreferrer"&gt;Confidence and calibration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.typesafe.ai/model-jaggedness/jev-1.13" rel="noopener noreferrer"&gt;Jev 1.13 model jaggedness&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openrouter.ai/~typesafe/jev-latest" rel="noopener noreferrer"&gt;Jev on OpenRouter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://evals.typesafe.ai/" rel="noopener noreferrer"&gt;TypeSafe workflow evals&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/aws/agent-toolkit-for-aws" rel="noopener noreferrer"&gt;Agent Toolkit for AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/agent-toolkit/latest/userguide/mcp-server.html" rel="noopener noreferrer"&gt;AWS MCP Server docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>nuxt</category>
      <category>aws</category>
    </item>
    <item>
      <title>Frontend Isn't Dead. But the Job Title Is Changing</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 15 Sep 2026 17:00:26 +0000</pubDate>
      <link>https://dev.to/erikch/frontend-isnt-dead-but-the-job-title-is-changing-2fj7</link>
      <guid>https://dev.to/erikch/frontend-isnt-dead-but-the-job-title-is-changing-2fj7</guid>
      <description>&lt;p&gt;I keep seeing people claim frontend development is dead, so I went and did some research, and not all of it was bad.&lt;/p&gt;

&lt;p&gt;If you rather watch, check out this video: &lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/ucJMazXR3X8" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Job posting show an interesting story
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.hiringlab.org/2025/07/30/the-us-tech-hiring-freeze-continues/" rel="noopener noreferrer"&gt;Indeed's Hiring Lab&lt;/a&gt; looked at tech job titles against pre-pandemic levels. Software engineer postings were down 49 percent. Web developer postings were down more than 60 percent. This isn't great.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://leaddev.com/ai/does-ai-spell-death-front-end-engineering" rel="noopener noreferrer"&gt;LeadDev&lt;/a&gt; tracked frontend openings from August 2021 through July 2025. The job openings tracked about the same as Indeed. Role postings has tracked lower.&lt;/p&gt;

&lt;p&gt;Less developers are identifying themselves as frontend developers. In the &lt;a href="https://survey.stackoverflow.co/2025/developers/" rel="noopener noreferrer"&gt;2025 Stack Overflow&lt;/a&gt; survey, developers identifying as frontend went from 6.6 percent in 2023 to 4.3 percent in 2025. On the other hand developers who identify themselves as full-stack are at a much higher percentage.&lt;/p&gt;

&lt;p&gt;All this data points in one direction. Frontend as a job, as a title, is on the decline. And I don't think there is only one reason. It would be easy to point to just AI, but I really think the shift is happening because of a multiple factors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;First, yes, more people are using AI. 84 percent of Stack Overflow respondents are using or planning to use AI tools, up from 76 percent the year before. 51 percent of professional developers use them daily.&lt;/p&gt;

&lt;p&gt;And AI does a really good job at many frontend tasks. For example, taking a Figma file and turning it into HTML and CSS is a great use case. I've done that myself. I've used AI to help mock up an interface that would have taken me much longer.&lt;/p&gt;

&lt;p&gt;However, AI isn't perfect. In that same Stack Overflow survey 46 percent of developers distrust AI accuracy against 33 percent who trust it, and only 3 percent report high trust. The most experienced developers are the most skeptical, at 2.6 percent high trust and 20 percent high distrust. &lt;/p&gt;

&lt;p&gt;So I don't think AI is the sole problem. We still need developers to make those frontends, and as you can see, we don't always trust AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The skills moved to meta frameworks
&lt;/h2&gt;

&lt;p&gt;More often, the skills needed by frontend developers also include a lot of what you imagine a fullstack developer does. It also matches what these meta frameworks have been doing. Next.js, Nuxt, SvelteKit, and Astro all put both sides of the API boundary in the same project.  Now frontend developers are working on both sides, and no longer consider themselves "frontend" deves.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what to do?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pick a backend framework and go deep on it.&lt;/strong&gt; If you haven't already start working in Next, Nuxt, SvelteKit. Learn how data is passed from the client to server. Pick up, Java, C#, or Ruby on Rails. Go deep and learn it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get good at what AI handles badly.&lt;/strong&gt; AI does a lot of things well but it still has problems with performance work, accessibility, and something I like to call "taste". Automated tooling still detects WCAG failures on 95.9 percent of the top million home pages, averaging 56.1 errors each. The work is there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Own the review, not the typing.&lt;/strong&gt; Kiro's &lt;a href="https://kiro.dev/topics/frontier-engineering" rel="noopener noreferrer"&gt;guide to frontier engineering&lt;/a&gt; lays out ten principles for working with agents. If 84 percent of developers are generating code with AI however only 3 percent highly trust the output (see the Stack Overflow survey from earlier), then you need to look at the total AI software development life cycle to see where you can improve. Start with this guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Websites still need to get built. Frontends are still necessary. But we need to look beyond the HTML, CSS and JavaScript.&lt;/p&gt;

&lt;p&gt;The work has changed to these full stack meta frameworks, and AI does a lot of work for us. Keep on building and learning! Frontend isn't dead, but it's changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://leaddev.com/ai/does-ai-spell-death-front-end-engineering" rel="noopener noreferrer"&gt;LeadDev and TalentNeuron on frontend vs full-stack postings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hiringlab.org/2025/07/30/the-us-tech-hiring-freeze-continues/" rel="noopener noreferrer"&gt;Indeed Hiring Lab, The US Tech Hiring Freeze Continues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://survey.stackoverflow.co/2025/ai/" rel="noopener noreferrer"&gt;Stack Overflow 2025 Developer Survey, AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://survey.stackoverflow.co/2025/developers/" rel="noopener noreferrer"&gt;Stack Overflow 2025 Developer Survey, developer roles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.bls.gov/ooh/computer-and-information-technology/web-developers.htm" rel="noopener noreferrer"&gt;BLS, web developers and digital designers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.bls.gov/ooh/computer-and-information-technology/software-developers.htm" rel="noopener noreferrer"&gt;BLS, software developers, QA analysts, and testers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.bls.gov/cps/cpsaat11b.htm" rel="noopener noreferrer"&gt;BLS, employed people by detailed occupation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hackerrank.com/blog/top-developer-skills-in-2025-momentum-not-mayhem/" rel="noopener noreferrer"&gt;HackerRank, top developer skills&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webaim.org/projects/million/" rel="noopener noreferrer"&gt;WebAIM Million accessibility report&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have been hiring or job hunting lately, I want to know what the titles look like in your market. Drop a comment with the role name and your region.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>career</category>
      <category>javascript</category>
      <category>ai</category>
    </item>
    <item>
      <title>Would You Choose a Library Because AI Writes It Better?</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 08 Sep 2026 20:27:23 +0000</pubDate>
      <link>https://dev.to/erikch/would-you-choose-a-library-because-ai-writes-it-better-9i4</link>
      <guid>https://dev.to/erikch/would-you-choose-a-library-because-ai-writes-it-better-9i4</guid>
      <description>&lt;p&gt;I was at a conference recently and watched Joel Hooks talk about &lt;a href="https://effect.website/" rel="noopener noreferrer"&gt;Effect&lt;/a&gt;. Effect homepage h1 advertises that it's the "Reliable TypeScript for the AI era". Joel explained that AI agents (&lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;, Claude Code, etc) can write way better TypeScript with Effect then he could ever do by himself. It made me start thinking, is this the future?&lt;/p&gt;

&lt;p&gt;That raised a question I had not seriously considered before. Are we going to choose libraries based on what coding agents can write reliably, even when those libraries are harder for us to learn?&lt;/p&gt;

&lt;p&gt;I made a video about that question using two libraries: Effect and &lt;a href="https://stylexjs.com/" rel="noopener noreferrer"&gt;StyleX&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/nWJa28MS-gI" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Both libraries work really well with coding agents. Effect can more easily catch expected failures in the type system. StyleX puts styles behind a typed JavaScript API. Both help reduce errors during compilation.&lt;/p&gt;

&lt;p&gt;That sounds great. However, it also creates a strange situation where the agent may understand the stack better then I could, even though I'm responsible to maintain it long term.&lt;/p&gt;

&lt;p&gt;To help understand this more let's see how StyleX and Effect work, and where I think where this is all going.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put expected failures in the type system with Effect
&lt;/h2&gt;

&lt;p&gt;To understand Effect, you need to understand how it handles async functions. A normal asynchronous TypeScript function often tells you the success type and leaves the failure behavior in comments, thrown exceptions, or tribal knowledge.&lt;/p&gt;

&lt;p&gt;Effect gives failures their own typed channel. In the demo, each expected problem has a tagged error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;effect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotFoundError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TaggedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NotFoundError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RateLimitError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TaggedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RateLimitError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;retryAfterSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NetworkError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TaggedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NetworkError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The profile program returns one of those errors based on the scenario:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadProfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scenario&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not-found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NotFoundError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent-editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scenario&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rate-limited&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RateLimitError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;retryAfterSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scenario&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;offline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NetworkError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The demo API is offline.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I can infer the complete error union from the program instead of maintaining it separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LoadProfileError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
  &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;loadProfile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI then handles each tag in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;describeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LoadProfileError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NotFoundError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`No profile exists for @&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.`&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RateLimitError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Try again in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryAfterSeconds&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; seconds.`&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NetworkError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;assertNever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a coding agent adds a &lt;code&gt;MaintenanceError&lt;/code&gt; to the program and forgets to update the UI, the &lt;code&gt;assertNever&lt;/code&gt; call can turn that omission into a type error. The agent gets a correction signal immediately.&lt;/p&gt;

&lt;p&gt;Here is the feedback loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Coding agent changes the program
              |
              v
Effect updates the typed error channel
              |
              v
TypeScript checks every UI branch
              |
      missing case? fix it
              |
              v
Run tests and review behavior
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler is catching the errors first.&lt;/p&gt;

&lt;p&gt;Effect introduces a functional programming model, its own vocabulary, and more abstraction than a plain &lt;code&gt;async&lt;/code&gt; function. &lt;/p&gt;

&lt;p&gt;This is great, but with this abstraction comes more complexity, and a steeper learning curve.&lt;/p&gt;

&lt;p&gt;Let's take a look at how StyleX works next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put visual states behind a typed API with StyleX
&lt;/h2&gt;

&lt;p&gt;StyleX takes a similar idea into styling. Instead of handing an agent a stylesheet with global selectors and arbitrary class names, you define styles through &lt;code&gt;stylex.create&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stylex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;borderRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;solid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;minHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;resultSuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;successSurface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;resultWarning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;warningSurface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;resultDanger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dangerSurface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;danger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The component composes those states explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;stylex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;props&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resultSuccess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tone&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resultWarning&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tone&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;danger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resultDanger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Result UI */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the agent named visual states and a type-checked API. It can still go wrong of course, but there are fewer ways to accidentally make up selectors, misspell properties, or create styles that never get attached to the component.&lt;/p&gt;

&lt;p&gt;I'm not going to lie, I do not like the output to the page. Generated atomic class names look like random strings, and the CSS-in-JS syntax feels heavier than Tailwind.&lt;/p&gt;

&lt;p&gt;Personally, I think StyleX may be a better interface for agents, but I'm not completely sold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide who your stack is optimized for
&lt;/h2&gt;

&lt;p&gt;I used to choose libraries based on the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the team already know it?&lt;/li&gt;
&lt;li&gt;Is the documentation good?&lt;/li&gt;
&lt;li&gt;Can we debug it in production?&lt;/li&gt;
&lt;li&gt;Will it still be maintained in a few years?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With coding agents writing most of the code, we now need to decide if it should be the most important part. &lt;/p&gt;

&lt;p&gt;In my opinion, that question is important, but it should not be the only reason you pick a library.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Helpful for an agent&lt;/th&gt;
&lt;th&gt;Cost for a developer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Effect&lt;/td&gt;
&lt;td&gt;Typed failures, explicit composition, compiler feedback&lt;/td&gt;
&lt;td&gt;New programming model and learning curve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StyleX&lt;/td&gt;
&lt;td&gt;Typed styles, named states, constrained composition&lt;/td&gt;
&lt;td&gt;Less familiar syntax and generated class names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plain TypeScript&lt;/td&gt;
&lt;td&gt;Familiar syntax and broad ecosystem&lt;/td&gt;
&lt;td&gt;Expected failures are easier to leave implicit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailwind CSS&lt;/td&gt;
&lt;td&gt;Familiar utilities and readable markup for many teams&lt;/td&gt;
&lt;td&gt;More freedom for inconsistent generated combinations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With all that said, there may be a middle ground. Libraries such as shadcn/ui work well with coding agents because the patterns are common and the component source stays in your project. A developer can still open the file and understand what was generated without first learning a new programming model or paradigm.&lt;/p&gt;

&lt;p&gt;That is the balance I want. Give the agent enough structure to catch mistakes while keeping the code review useful for the person who owns the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finale
&lt;/h2&gt;

&lt;p&gt;I am not moving every TypeScript project to Effect or rewriting every Tailwind component with StyleX. For now, I am sticking with the tools I know well enough to review.&lt;/p&gt;

&lt;p&gt;I am keeping an eye out for these new type libraries. I really think in the future that agent friendly libraries will be the new normal, and the human readability, while important, will not be as important.&lt;/p&gt;

&lt;p&gt;However, we the developers are still responsible for architecture, behavior, and what reaches production. A compiler can catch an error, but us developers need to understand if the app is behaving the way it's supposed to.&lt;/p&gt;

&lt;p&gt;Would you adopt a library because your coding agent writes it better, even if your team has to work harder to learn it?&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>10 MCP Servers Worth Adding to Your AI Coding Workflow in 2026</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 01 Sep 2026 22:23:37 +0000</pubDate>
      <link>https://dev.to/erikch/10-mcp-servers-worth-adding-to-your-ai-coding-workflow-in-2026-1j1m</link>
      <guid>https://dev.to/erikch/10-mcp-servers-worth-adding-to-your-ai-coding-workflow-in-2026-1j1m</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I am a big fan of the Model Context Protocol. MCP keeps getting better, and I really think it's the future.&lt;/p&gt;

&lt;p&gt;This list is my favorite MCP servers that I use every day. I picked them based on how useful they are, and how much I've been able to get out of them.&lt;/p&gt;

&lt;p&gt;I also picked them based on these four signals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Public adoption&lt;/li&gt;
&lt;li&gt;Active maintenance&lt;/li&gt;
&lt;li&gt;Official backing&lt;/li&gt;
&lt;li&gt;Whether the server solves a problem developers actually hit&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the video version of the list:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/uiPSA1htycs" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need an MCP-capable coding agent or editor to use these servers. Kiro, Claude Code, Cursor, VS Code, and other clients support different combinations of local and remote MCP connections.&lt;/p&gt;

&lt;p&gt;Keep in mind that whenever you install an MCP server you should be cautious. Don't accept all changes, look at what it's executing first. It's also not a bad idea to try them out on non-production code bases first, just in case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps: the 10 MCP servers worth considering
&lt;/h2&gt;

&lt;p&gt;Here are my top MCP servers, in no particular order.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;MCP server&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Access level to watch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Context7&lt;/td&gt;
&lt;td&gt;Current library documentation&lt;/td&gt;
&lt;td&gt;Low, but verify package resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Chrome DevTools MCP&lt;/td&gt;
&lt;td&gt;Browser debugging and performance&lt;/td&gt;
&lt;td&gt;Full connected-browser access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Playwright MCP&lt;/td&gt;
&lt;td&gt;Repeatable browser automation&lt;/td&gt;
&lt;td&gt;Authenticated pages and local services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;GitHub MCP Server&lt;/td&gt;
&lt;td&gt;Repositories, issues, and pull requests&lt;/td&gt;
&lt;td&gt;Repository and workflow writes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Serena&lt;/td&gt;
&lt;td&gt;Semantic code navigation and editing&lt;/td&gt;
&lt;td&gt;Project files and language tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Supabase MCP&lt;/td&gt;
&lt;td&gt;Database and project workflows&lt;/td&gt;
&lt;td&gt;Schema, migration, and data changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;AWS MCP Server&lt;/td&gt;
&lt;td&gt;AWS documentation and APIs&lt;/td&gt;
&lt;td&gt;The caller's IAM permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Vercel MCP&lt;/td&gt;
&lt;td&gt;Deployments, logs, and analytics&lt;/td&gt;
&lt;td&gt;Project and deployment operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Cloudflare API MCP&lt;/td&gt;
&lt;td&gt;Workers, R2, D1, DNS, and APIs&lt;/td&gt;
&lt;td&gt;Broad account API access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Excalidraw MCP&lt;/td&gt;
&lt;td&gt;Editable architecture diagrams&lt;/td&gt;
&lt;td&gt;Diagram content sent to the service&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  1. Context7
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/upstash/context7" rel="noopener noreferrer"&gt;upstash/context7&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Package:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/@upstash/context7-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;@upstash/context7-mcp&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Context7 is one of my favorite MCP servers. I use it all the time to help me look up documentation for my favorite APIs. It's also nice because I don't have to install 100 different skills, I can get it all in one MCP server.&lt;/p&gt;

&lt;p&gt;When I checked, Context7 had 61,446 GitHub stars, 2,960 forks, and 3,975,539 npm downloads during August 2026. Version 4.0.4 shipped on August 28.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It addresses one of the most common AI coding failures without requiring broad access to your project.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Chrome DevTools MCP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/ChromeDevTools/chrome-devtools-mcp" rel="noopener noreferrer"&gt;ChromeDevTools/chrome-devtools-mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Chrome DevTools MCP is my browser-debugging pick. It can inspect console messages, network requests, screenshots, Lighthouse results, performance traces, heap data, and the browser state the user actually sees.&lt;/p&gt;

&lt;p&gt;It had 50,274 GitHub stars and 3,538 forks when I checked. The npm package recorded 3,308,596 downloads during August 24 through August 30, 2026. It is an official Google and Chrome DevTools project, and version 1.8.0 shipped on August 25.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It lets the agent debug the running application instead of declaring success when the code merely compiles.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Playwright MCP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/microsoft/playwright-mcp" rel="noopener noreferrer"&gt;microsoft/playwright-mcp&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Package:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/@playwright/mcp" rel="noopener noreferrer"&gt;&lt;code&gt;@playwright/mcp&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Playwright MCP is the browser-automation and testing choice. It had 36,660 GitHub stars, 3,077 forks, and 23,749,682 npm downloads during August 2026. That was the strongest directly attributable package-download signal in my research.&lt;/p&gt;

&lt;p&gt;I go back and forth between the Chrome Devtool and Playwright depending on the situation.&lt;/p&gt;

&lt;p&gt;Chrome DevTools goes deeper on live debugging, network behavior, memory, and performance. Playwright is a cleaner fit when the job is navigating a flow and proving it works repeatedly. Like check out flows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It gives an agent a repeatable way to test user flows instead of relying on a one-time manual check.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. GitHub MCP Server
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/github/github-mcp-server" rel="noopener noreferrer"&gt;github/github-mcp-server&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub's official MCP server is the obvious source-control pick. It can search repositories and code, read issues, work with pull requests, and inspect workflows.&lt;/p&gt;

&lt;p&gt;The repository had 32,626 stars and 4,872 forks when I checked. Version 1.11.0 shipped on August 25, 2026, and GitHub supports hosted, container, and binary connection paths.&lt;/p&gt;

&lt;p&gt;I use this all the time to push up code, or fix merge issues. I go back and forth with this MCP server and using the &lt;code&gt;gh&lt;/code&gt; CLI tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; Source control context appears in almost every software project.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Serena
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/oraios/serena" rel="noopener noreferrer"&gt;oraios/serena&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Package:&lt;/strong&gt; &lt;a href="https://pypi.org/project/serena-agent/" rel="noopener noreferrer"&gt;&lt;code&gt;serena-agent&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Serena is the codebase specialist. Instead of treating a repository as a pile of text files, it uses language tooling for symbol-level retrieval and semantic edits.&lt;/p&gt;

&lt;p&gt;It had 28,686 GitHub stars, 1,934 forks, and 160,865 rolling-month PyPI downloads reported on August 31, 2026. Version 1.7.0 shipped on August 9.&lt;/p&gt;

&lt;p&gt;I have not used Serena a lot, but from what I can tell it's really useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It gives agents a structured view of code when generic file search is not enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Supabase MCP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/supabase/mcp" rel="noopener noreferrer"&gt;supabase/mcp&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Package:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/@supabase/mcp-server-supabase" rel="noopener noreferrer"&gt;&lt;code&gt;@supabase/mcp-server-supabase&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Supabase MCP connects the agent to project, database, schema, and migration workflows. The repository had 2,883 stars and 403 forks, while the npm package recorded 439,749 downloads during August 2026.&lt;/p&gt;

&lt;p&gt;This is where the list becomes very tool specific. If you are using Supabase, use this tool!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It gives a Supabase project direct backend context, but only projects already using Supabase need it.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. AWS MCP Server
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://docs.aws.amazon.com/agent-toolkit/latest/userguide/mcp-server.html" rel="noopener noreferrer"&gt;AWS MCP Server&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Toolkit:&lt;/strong&gt; &lt;a href="https://github.com/aws/agent-toolkit-for-aws" rel="noopener noreferrer"&gt;aws/agent-toolkit-for-aws&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AWS is the cloud option I wanted to spend time on it because it's my favorite! The &lt;a href="https://github.com/aws/agent-toolkit-for-aws" rel="noopener noreferrer"&gt;Agent Toolkit for AWS&lt;/a&gt; is larger than one MCP server. It combines MCP configuration with skills, plugins, and agent integrations.&lt;/p&gt;

&lt;p&gt;The managed AWS MCP Server can search current AWS documentation and call AWS APIs across more than 300 services. The skill tells the agent how to approach a task. The server supplies current information and runtime tools. Same ecosystem, different jobs.&lt;/p&gt;

&lt;p&gt;Check it out!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It combines current AWS knowledge with live cloud inspection while retaining familiar IAM and audit controls.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Vercel MCP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://vercel.com/docs/mcp/vercel-mcp" rel="noopener noreferrer"&gt;Vercel MCP&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vercel MCP is a hosted server for teams already deploying on Vercel. It can inspect projects, deployments, build and runtime logs, analytics, and traces. It can also create deployments and perform collaboration operations.&lt;/p&gt;

&lt;p&gt;Be aware that &lt;a href="https://github.com/vercel/mcp-handler" rel="noopener noreferrer"&gt;&lt;code&gt;vercel/mcp-handler&lt;/code&gt;&lt;/a&gt; is a framework for building MCP servers. The AI SDK includes an MCP client. Vercel also ships an agent plugin. None of those is the hosted Vercel MCP server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It brings deployment state and production diagnosis into the agent for projects that already run on Vercel.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Cloudflare API MCP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/cloudflare/mcp" rel="noopener noreferrer"&gt;cloudflare/mcp&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Catalog:&lt;/strong&gt; &lt;a href="https://developers.cloudflare.com/agents/model-context-protocol/mcp-servers-for-cloudflare/" rel="noopener noreferrer"&gt;Cloudflare MCP servers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cloudflare's API MCP covers Workers, R2, D1, DNS, deployments, and roughly 2,500 Cloudflare API endpoints.&lt;/p&gt;

&lt;p&gt;Vercel and Cloudflare are alternatives in this list. Pick the server for the platform that runs your project rather than installing both because they appear in a roundup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It exposes a broad platform API without filling the context window with thousands of tools at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Excalidraw MCP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/excalidraw/excalidraw-mcp" rel="noopener noreferrer"&gt;excalidraw/excalidraw-mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Excalidraw is an amazing drawing tool, and it's worth adding to this list. Its first-party server had 5,212 stars and 487 forks when I checked. Version 0.3.2 shipped in February 2026, with code activity cited through March.&lt;/p&gt;

&lt;p&gt;I use it all the time to help me create architecture diagrams!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It turns project context into an editable artifact that can leave the editor and help a team communicate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about Canva and Figma?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://canva.dev/docs/mcp/" rel="noopener noreferrer"&gt;Canva MCP&lt;/a&gt; is useful when you need thumbnails, launch graphics, presentations, or other design assets. Canva also has a separate Dev MCP for people building Canva apps and integrations. It does not publish a comparable server-specific adoption number, and it sits farther from the code, so I treat it as a bonus rather than one of the ten core coding picks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developers.figma.com/docs/figma-mcp-server/" rel="noopener noreferrer"&gt;Figma MCP&lt;/a&gt; is the design-to-code alternative. It can supply design context, Code Connect mappings, assets, and diagram information. It makes sense when the implementation starts from a Figma design.&lt;/p&gt;

&lt;p&gt;If you need design, check both of them out!&lt;/p&gt;

&lt;h2&gt;
  
  
  Three popular names I left off
&lt;/h2&gt;

&lt;p&gt;Here are a few other MCP servers I left out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Puppeteer MCP
&lt;/h3&gt;

&lt;p&gt;The historical &lt;a href="https://www.npmjs.com/package/@modelcontextprotocol/server-puppeteer" rel="noopener noreferrer"&gt;&lt;code&gt;@modelcontextprotocol/server-puppeteer&lt;/code&gt;&lt;/a&gt; package is deprecated, and its source lives in the &lt;a href="https://github.com/modelcontextprotocol/servers-archived/tree/main/src/puppeteer" rel="noopener noreferrer"&gt;archived reference-server repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It still recorded 28,716 npm downloads during August 24 through August 30, 2026. That is evidence that package downloads can outlive maintenance, not a reason to recommend it for a new setup. Use Chrome DevTools MCP or Playwright MCP instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory reference server
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/modelcontextprotocol/servers/tree/main/src/memory" rel="noopener noreferrer"&gt;Knowledge Graph Memory server&lt;/a&gt; is useful for learning how basic MCP persistence works. Its maintainers describe the reference servers as educational examples rather than production-ready products.&lt;/p&gt;

&lt;p&gt;The package recorded 74,597 weekly downloads, but the roughly 89,000 stars belong to the entire &lt;code&gt;modelcontextprotocol/servers&lt;/code&gt; repository. They cannot be assigned to Memory alone. Every connected client can also read or change its local JSONL graph, which may accumulate sensitive project data.&lt;/p&gt;

&lt;h3&gt;
  
  
  TanStack Intent
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/TanStack/intent" rel="noopener noreferrer"&gt;TanStack Intent&lt;/a&gt; discovers and packages versioned Agent Skills. I like the project, and it is relevant to AI coding, but it is not an MCP server, client, or registry.&lt;/p&gt;

&lt;p&gt;The distinction matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Skills provide instructions and procedural knowledge loaded into the agent.&lt;/li&gt;
&lt;li&gt;MCP provides runtime tools, resources, and live external data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS and Vercel ship products across both categories, which is why the names can blur together. Useful does not automatically mean MCP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finale
&lt;/h2&gt;

&lt;p&gt;I use many of these MCP servers every day. Try a few out and see how you like them.&lt;/p&gt;

&lt;p&gt;Today my starter stack looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context7&lt;/strong&gt; for current APIs and examples&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome DevTools MCP or Playwright MCP&lt;/strong&gt; for verification&lt;/li&gt;
&lt;li&gt;**GitHub, and AWS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add Excalidraw, Canva, or Figma when the work needs to become a diagram or design.&lt;/p&gt;

&lt;p&gt;The full set of research links and measurement windows is available in the &lt;a href="https://www.youtube.com/watch?v=uiPSA1htycs" rel="noopener noreferrer"&gt;video description&lt;/a&gt;. Which MCP server has stayed in your setup after the first week?&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Build a Full-Stack Music Station with OpenRouter, Amazon Bedrock, and Nuxt</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Wed, 26 Aug 2026 00:48:03 +0000</pubDate>
      <link>https://dev.to/aws/build-a-full-stack-music-station-with-openrouter-amazon-bedrock-and-nuxt-5fh4</link>
      <guid>https://dev.to/aws/build-a-full-stack-music-station-with-openrouter-amazon-bedrock-and-nuxt-5fh4</guid>
      <description>&lt;p&gt;Have you ever been coding and then gotten into that flow state? You know where hours pass by , and it feels to you it's only been a few minutes? Me too. One thing that really helps me get into that state is music. So I create my own music Lo-Fi server called compile and chill. &lt;/p&gt;

&lt;p&gt;As a part of this project, I created three radio stations. Each station can generate a 16:9 scene with &lt;a href="https://aws.amazon.com/bedrock/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock&lt;/a&gt;, compose an instrumental loop with ElevenLabs, and turn an illustration into a six-second video through OpenRouter. Generated files live in private &lt;a href="https://aws.amazon.com/s3/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon S3&lt;/a&gt; storage and return to the browser through the Nuxt server.&lt;/p&gt;

&lt;p&gt;I also added a Stream Deck API interface! &lt;/p&gt;

&lt;p&gt;This tutorial shows how to build this radio station from start to finish. &lt;/p&gt;

&lt;p&gt;The complete source code is available in the &lt;a href="https://github.com/ErikCH/compile-and-chill" rel="noopener noreferrer"&gt;Compile &amp;amp; Chill repository&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/6tEVRqptHmg" width="100%" height="390"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=6tEVRqptHmg" rel="noopener noreferrer"&gt;Watch the full video on YouTube&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need the following tools for the complete build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 22.19 or newer. The locked Nuxt 4.5.2 release requires Node 22.19+, 24.11+, or 26+.&lt;/li&gt;
&lt;li&gt;npm 10 or newer.&lt;/li&gt;
&lt;li&gt;An AWS account and a configured &lt;a href="https://aws.amazon.com/cli/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS Command Line Interface (AWS CLI)&lt;/a&gt; profile.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS Serverless Application Model (AWS SAM) CLI&lt;/a&gt; for the private storage stack.&lt;/li&gt;
&lt;li&gt;Access to Stability AI Stable Image Ultra through Amazon Bedrock in &lt;code&gt;us-west-2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An ElevenLabs API key for music generation.&lt;/li&gt;
&lt;li&gt;An OpenRouter API key for animated scenes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The provider credentials are optional. Without them, the UI, bundled scene, station switching, player, and Focus Block timer still work.&lt;/p&gt;

&lt;p&gt;The identity running the app needs &lt;code&gt;bedrock:InvokeModel&lt;/code&gt; plus bucket-scoped permissions for &lt;code&gt;s3:GetObject&lt;/code&gt;, &lt;code&gt;s3:PutObject&lt;/code&gt;, &lt;code&gt;s3:DeleteObject&lt;/code&gt;, &lt;code&gt;s3:DeleteObjectVersion&lt;/code&gt;, and &lt;code&gt;s3:ListBucketVersions&lt;/code&gt;. Use a role or profile scoped to the station bucket rather than an administrator identity.&lt;/p&gt;

&lt;p&gt;For this project I included infrastructure as code with SAM to help setup the AWS parts. It's also included in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Run the station without credentials
&lt;/h3&gt;

&lt;p&gt;Pull down the repo and get started!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ErikCH/compile-and-chill.git
&lt;span class="nb"&gt;cd &lt;/span&gt;compile-and-chill
npm ci
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://127.0.0.1:8231&lt;/code&gt;. You should see the station UI with the bundled placeholder scene.&lt;/p&gt;

&lt;p&gt;The environment file separates each feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ELEVENLABS_API_KEY=
BEDROCK_IMAGE_REGION=us-west-2
BEDROCK_IMAGE_MODEL_ID=stability.stable-image-ultra-v1:1
AWS_PROFILE=default
STATION_S3_BUCKET=
STATION_S3_REGION=us-west-2
OPENROUTER_API_KEY=
STATION_BIND_HOST=127.0.0.1
NUXT_CONTROL_TOKEN=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not add &lt;code&gt;NUXT_PUBLIC_&lt;/code&gt; to these names. Nuxt exposes public runtime configuration to browser code.&lt;/p&gt;

&lt;p&gt;Keep &lt;code&gt;STATION_BIND_HOST&lt;/code&gt; on loopback unless you need remote control. For Stream Deck access from another machine, bind to a private VPN interface address, never &lt;code&gt;0.0.0.0&lt;/code&gt; or a public IP, and set a long random &lt;code&gt;NUXT_CONTROL_TOKEN&lt;/code&gt;. The server refuses a non-loopback binding without that token.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Put provider calls behind Nuxt server routes
&lt;/h3&gt;

&lt;p&gt;The browser should call your application. Compile &amp;amp; Chill keeps provider credentials in Nuxt &lt;code&gt;runtimeConfig&lt;/code&gt; and places provider code under &lt;code&gt;server/api/&lt;/code&gt; and &lt;code&gt;server/utils/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fouhe9zlk3lg60u72i7m6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fouhe9zlk3lg60u72i7m6.png" alt="Architecture diagram showing the browser calling Nuxt server routes, which connect to Amazon Bedrock, ElevenLabs, OpenRouter, and private Amazon S3 storage" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The relevant section of &lt;code&gt;nuxt.config.ts&lt;/code&gt; looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;runtimeConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;elevenLabsApiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ELEVENLABS_API_KEY&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;imageGenerationRegion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BEDROCK_IMAGE_REGION&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-west-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;imageGenerationModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BEDROCK_IMAGE_MODEL_ID&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stability.stable-image-ultra-v1:1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stationStorageBucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STATION_S3_BUCKET&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stationStorageRegion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STATION_S3_REGION&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-west-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;openRouterApiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENROUTER_API_KEY&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This boundary gives you one place to validate requests, clamp paid parameters, redact signed URLs, and translate provider errors into useful HTTP responses.&lt;/p&gt;

&lt;p&gt;You can inspect the complete routes in &lt;a href="https://github.com/ErikCH/compile-and-chill/tree/21c60e6508f6f9c1fbda21ceab18b60a14b20aa1/server/api" rel="noopener noreferrer"&gt;&lt;code&gt;server/api&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Generate a scene with Amazon Bedrock
&lt;/h3&gt;

&lt;p&gt;Authenticate with your AWS profile first. This example uses &lt;a href="https://aws.amazon.com/iam/identity-center/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS IAM Identity Center&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sso login &lt;span class="nt"&gt;--profile&lt;/span&gt; your-profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the profile and model settings to &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS_PROFILE=your-profile
BEDROCK_IMAGE_REGION=us-west-2
BEDROCK_IMAGE_MODEL_ID=stability.stable-image-ultra-v1:1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image route validates the station mode, visual style, developer presentation, and optional seed. It then calls &lt;code&gt;generateStationScene()&lt;/code&gt; in &lt;a href="https://github.com/ErikCH/compile-and-chill/blob/21c60e6508f6f9c1fbda21ceab18b60a14b20aa1/server/utils/station-generation.ts" rel="noopener noreferrer"&gt;&lt;code&gt;server/utils/station-generation.ts&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The provider payload is small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildScenePrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;visualStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;developer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;diversitySeed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="c1"&gt;// Shortened here. The source contains the complete negative prompt.&lt;/span&gt;
  &lt;span class="na"&gt;negative_prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;logos, readable text, watermark, rear view, motion blur, extra fingers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;visualStyle&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;illustrated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;photograph, live action, photorealistic skin, 3D render&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cartoon, anime, cel shading, flat illustration&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-to-image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;aspect_ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;16:9&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;output_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InvokeModelCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageGenerationModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server normalizes the seed before constructing the payload. This matters because &lt;code&gt;Number(undefined)&lt;/code&gt; becomes &lt;code&gt;NaN&lt;/code&gt;, and JSON serializes &lt;code&gt;NaN&lt;/code&gt; as &lt;code&gt;null&lt;/code&gt;. Stable Image Ultra expects an integer.&lt;/p&gt;

&lt;p&gt;The prompt also ties visual style to output type. Static scenes use a realistic style. Animated scenes use a clearly illustrated 2D style so the source is visibly non-photorealistic. I added that distinction after a provider's person-likeness classifier refused one of the illustrated anchors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoint:&lt;/strong&gt; restart the app, open Visual settings, choose Realistic static, and generate one scene. If the model is unavailable in the configured region, the route returns the provider error and model ID instead of silently switching models.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Add looping music with ElevenLabs
&lt;/h3&gt;

&lt;p&gt;The UI sends a 60-second duration, and the server should reject or normalize nonnumeric input before applying its 10-to-60-second bounds. This hardened version avoids sending &lt;code&gt;null&lt;/code&gt; to the provider if another client calls the route with an invalid value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestedDuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;durationSeconds&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;durationSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestedDuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requestedDuration&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.elevenlabs.io/v1/music?output_format=mp3_44100_128&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xi-api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elevenLabsApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildMusicPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;music_length_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;durationSeconds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;music_v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;generation_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;force_instrumental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sign_with_c2pa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;generation_mode: 'loop'&lt;/code&gt; is one part of making the repeat sound natural. The prompt also asks for no intro pickup, ending cadence, or fade-out. A track written like a normal song sounds broken when it jumps from the ending back to the first beat.&lt;/p&gt;

&lt;p&gt;Compile &amp;amp; Chill keeps one generated track per station mode. Switching from Deep Work to Rainy Debug and back reuses the existing track instead of making another paid request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoint:&lt;/strong&gt; add &lt;code&gt;ELEVENLABS_API_KEY&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt;, restart the server, and generate one 60-second track. Switch modes and confirm that returning to the original mode reuses its audio.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Deploy the private media library
&lt;/h3&gt;

&lt;p&gt;The repository includes an AWS SAM template managed by &lt;a href="https://aws.amazon.com/cloudformation/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS CloudFormation&lt;/a&gt;. Validate and deploy it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam validate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-file&lt;/span&gt; infra/compile-and-chill-private-station.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2

sam deploy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; compile-and-chill-private-station &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-file&lt;/span&gt; infra/compile-and-chill-private-station.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-confirm-changeset&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-fail-on-empty-changeset&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the media bucket output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation describe-stacks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; compile-and-chill-private-station &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Stacks[0].Outputs[?OutputKey=='StationMediaBucketName'].OutputValue"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy that value into &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STATION_S3_BUCKET=YOUR_STACK_OUTPUT
STATION_S3_REGION=us-west-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stack creates two versioned buckets. One stores generated station media. The other stores &lt;a href="https://aws.amazon.com/cloudtrail/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS CloudTrail&lt;/a&gt; data-event logs encrypted with &lt;a href="https://aws.amazon.com/kms/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS Key Management Service (AWS KMS)&lt;/a&gt;. An &lt;a href="https://aws.amazon.com/cloudwatch/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon CloudWatch&lt;/a&gt; alarm watches delete requests against the media bucket.&lt;/p&gt;

&lt;p&gt;The application assigns each browser profile a random station ID in an HttpOnly, SameSite cookie. Objects use this layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stations/&amp;lt;station-id&amp;gt;/manifest.json
stations/&amp;lt;station-id&amp;gt;/&amp;lt;mode&amp;gt;/scene.png
stations/&amp;lt;station-id&amp;gt;/&amp;lt;mode&amp;gt;/scene.mp4
stations/&amp;lt;station-id&amp;gt;/&amp;lt;mode&amp;gt;/music.mp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser receives same-origin URLs such as &lt;code&gt;/api/library/assets/deepWork/scene&lt;/code&gt;. The route reads the matching S3 object into server memory and returns it. The current implementation is a proxy, not a streaming pass-through, so account for memory when increasing the 50 MiB video limit.&lt;/p&gt;

&lt;p&gt;The storage implementation is in &lt;a href="https://github.com/ErikCH/compile-and-chill/blob/21c60e6508f6f9c1fbda21ceab18b60a14b20aa1/server/utils/station-library.ts" rel="noopener noreferrer"&gt;&lt;code&gt;server/utils/station-library.ts&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoint:&lt;/strong&gt; regenerate a scene or track, reload the page in the same browser profile, and confirm that the media returns. Open a private browsing window and confirm that it starts with a separate station.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Turn one illustration into a seamless video loop
&lt;/h3&gt;

&lt;p&gt;Animated mode generates a fresh illustrated anchor with Amazon Bedrock, saves it to S3, and creates a 300-second presigned read URL. OpenRouter needs that temporary URL because its video provider must download the input image.&lt;/p&gt;

&lt;p&gt;The trick is to submit the same anchor as both frame constraints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;720p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;aspect_ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;16:9&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;generate_audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Animate this 2D cartoon illustration of a fictional character.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Use a locked-off camera and keep the illustrated composition unchanged.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The provided illustration is both the first and last frame.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Allow only tiny two-hand typing motion and one gentle blink.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No zoom, pan, cut, identity change, new objects, or geometry changes.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Create a silent seamless six-second ambient loop.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;frame_images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sourceUrl&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;frame_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first_frame&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sourceUrl&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;frame_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;last_frame&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ryc46qkmezlhtirqt03.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ryc46qkmezlhtirqt03.png" alt="Seven-step animation pipeline showing an Amazon Bedrock anchor saved to Amazon S3, signed for five minutes, used as both video endpoints, polled, downloaded, and saved" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The model can move away from the anchor, but the matching frame constraints request a return to the same composition at the end. The prompt limits movement to a small typing motion and one blink, with no camera movement, reframing, cuts, new objects, or identity changes.&lt;/p&gt;

&lt;p&gt;The app discovers compatible video models at runtime. A candidate must support six seconds, 720p, &lt;code&gt;first_frame&lt;/code&gt;, and &lt;code&gt;last_frame&lt;/code&gt;. It prefers Seedance 2.0 Fast, then checks a short fallback list.&lt;/p&gt;

&lt;p&gt;The fallback policy is intentionally narrow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP &lt;code&gt;402&lt;/code&gt; stops because credits are unavailable.&lt;/li&gt;
&lt;li&gt;HTTP &lt;code&gt;429&lt;/code&gt; stops because the account is rate limited.&lt;/li&gt;
&lt;li&gt;An input-image moderation refusal may try the next compatible model because no video job was created.&lt;/li&gt;
&lt;li&gt;Other failures stop rather than risk submitting and billing a duplicate job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See the complete policy in &lt;a href="https://github.com/ErikCH/compile-and-chill/blob/21c60e6508f6f9c1fbda21ceab18b60a14b20aa1/server/utils/openrouter-video.ts" rel="noopener noreferrer"&gt;&lt;code&gt;server/utils/openrouter-video.ts&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Video generation takes minutes rather than seconds, and provider timing and prices change. Treat the request as a paid background job even if the first version runs inside one long HTTP request.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Report real progress during a long request
&lt;/h3&gt;

&lt;p&gt;A spinner cannot tell the user whether a three-minute provider job is moving or stuck. Compile &amp;amp; Chill keeps a process-local job registry with six phases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;creating anchor
preparing source
submitting video
rendering loop
downloading loop
saving privately
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser creates a job ID, starts the generation request, and polls the Nuxt status endpoint every 750 milliseconds. Nuxt polls the provider every 15 seconds, up to 48 times. These are separate loops.&lt;/p&gt;

&lt;p&gt;The registry also rejects a second active job for the same station with HTTP &lt;code&gt;409&lt;/code&gt;. That protects the user from duplicate clicks, but it is not a distributed queue. A process restart loses status, and multiple Nuxt instances would each have their own registry. For a multi-instance deployment, move job state and concurrency control to a shared data store and worker queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Validate the build
&lt;/h3&gt;

&lt;p&gt;Run the same checks used for the source project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;test
&lt;/span&gt;npm run typecheck
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current repository has 26 passing tests plus a clean type check and production build.&lt;/p&gt;

&lt;p&gt;Then test one provider at a time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with no credentials and verify the UI.&lt;/li&gt;
&lt;li&gt;Add Amazon Bedrock and generate one static scene.&lt;/li&gt;
&lt;li&gt;Add ElevenLabs and generate one track.&lt;/li&gt;
&lt;li&gt;Deploy the storage stack and verify reload persistence.&lt;/li&gt;
&lt;li&gt;Add OpenRouter last, acknowledge the paid operation, and generate one animated loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This order keeps failures small. If the video path fails, you already know that image generation, storage, and browser identity work independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;The cleanup steps remove data permanently. Download anything you want to keep before continuing. The buckets and KMS key use retention policies, so they can remain after stack deletion and may continue to incur charges.&lt;/p&gt;

&lt;p&gt;First, use &lt;strong&gt;Delete saved station&lt;/strong&gt; inside each browser profile whose generated media should be removed. This purges object versions and delete markers under that profile's station prefix.&lt;/p&gt;

&lt;p&gt;Before deleting the stack, record the retained resource names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MEDIA_BUCKET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws cloudformation describe-stacks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; compile-and-chill-private-station &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Stacks[0].Outputs[?OutputKey=='StationMediaBucketName'].OutputValue"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;AUDIT_BUCKET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws cloudformation describe-stacks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; compile-and-chill-private-station &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Stacks[0].Outputs[?OutputKey=='CloudTrailLogBucketName'].OutputValue"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;KMS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws kms describe-key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-id&lt;/span&gt; &lt;span class="nb"&gt;alias&lt;/span&gt;/compile-and-chill-cloudtrail &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; KeyMetadata.KeyId &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete the non-retained stack resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam delete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; compile-and-chill-private-station &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The media bucket, audit bucket, and KMS key are retained by design. Empty &lt;strong&gt;all versions and delete markers&lt;/strong&gt; from both versioned buckets in the S3 console, then delete the buckets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api delete-bucket &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MEDIA_BUCKET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2
aws s3api delete-bucket &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AUDIT_BUCKET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schedule the retained KMS key for deletion after confirming that you no longer need the encrypted audit logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws kms schedule-key-deletion &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KMS_KEY_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pending-window-in-days&lt;/span&gt; 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;KMS key deletion has a waiting period. After deletion, data encrypted only by that key cannot be recovered.&lt;/p&gt;

&lt;p&gt;For local cleanup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; node_modules .nuxt .output
&lt;span class="nb"&gt;rm&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Finale
&lt;/h2&gt;

&lt;p&gt;This has been a very fun project. Let me know if you try it out!&lt;/p&gt;

&lt;p&gt;You can explore every route, prompt, infrastructure resource, and test in the &lt;a href="https://github.com/ErikCH/compile-and-chill" rel="noopener noreferrer"&gt;Compile &amp;amp; Chill source code&lt;/a&gt;. For another Nuxt project that keeps AI actions behind explicit user approval, read &lt;a href="https://dev.to/aws/how-to-build-an-ai-agent-that-asks-permission-first-nuxt-ai-sdk-7-n42"&gt;How to Build an AI Agent That Asks Permission First&lt;/a&gt;. Leave a comment on what you think! Thank!&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>aws</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Build an AI Agent That Asks Permission First (Nuxt + AI SDK 7)</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 18 Aug 2026 18:38:25 +0000</pubDate>
      <link>https://dev.to/aws/how-to-build-an-ai-agent-that-asks-permission-first-nuxt-ai-sdk-7-n42</link>
      <guid>https://dev.to/aws/how-to-build-an-ai-agent-that-asks-permission-first-nuxt-ai-sdk-7-n42</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I did something stupid. I built a superhero-themed Nuxt app, connected it to an Anthropic model through &lt;a href="https://aws.amazon.com/bedrock/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock&lt;/a&gt;, and gave it a tool that deletes files from my computer. In fact, if I wasn't careful, it could have deleted all my files!&lt;/p&gt;

&lt;p&gt;The first time I tried it, I didn't use any sort of approval mechanism. And as you expected it just deleted things. Then I looked into how my coding agent works, and I learned about tool approvals.&lt;/p&gt;

&lt;p&gt;I learned that &lt;a href="https://vercel.com/blog/ai-sdk-7" rel="noopener noreferrer"&gt;AI SDK 7&lt;/a&gt; has a tool approval at the model-call level. It works by pausing for an approval, showing an approval window, and then deleting it. I then put &lt;a href="https://kiro.dev/docs/cli/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro CLI&lt;/a&gt; behind the same interface using Agent Client Protocol (ACP).&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/RktwtjobGI4"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=RktwtjobGI4" rel="noopener noreferrer"&gt;Watch the full video on YouTube&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 22 or later. AI SDK 7 requires Node.js 22 and uses ECMAScript modules (ESM).&lt;/li&gt;
&lt;li&gt;npm 11 or another package manager that works with Nuxt 4.&lt;/li&gt;
&lt;li&gt;AWS credentials available through the standard provider chain.&lt;/li&gt;
&lt;li&gt;Access to an Amazon Bedrock model in your AWS Region.&lt;/li&gt;
&lt;li&gt;The AWS CLI if you want to list the inference profiles available to your account.&lt;/li&gt;
&lt;li&gt;An authenticated Kiro CLI installation for the optional ACP section.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Create the Nuxt app
&lt;/h2&gt;

&lt;p&gt;Create the project and install the versions used in the recorded demo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nuxi@latest init nuxt-agent-approval
&lt;span class="nb"&gt;cd &lt;/span&gt;nuxt-agent-approval

npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  nuxt@4.5.2 &lt;span class="se"&gt;\&lt;/span&gt;
  vue@3.5.41 &lt;span class="se"&gt;\&lt;/span&gt;
  ai@7.0.66 &lt;span class="se"&gt;\&lt;/span&gt;
  @ai-sdk/vue@4.0.66 &lt;span class="se"&gt;\&lt;/span&gt;
  @ai-sdk/amazon-bedrock@5.0.57 &lt;span class="se"&gt;\&lt;/span&gt;
  @aws-sdk/credential-providers@3.1111.0 &lt;span class="se"&gt;\&lt;/span&gt;
  @nuxt/ui@4.10.0 &lt;span class="se"&gt;\&lt;/span&gt;
  zod@4.4.3

npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; @iconify-json/lucide@1.2.123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register Nuxt UI and expose the Amazon Bedrock settings through server-side runtime config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// nuxt.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nuxt/ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;css&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~/assets/css/main.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;runtimeConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;awsRegion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AWS_REGION&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-west-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;bedrockModelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NUXT_BEDROCK_MODEL_ID&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the two Nuxt UI imports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* app/assets/css/main.css */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"@nuxt/ui"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can compare your setup with the &lt;a href="https://github.com/ErikCH/blog-posts/tree/main/apps/ai-sdk-7-nuxt-agents" rel="noopener noreferrer"&gt;complete companion project&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Keep the file tool inside a fixture directory
&lt;/h2&gt;

&lt;p&gt;The video uses two fixture files, &lt;code&gt;old-draft.md&lt;/code&gt; and &lt;code&gt;keep-me.md&lt;/code&gt;. Create them before adding the tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; fixtures
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'# Old draft\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fixtures/old-draft.md
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'# Keep me\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fixtures/keep-me.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent can list or remove files in that directory, but it should not accept a path such as &lt;code&gt;../../package.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Approval decides whether a tool runs. It does not decide what the tool can reach after it starts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/utils/file-tools.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lstat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readdir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;realpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isAbsolute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveInsideFixtures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixtures&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inputPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;isAbsolute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;createError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;statusMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Path escapes the fixtures directory: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;inputPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;List the files in the project fixtures directory.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({}),&lt;/span&gt;
  &lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixtures&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;withFileTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFile&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deleteFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Permanently delete one file from the fixtures directory.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolveInsideFixtures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;lstat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;isFile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSymbolicLink&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;deleted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Not a regular file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;realpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixtures&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canonicalTarget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;realpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canonicalTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;isAbsolute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;createError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;statusMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;File resolves outside fixtures&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canonicalTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;deleted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second check happens after &lt;code&gt;realpath()&lt;/code&gt;. That catches a path that looked local before resolution but points outside the fixture directory through a symbolic link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Connect the route to Amazon Bedrock
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I used Amazon Bedrock, but you can use provider with AI-SDK&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create the provider in &lt;code&gt;server/utils/bedrock.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createAmazonBedrock&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ai-sdk/amazon-bedrock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fromNodeProviderChain&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aws-sdk/credential-providers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useBedrock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;awsRegion&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRuntimeConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createAmazonBedrock&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;awsRegion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;credentialProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fromNodeProviderChain&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fromNodeProviderChain()&lt;/code&gt; uses the AWS credentials already available to your local environment, including &lt;a href="https://aws.amazon.com/iam/identity-center/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS IAM Identity Center&lt;/a&gt; sessions, named profiles, environment variables, and instance roles. You do not need to put a long-lived access key in the Nuxt project.&lt;/p&gt;

&lt;p&gt;Do not copy a model ID from this post. Available IDs vary by account and AWS Region. List the active &lt;a href="https://aws.amazon.com/blogs/machine-learning/getting-started-with-cross-region-inference-in-amazon-bedrock/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock inference profiles&lt;/a&gt; for your account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws bedrock list-inference-profiles &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'inferenceProfileSummaries[?status==`ACTIVE`].inferenceProfileId'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set one of the returned profile IDs before starting Nuxt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-west-2
&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Inference profile ID: "&lt;/span&gt; NUXT_BEDROCK_MODEL_ID
&lt;span class="nb"&gt;export &lt;/span&gt;NUXT_BEDROCK_MODEL_ID
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now add an unguarded chat route. Starting without approval makes the failure visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/api/chat.post.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;stepCountIs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;convertToModelMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;toUIMessageStream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createUIMessageStreamResponse&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AmazonBedrockProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ai-sdk/amazon-bedrock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BedrockModelId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Parameters&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AmazonBedrockProvider&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineEventHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bedrockModelId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRuntimeConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bedrock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useBedrock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bedrockModelId&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;BedrockModelId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Manage files in the fixture project. List files before deleting. Never guess a filename.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;convertToModelMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;listFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deleteFile&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;stopWhen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;stepCountIs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toUIMessageStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createUIMessageStreamResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;stopWhen&lt;/code&gt; is important. AI SDK 7 stops after one step by default. The model can call &lt;code&gt;deleteFile&lt;/code&gt;, receive the result, and then stop before it tells the user what happened. Five steps leave room to list, delete, and summarize while keeping the loop bounded.&lt;/p&gt;

&lt;p&gt;At this point, &lt;code&gt;delete old-draft.md&lt;/code&gt; removes the file as soon as the model selects the tool. That is what happened in the first minute of the video.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Add approval to the delete tool
&lt;/h2&gt;

&lt;p&gt;Add one option to the &lt;code&gt;streamText()&lt;/code&gt; call (toolApproval):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bedrockModelId&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;BedrockModelId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Manage files in the fixture project. List files before deleting. Never guess a filename.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;convertToModelMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;listFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deleteFile&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;stopWhen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;stepCountIs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;toolApproval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;deleteFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-approval&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The option lives on &lt;code&gt;streamText()&lt;/code&gt;, not inside the tool definition. The same &lt;code&gt;deleteFile&lt;/code&gt; tool might run unattended in a maintenance job and require a person in a customer-facing chat. &lt;/p&gt;

&lt;p&gt;AI SDK 7 supports more than a yes-or-no policy. A policy function can approve a call, deny it without asking, or send it to the user. This example uses the direct &lt;code&gt;user-approval&lt;/code&gt; status because every delete should stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Render the approval request in Nuxt
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ui.nuxt.com/docs/components/chat" rel="noopener noreferrer"&gt;Nuxt UI's chat documentation&lt;/a&gt; follows the same AI SDK message-part model. The recorded app uses &lt;code&gt;useChat()&lt;/code&gt; from &lt;code&gt;@ai-sdk/vue&lt;/code&gt; and the &lt;code&gt;isToolApprovalPending()&lt;/code&gt; helper from Nuxt UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;DefaultChatTransport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;getToolName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isTextUIPart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isToolUIPart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;lastAssistantMessageIsCompleteWithApprovalResponses&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useChat&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ai-sdk/vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isToolApprovalPending&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nuxt/ui/utils/ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;addToolApprovalResponse&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useChat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DefaultChatTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;sendAutomaticallyWhen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lastAssistantMessageIsCompleteWithApprovalResponses&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sendAutomaticallyWhen&lt;/code&gt; resumes the interrupted turn after the user answers. Without it, the approval state changes in the browser but the agent does not continue on its own.&lt;/p&gt;

&lt;p&gt;Render tool parts and attach the two decisions while approval is pending:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"message in messages"&lt;/span&gt; &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"message.id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt;
      &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"(part, index) in message.parts"&lt;/span&gt;
      &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"`$&lt;/span&gt;{message.id}-${part.type}-${index}`"
    &amp;gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"isToolUIPart(part)"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"tool-card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nf"&gt;getToolName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;pre&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"part.input"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"isToolApprovalPending(part)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
            &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
            &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"addToolApprovalResponse(&lt;/span&gt;{
              id: part.approval!.id,
              approved: true
            })"
          &amp;gt;
            Do it
          &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

          &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
            &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
            &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"addToolApprovalResponse(&lt;/span&gt;{
              id: part.approval!.id,
              approved: false,
              reason: 'The user declined this file deletion.'
            })"
          &amp;gt;
            Nope
          &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"isTextUIPart(part)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;submit.prevent=&lt;/span&gt;&lt;span class="s"&gt;"onSubmit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"input"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"delete old-draft.md"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;:disabled=&lt;/span&gt;&lt;span class="s"&gt;"status !== 'ready'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FYI, An approval button is not useful when the person cannot see which path the agent wants to remove.&lt;/p&gt;

&lt;p&gt;The denial reason is also useful. &lt;code&gt;approved: false&lt;/code&gt; tells the model it cannot run the tool. The reason gives it enough context to explain what happened rather than trying the same call again.&lt;/p&gt;

&lt;p&gt;Run the prompt twice. Deny it once and confirm that &lt;code&gt;old-draft.md&lt;/code&gt; remains. Reset the fixtures, repeat the prompt, approve it, and confirm that the file disappears. The model received the same request both times. &lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Put Kiro CLI behind the same Nuxt UI
&lt;/h2&gt;

&lt;p&gt;The video switches the backend for the final demo. Instead of sending the prompt directly to an Anthropic model through Amazon Bedrock, the Nuxt app talks to Kiro CLI over &lt;a href="https://kiro.dev/docs/cli/acp/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Agent Client Protocol&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;ACP gives the app a common way to start an agent session, send a prompt, receive tool events, and answer permission requests. Kiro runs as a separate process and keeps its existing agent tools and Model Context Protocol (MCP) integrations. The Nuxt app remains responsible for the interface and the host tools it exposes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F343y1kh8jr47vrby673y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F343y1kh8jr47vrby673y.png" alt="ACP Image" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Install the AI SDK harness packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  @ai-sdk/harness@1.0.73 &lt;span class="se"&gt;\&lt;/span&gt;
  @ai-sdk/harness-acp@1.0.11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Kiro route creates a &lt;code&gt;HarnessAgent&lt;/code&gt; instead of calling &lt;code&gt;streamText()&lt;/code&gt; directly. The &lt;code&gt;createKiroHarness()&lt;/code&gt; ACP preset and &lt;code&gt;createUnsafeLocalSandbox()&lt;/code&gt; development adapter come from the companion project, so treat this as the route configuration rather than a standalone file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HarnessAgent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ai-sdk/harness/agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;kiroExecutable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;which&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kiro-cli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HarnessAgent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;harness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createKiroHarness&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4100&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createUnsafeLocalSandbox&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4100&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;hostBins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;harnessId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kiro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kiro-cli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;kiroExecutable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;permissionMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;allow-reads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Manage files in the fixture project. List files before deleting. Never guess a filename.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;listFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deleteFile&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;toolApproval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;deleteFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-approval&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/ErikCH/blog-posts/tree/main/apps/ai-sdk-7-nuxt-agents/server" rel="noopener noreferrer"&gt;complete Kiro route and ACP preset&lt;/a&gt; include session creation and approval continuation. When a response comes back from the browser, the route gathers pending approval responses and calls &lt;code&gt;continueStream()&lt;/code&gt; against the Kiro session.&lt;/p&gt;

&lt;p&gt;My recorded run asked more than once before deleting &lt;code&gt;keep-me.md&lt;/code&gt;. Kiro confirmed the target, its permission flow asked to run the tool, and the host &lt;code&gt;deleteFile&lt;/code&gt; policy asked for the final approval. It was a little repetitive, but it exposed an important boundary. Kiro's built-in permissions and AI SDK's host-tool approval are separate systems.&lt;/p&gt;

&lt;p&gt;The local sandbox in this sample is a development adapter. It limits file API paths to a temporary root, but processes still run as the current operating-system user. Replace it with an isolated sandbox provider before exposing a coding agent to untrusted prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Keep approval in its lane
&lt;/h2&gt;

&lt;p&gt;Tool approval is a product DX improvement, it isn't security.&lt;/p&gt;

&lt;p&gt;If a person approves the wrong path, the tool still removes the wrong path. If the tool can reach the rest of the filesystem, approval does not narrow that access. Keep the path checks from Step 2, apply authorization inside the tool, validate inputs on the server, and use an isolated runtime for agents that can run commands.&lt;/p&gt;

&lt;p&gt;The sample app also keeps its recording controls in development mode. The browser can turn approval off for the first demo, but the built app ignores that flag and requires approval. A client-controlled switch that disables confirmation should not ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;Stop the Nuxt development server. If you deleted either fixture during the demo, recreate both files before your next run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'# Old draft\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fixtures/old-draft.md
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'# Keep me\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fixtures/keep-me.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tutorial does not provision AWS resources. Amazon Bedrock requests can still incur charges, so stop sending test prompts when you finish.&lt;/p&gt;

&lt;p&gt;If you delete the local project directory, its files and any local session state are removed. Copy anything you want to keep before deleting it. AWS credentials loaded through the provider chain remain in their original profile or identity-center cache; this app does not write them into the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finale
&lt;/h2&gt;

&lt;p&gt;I learned a lot by adding this tool approval process. And even though I normally use Strands Agents, the AI SDK 7 worked really well.&lt;/p&gt;

&lt;p&gt;Make sure to leave a comment below if you got this far!&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://vercel.com/blog/ai-sdk-7" rel="noopener noreferrer"&gt;AI SDK 7 announcement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/changelog/ai-sdk-7" rel="noopener noreferrer"&gt;AI SDK 7 changelog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sdk.vercel.ai/docs/getting-started/nuxt" rel="noopener noreferrer"&gt;AI SDK Nuxt guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ui.nuxt.com/docs/components/chat" rel="noopener noreferrer"&gt;Nuxt UI chat components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kiro.dev/docs/cli/acp/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro CLI ACP documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/machine-learning/getting-started-with-cross-region-inference-in-amazon-bedrock/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock cross-Region inference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ErikCH/blog-posts/tree/main/apps/ai-sdk-7-nuxt-agents" rel="noopener noreferrer"&gt;Companion code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nuxt</category>
      <category>ai</category>
      <category>aws</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
