<?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>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>
    <item>
      <title>Nuxt 4.5 SSR Streaming Is Kind Of A Big Deal</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 11 Aug 2026 17:41:30 +0000</pubDate>
      <link>https://dev.to/erikch/nuxt-45-ssr-streaming-is-kind-of-a-big-deal-2i37</link>
      <guid>https://dev.to/erikch/nuxt-45-ssr-streaming-is-kind-of-a-big-deal-2i37</guid>
      <description>&lt;p&gt;&lt;a href="https://nuxt.com/blog/v4-5" rel="noopener noreferrer"&gt;Nuxt 4.5&lt;/a&gt; launched last month and it's really neat. One of my most favorite features is the experimental SSR streaming. With one simple change in the Nuxt config I was able to reduce the Largest Contentful Paint (LCP) by over 2 seconds. I simply enabled the experimental SSR streaming, and the first byte arrived in 16 milliseconds.&lt;/p&gt;

&lt;p&gt;With out it, the server spent about 2.5 seconds rendering the page. Streaming changed what the browser could display during that wait. &lt;/p&gt;

&lt;p&gt;As a part of my testing I also tried out two other features: &lt;code&gt;useLayout()&lt;/code&gt; and named views. Let's take a look at all of these now!&lt;/p&gt;

&lt;p&gt;This post walks through all three features using Nuxt 4.5.2. Let's take a look!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you rather watch check out my full video here&lt;/strong&gt;&lt;/p&gt;

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

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

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

&lt;ul&gt;
&lt;li&gt;Node.js 22 or another version supported by Nuxt 4.5&lt;/li&gt;
&lt;li&gt;npm&lt;/li&gt;
&lt;li&gt;Basic experience with Nuxt pages, components, and layouts&lt;/li&gt;
&lt;li&gt;Chrome or another browser with document-request timing tools&lt;/li&gt;
&lt;li&gt;My sample app! &lt;a href="https://github.com/ErikCH/nuxt-ssr-streaming" rel="noopener noreferrer"&gt;https://github.com/ErikCH/nuxt-ssr-streaming&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After cloning down the repo, build it and run it in preview.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm ci
npm run typecheck
npm run build
npm run preview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo preview runs on port 3010. The streamed route is &lt;code&gt;/&lt;/code&gt;, and &lt;code&gt;/buffered&lt;/code&gt; uses the same components with streaming disabled by a route rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature 1: Experimental SSR streaming
&lt;/h2&gt;

&lt;p&gt;Normal server-side rendering can buffer the document until every server-rendered component finishes. A slow database query or API request can leave the browser waiting even when the page shell is ready.&lt;/p&gt;

&lt;p&gt;Nuxt 4.5 added the experimental &lt;code&gt;ssrStreaming&lt;/code&gt; option. It uses Vue's web-stream renderer to flush ready HTML while later boundaries continue rendering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a controlled slow component
&lt;/h3&gt;

&lt;p&gt;I used a fixed delay so the delivery difference would be easy to see. The delay runs on the server, and &lt;code&gt;useState&lt;/code&gt; serializes the result into the Nuxt payload for hydration.&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SlowResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;resolvedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;serverDurationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;renderMarker&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;withDefaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;defineProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2500&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRoute&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="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SlowResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`slow-panel:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;route&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&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="na"&gt;resolvedAt&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="na"&gt;serverDurationMs&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="na"&gt;renderMarker&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="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="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;server&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;startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delayMs&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;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;resolvedAt&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;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;serverDurationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;renderMarker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;6&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="p"&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;Place the component inside a Vue &lt;code&gt;Suspense&lt;/code&gt; boundary. The fallback becomes part of the shell that Nuxt can send before &lt;code&gt;SlowPanel&lt;/code&gt; resolves.&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;main&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;header&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Nuxt 4.5 rendering test&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;The shell is ready&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;Suspense&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;SlowPanel&lt;/span&gt; &lt;span class="na"&gt;:delay-ms=&lt;/span&gt;&lt;span class="s"&gt;"2500"&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;#fallback&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;aria-live=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt; &lt;span class="na"&gt;aria-busy=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Shell received&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Waiting for the slow server boundary...&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/article&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;/Suspense&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/main&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;The fallback needs to be useful. Navigation, page context, and a clear loading state give the visitor something to work with. Sending an empty shell earlier does little for the experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enable streaming in production
&lt;/h3&gt;

&lt;p&gt;The demo enables streaming outside development and keeps &lt;code&gt;/buffered&lt;/code&gt; as a control route.&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;ssrStreamingEnabled&lt;/span&gt; &lt;span class="o"&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;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&lt;/span&gt;&lt;span class="dl"&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;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;compatibilityDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-07-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;devLogs&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="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;ssrStreaming&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ssrStreamingEnabled&lt;/span&gt;&lt;span class="p"&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;public&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ssrStreamingEnabled&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;routeRules&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;/buffered&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;streaming&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="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;code&gt;/buffered&lt;/code&gt; route uses the same slow component, delay, and shell. Its route rule changes the response mode without changing the work performed by the component.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compare the responses
&lt;/h3&gt;

&lt;p&gt;Open the streamed and buffered routes as full document requests. Disable the browser cache and use a hard reload. Client-side navigation does not create a new SSR document request.&lt;/p&gt;

&lt;p&gt;You can also measure the response chunks from a second terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run measure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My local production preview produced this result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;streamed: first byte 16 ms, complete 2562 ms, 14 chunks, 5.8 KB
buffered: first byte 2518 ms, complete 2518 ms, 1 chunk, 5.8 KB
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both routes performed the same delayed work and returned the same amount of HTML. The streamed route delivered the response in 14 chunks and gave the browser useful HTML much earlier. The buffered route delivered one chunk after the slow boundary finished.&lt;/p&gt;

&lt;p&gt;This was just a test, on a somewhat contrived setup. However, I would try this yourself in your own app and see how the results go. Make sure to try it in development first, before going to production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check the production constraints
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ssrStreaming&lt;/code&gt; is experimental and disabled by default. Nuxt can fall back to buffered rendering for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bots and crawlers&lt;/li&gt;
&lt;li&gt;Cached routes&lt;/li&gt;
&lt;li&gt;Incremental Static Regeneration (ISR)&lt;/li&gt;
&lt;li&gt;Stale-while-revalidate (SWR) routes&lt;/li&gt;
&lt;li&gt;Redirects&lt;/li&gt;
&lt;li&gt;Routes using &lt;code&gt;ssr: false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Prerendered output&lt;/li&gt;
&lt;li&gt;Routes with &lt;code&gt;streaming: false&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Streaming also changes when the HTTP response becomes committed. After the shell has been flushed, later component code may be too late to change the status, headers, or cookies. Test authentication redirects, cookie writes, cache headers, errors, and middleware before enabling streaming on a production route.&lt;/p&gt;

&lt;p&gt;I would start with a content-heavy page that has a useful shell and one isolated slow boundary. Measure the deployed route with its real adapter and route rules. Keep buffering where the application needs to finish response decisions before sending HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature 2: Read the resolved layout with &lt;code&gt;useLayout()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Nuxt 4.5 added a stable &lt;code&gt;useLayout()&lt;/code&gt; composable. It returns a read-only computed ref containing the layout Nuxt resolved for the current route.&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;layout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLayout&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;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;output&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"layout-badge"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;code&amp;gt;&lt;/span&gt;useLayout()&lt;span class="nt"&gt;&amp;lt;/code&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="nx"&gt;layout&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;layout&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;/output&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value accounts for page metadata, route-rule layout selection, and the default layout. It also updates during navigation.&lt;/p&gt;

&lt;p&gt;In the demo, two child pages select different layouts:&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="c"&gt;&amp;lt;!-- app/pages/features/overview.vue --&amp;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="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="nf"&gt;definePageMeta&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&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="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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/pages/features/focus.vue --&amp;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="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="nf"&gt;definePageMeta&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;focus&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="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;The parent page stays mounted while navigation changes the child route. The computed layout updates from &lt;code&gt;default&lt;/code&gt; to &lt;code&gt;focus&lt;/code&gt; without requiring the parent to inspect route metadata itself.&lt;/p&gt;

&lt;p&gt;Reading &lt;code&gt;route.meta.layout&lt;/code&gt; can miss layouts selected elsewhere in Nuxt's resolution chain. Use &lt;code&gt;useLayout()&lt;/code&gt; when a component needs the layout Nuxt is actually rendering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature 3: Render multiple outlets with named views
&lt;/h2&gt;

&lt;p&gt;Named views let one route render into multiple page outlets. Nuxt 4.5 uses a &lt;code&gt;name@view.vue&lt;/code&gt; filename convention for the additional view files.&lt;/p&gt;

&lt;p&gt;The demo uses this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/pages/
  features.vue
  features/
    index.vue
    overview.vue
    overview@sidebar.vue
    focus.vue
    focus@sidebar.vue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parent page creates a default outlet and a named sidebar outlet:&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;section&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Named view outlets"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;article&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;NuxtPage&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;aside&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;NuxtPage&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"sidebar"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/section&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you visit &lt;code&gt;/features/overview&lt;/code&gt;, &lt;code&gt;overview.vue&lt;/code&gt; fills the default outlet and &lt;code&gt;overview@sidebar.vue&lt;/code&gt; fills the sidebar. The &lt;code&gt;focus&lt;/code&gt; files do the same work for &lt;code&gt;/features/focus&lt;/code&gt;. Both outlets belong to one URL, and the parent does not need a manual component map.&lt;/p&gt;

&lt;p&gt;Keep route metadata in the default page file. Nuxt ignores &lt;code&gt;definePageMeta&lt;/code&gt; in an &lt;code&gt;@sidebar&lt;/code&gt; file. If a route has no matching named-view file, that outlet remains empty.&lt;/p&gt;

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

&lt;p&gt;I am really impressed by the incremental changes in Nuxt 4.5. SSR streaming is something I'll be using on all my future apps. And useLayout and named views are nice DX bonuses. &lt;/p&gt;

&lt;p&gt;Leave a comment and let me know which of these features you'll be using next!&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nuxt.com/blog/v4-5" rel="noopener noreferrer"&gt;Nuxt 4.5 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nuxt.com/docs/4.x/guide/going-further/experimental-features" rel="noopener noreferrer"&gt;Nuxt experimental features&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nuxt.com/docs/4.x/api/composables/use-layout" rel="noopener noreferrer"&gt;Nuxt &lt;code&gt;useLayout()&lt;/code&gt; documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nuxt.com/docs/4.x/guide/directory-structure/app/pages#named-views" rel="noopener noreferrer"&gt;Nuxt named views documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vuejs.org/api/ssr.html#rendertowebstream" rel="noopener noreferrer"&gt;Vue server-rendering API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=Bty-hzN8l84" rel="noopener noreferrer"&gt;Watch the full Nuxt 4.5 video&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nuxt</category>
      <category>vue</category>
      <category>ssr</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How To learn Web Development in 2026</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Mon, 03 Aug 2026 17:19:26 +0000</pubDate>
      <link>https://dev.to/erikch/how-to-learn-web-development-in-2026-52j5</link>
      <guid>https://dev.to/erikch/how-to-learn-web-development-in-2026-52j5</guid>
      <description>&lt;p&gt;Learning web development in 2026 is a lot different than it was when I first learned in 2010. Back then, I used Udemy courses, Stack Overflow, and official documentation. I’d spend hours copying and pasting code, only to discover that the blog post I was pulling it from was out of date. It was frustrating, to say the least.&lt;/p&gt;

&lt;p&gt;Luckily, learning a new framework in 2026 is a lot easier than it was. &lt;/p&gt;

&lt;p&gt;Recently I was at Chain React. It's a React Native conference and for &lt;a href="https://chainreactconf.com/talks/how-i-learned-react-native-with-ai" rel="noopener noreferrer"&gt;my talk&lt;/a&gt; I decided to really deep into the framework and learn it. I started doing the same thing I always did, reading the docs, buying courses etc. Then I realized I could use AI to speed up my learning. This is what I want to teach you today, with a few additional tips along the way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want to watch instead, check out my full video on the subject!&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;(Full disclosure before I start naming tools: 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. It's the agentic harness I use daily. Everything here works the same in Claude Code, Cursor, or whatever you already have open.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a goal, not a curriculum
&lt;/h2&gt;

&lt;p&gt;I don't start by learning a framework. I start with something that I want to build for fun.&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%2Fo4bboqg3ouoms52chq52.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%2Fo4bboqg3ouoms52chq52.png" alt="thinking slide" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I was learning React Native, it was a workout tracker. I wanted to learn a program called 75 Hard, which is 75 days of hitting the same set of daily goals including a diet, an outdoor workout, and a second workout. I wanted to check those off on my phone. That was the goal.&lt;/p&gt;

&lt;p&gt;Making sure you have a goal is really important. A todo list app teaches you nothing, because it means almost nothing. A real app forces decisions you have to make, like does it work offline, where does the data live, what happens when permissions get denied. Pick something with a little resistance in it, ideally tied to a hobby so you'll still care as your learning progression continues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide how much you actually need to know
&lt;/h2&gt;

&lt;p&gt;In other words, how much of it do I need to actually learn to achieve my goal?&lt;/p&gt;

&lt;p&gt;In today's age, you need to learn the primitives. The basics, and some architecture of how it all works. You do not need to memorize API signatures anymore.&lt;/p&gt;

&lt;p&gt;Let's imagine you are learning React. If you're using &lt;code&gt;useState&lt;/code&gt; everywhere, understand what it does and why re-renders happen. Whether you can recall the exact argument order from memory is irrelevant, because the model will write it. Same with optional chaining, same with whatever config format the build tool wants this year. Learn the basics of the framework or library you are learning. Let the agent handle the syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask three models what they'd build today
&lt;/h2&gt;

&lt;p&gt;Before I learn anything, I want to know what the ecosystem actually looks like right now. So I ask several models the same broad question and compare.&lt;/p&gt;

&lt;p&gt;When I was learning React Native, I ran the same prompt through Gemini, Claude, and GPT. Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build a mobile application for iOS.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That comes back with Swift, which tells me something. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build a cross-platform iOS and Android application.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I get Flutter or React Native. I'm not asking for code here. I'm reading the consensus from the agent to find out which frameworks and libraries it's recommending, which tells me whether the thing I'm about to learn is the thing I should be learning.&lt;/p&gt;

&lt;p&gt;When the models disagree, that's when I dig in further to find out why. I'll usually google around a bit to see if I'm on the right path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get a personalized learning path
&lt;/h2&gt;

&lt;p&gt;Finally, I work on a personalized learning path, based on what I know already. Here is an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build me a curriculum to learn React Native. Assume I already
have basic knowledge of React, HTML, CSS, and JavaScript.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second sentence is where I brought in my own personalization. I know web development, so I wanted to make sure my learning path is tailored to me. Also, I called out React Native, as per the last section, I learned it's the most popular and makes the most sense for me.&lt;/p&gt;

&lt;p&gt;Because I told it I already knew web basics, it skips React fundamentals and goes straight to what's different: native components instead of the DOM, &lt;code&gt;StyleSheet&lt;/code&gt; instead of the CSS cascade, navigation as a stack instead of URLs, you get the idea. I then get topics with a suggested time frame, and I can work through them at my own pace.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; model selection matters here more than almost anywhere else. I ran this against Sonnet 4 and Opus 5. Sonnet 4 handed me what was essentially 2023 React Native. Opus 5 gave me current information and a noticeably better curriculum. Every model has a different training cutoff, and a stale curriculum is worse than no curriculum, because you don't know which parts are wrong.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're stuck on an older or a local model, run an adversarial review. Have the cheap model draft the learning path, then hand it to a current model and ask what's out of date. Going past two or three models surfaces a surprising amount, and it's also a good idea when you are trying to save tokens.&lt;/p&gt;

&lt;p&gt;Also make sure you use some of the tools your harness gives you. Most harnesses have search built in, plus MCP servers and skills. If you're learning React, install the current React skills before you generate anything. Scaffolding tools help here too: &lt;code&gt;create-next-app&lt;/code&gt; now drops an &lt;code&gt;AGENTS.md&lt;/code&gt; in your project, and that file is a better starting point for your curriculum than the model's memory. &lt;/p&gt;

&lt;h2&gt;
  
  
  Build it: I do, we do, you do
&lt;/h2&gt;

&lt;p&gt;A learning path is a good start. It's still reading. To move to actual understanding I used an old teaching framework from 1983.&lt;/p&gt;

&lt;p&gt;It's called gradual release of responsibility, from a paper by Pearson and Gallagher, and it sits on top of Vygotsky's zone of proximal development. The sequence is &lt;strong&gt;I do, we do, you do&lt;/strong&gt;. The teacher demonstrates, then you work together, then you work alone.&lt;/p&gt;

&lt;p&gt;To use this framework today, the &lt;strong&gt;the AI becomes the "I."&lt;/strong&gt; It does, you watch. Then you work together with the AI, and finally you do it yourself. It's really helped me learn. Let's start with ** I do **&lt;/p&gt;

&lt;h3&gt;
  
  
  I do: let it build, then read it
&lt;/h3&gt;

&lt;p&gt;Let the agent build the whole thing. Just create a simple prompt, and let it do it's thing. Don't type anything. Read the output, look at the file structure, see how it wired things together.&lt;/p&gt;

&lt;p&gt;You are not learning yet, and that's fine. This phase is the right one because it gives you an idea what is possible. If you like you could look over the code that was written, but it's not the best way to learn.&lt;/p&gt;

&lt;h3&gt;
  
  
  We do: spec-driven development
&lt;/h3&gt;

&lt;p&gt;This is where most of the learning actually happened for me.&lt;/p&gt;

&lt;p&gt;Spec-driven development means writing structured specifications first, so the agent can build and verify against them. In practice I ask for a spec instead of an app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a spec that helps build a retro workout planner app.
&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%2Favz2px260gllq0i7yxhm.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%2Favz2px260gllq0i7yxhm.png" alt="wedo graphic" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What comes back is a design document, and the design document is the cheat sheet. Mine told me to use Expo Router 57, &lt;code&gt;expo-sqlite&lt;/code&gt;, an image picker, and notifications. That's a map of the architecture and the current library choices for a framework I didn't know yet. When I was working like this, I would constantly ask clarifying questions and ask why certain decisions were made. I would even often ask for changes.&lt;/p&gt;

&lt;p&gt;Then it generates requirements, usually as user stories. I skip this one when I'm learning. It's more useful when you're shipping to other people.&lt;/p&gt;

&lt;p&gt;The implementation plan is the part you need to pay attention to. It's a task list. &lt;strong&gt;Instead of letting the agent execute it, I work through the tasks myself, with the design doc open.&lt;/strong&gt; When I get stuck, I say so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I tried to install NativeWind 5 and I don't understand what to do here. Can you help?
&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%2Ffbi8yu5j856qvl0efjzs.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%2Ffbi8yu5j856qvl0efjzs.png" alt="implementation plan" width="800" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I get help, and I might even read the official documentation anyways. That's the "we do" phase working exactly as designed. I have enough context to attempt it and a patient buddy for the gaps. It's almost like &lt;a href="https://en.wikipedia.org/wiki/Rubber_duck_debugging" rel="noopener noreferrer"&gt;rubber ducking&lt;/a&gt;, but with something that can respond.&lt;/p&gt;

&lt;p&gt;Keep in mind, if you write a spec and then let the agent implement all of it, you haven't learned much. You can now explain what your app does and still not explain how the framework does it. So I settled on a rule. &lt;strong&gt;Spec the what, hand-write the how, at least once per concept.&lt;/strong&gt; Write the spec for the app, but make sure your writing things yourself (with help if needed). &lt;/p&gt;

&lt;h3&gt;
  
  
  You do: turn it off
&lt;/h3&gt;

&lt;p&gt;Close the tab. Build something small with no assistance.&lt;/p&gt;

&lt;p&gt;Don't skip this step. This is the best way to check your recall and that you really understand everything. I wouldn't write the whole thing from scratch, mind you, but a few pieces just to make sure I understand the underlying concepts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more patterns worth stealing
&lt;/h2&gt;

&lt;p&gt;Let the AI quiz you. Take the learning path you already generated and ask for a quiz on it, then go back and forth. It's a quick way to find out which parts you only think you know.&lt;/p&gt;

&lt;p&gt;The stronger version of this flips the direction. Don't ask the model to explain hooks to you. Explain hooks &lt;em&gt;to the model&lt;/em&gt; and ask it to grade you. "Here's my understanding of the New Architecture, what did I get wrong?" Use text-to-speach if you can, to make this even quicker.&lt;/p&gt;

&lt;p&gt;The other pattern is learning in public, which I was doing long before agents existed. Post what you're learning, on YouTube or Bluesky or a blog. Writing it down for someone else is what exposes the parts you weren't sure on. This post exists because I told a conference I'd stand up and explain React Native, and that deadline taught me more than any course.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I've landed
&lt;/h2&gt;

&lt;p&gt;Start with a goal you care about. Use your usual harness on a current model. Have it build you a plan, with search and MCP servers turned on so it isn't working from memory. Learn the primitives and the architecture, skip memorizing the APIs. Then try out something like: having the AI build it, build alongside it, then build alone.&lt;/p&gt;

&lt;p&gt;AI didn't replace the learning. It replaced the searching. All those hours of hunting for the right Stack Overflow answer are gone, and I don't miss them. But the part where you sit with something confusing until it stops being confusing? That still has to happen. There's just no tab for it.&lt;/p&gt;

&lt;p&gt;How do you learn a new framework these days? Let me know in the comments if you do it differently. Until next time.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>learning</category>
      <category>beginners</category>
    </item>
    <item>
      <title>7 Kiro Features You're Probably Not Using</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Mon, 27 Jul 2026 15:39:49 +0000</pubDate>
      <link>https://dev.to/aws/7-kiro-features-youre-probably-not-using-2417</link>
      <guid>https://dev.to/aws/7-kiro-features-youre-probably-not-using-2417</guid>
      <description>&lt;p&gt;Did you know that &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; doesn't just have a Spec-Driven Development (SDD) flow, but also a bug fix workflow that helps you resolve one issue at a time? That's one of seven features worth knowing about.&lt;/p&gt;

&lt;p&gt;If you're completely new to Kiro, it's an agentic harness for the CLI, web, IDE, iOS, and more. It helps teams and individuals do their best work while coding. I've been using it since it launched in July last year, and I keep finding features I didn't know were there.&lt;/p&gt;

&lt;p&gt;(Full disclosure: I'm a Developer Advocate at AWS, and Kiro is a part of AWS. I use it every day, and I'll be forthcoming about the parts that are still preview or experimental.)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; Kiro ships fast. I've flagged the version-sensitive bits of these features inline. Check the &lt;a href="https://kiro.dev/docs/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;docs&lt;/a&gt; if something looks different in your build.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Stop approving every single command
&lt;/h2&gt;

&lt;p&gt;After talking to a lot of people about Kiro, one of the main pieces of feedback I get is on approving commands. When Kiro asks permission to run a shell command, the default reaction is to hit yes and move on. Then it asks again for the next &lt;code&gt;git&lt;/code&gt; command. And the next one.&lt;/p&gt;

&lt;p&gt;Press &lt;strong&gt;Tab&lt;/strong&gt; instead in the CLI. This allows you to edit it and put the exact permissions you'd like. For example you can be pickier on the trust tiers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git pull &lt;span class="nt"&gt;--rebase&lt;/span&gt;     &lt;span class="c"&gt;# this exact command&lt;/span&gt;
git pull &lt;span class="k"&gt;*&lt;/span&gt;            &lt;span class="c"&gt;# git pull with any arguments&lt;/span&gt;
git &lt;span class="k"&gt;*&lt;/span&gt;                 &lt;span class="c"&gt;# anything git&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt;                     &lt;span class="c"&gt;# the entire shell tool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whatever you pick persists for the session and gets stored as a regex in your agent's &lt;code&gt;allowedCommands&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There's also &lt;code&gt;/tools trust-all&lt;/code&gt;, which trusts everything. It's the documented replacement for the old &lt;code&gt;/acceptall&lt;/code&gt;, and the &lt;a href="https://kiro.dev/docs/cli/chat/security/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;security docs&lt;/a&gt; are blunt about it: don't use it in production or with sensitive data, and you're responsible for whatever it does. &lt;/p&gt;

&lt;p&gt;One version note: on CLI v3 this moves to a &lt;code&gt;permissions.yaml&lt;/code&gt; file, so the agent JSON advice above is v2. More on v3 in a minute.&lt;/p&gt;

&lt;p&gt;Full details: &lt;a href="https://kiro.dev/docs/cli/chat/permissions/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;tool permissions&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The &lt;code&gt;#&lt;/code&gt; menu is bigger than &lt;code&gt;#file&lt;/code&gt; in the IDE
&lt;/h2&gt;

&lt;p&gt;Type &lt;code&gt;#&lt;/code&gt; in the IDE chat and you get a list of context providers. &lt;code&gt;#file&lt;/code&gt; is the one I use a lot. The full list:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#codebase&lt;/code&gt; · &lt;code&gt;#file&lt;/code&gt; · &lt;code&gt;#folder&lt;/code&gt; · &lt;code&gt;#git diff&lt;/code&gt; · &lt;code&gt;#terminal&lt;/code&gt; · &lt;code&gt;#problems&lt;/code&gt; · &lt;code&gt;#url:&lt;/code&gt; · &lt;code&gt;#code:&lt;/code&gt; · &lt;code&gt;#repository&lt;/code&gt; · &lt;code&gt;#current&lt;/code&gt; · &lt;code&gt;#steering:&lt;/code&gt; · &lt;code&gt;#docs:&lt;/code&gt; · &lt;code&gt;#spec:&lt;/code&gt; · &lt;code&gt;#mcp:&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You can stack them in a single message. &lt;code&gt;#git diff #problems review my changes&lt;/code&gt; is a different request than either one alone.&lt;/p&gt;

&lt;p&gt;The one I've been using more is the &lt;code&gt;#terminal&lt;/code&gt;. It hands Kiro your recent terminal output and command history, so you stop copying and pasting errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;terminal why did this build fail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing I find handy is the "#Currently Open files." However, keep in mind that Kiro pulls in your open files and their dependencies automatically without you asking.&lt;/p&gt;

&lt;p&gt;Full list: &lt;a href="https://kiro.dev/docs/chat/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el#context-providers" rel="noopener noreferrer"&gt;context providers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also you can't drag a file in the IDE's own explorer to the chat window, however you can copy and past the location of the file and past that into the chat window as a workaround. &lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pick a workflow instead of freeform chatting
&lt;/h2&gt;

&lt;p&gt;Kiro gives you structured starting points, and it's tempting to just start typing into the chat box instead. Instead you can choose any of these in the IDE. &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%2Fc5jfyheu0l5gd8ftqp2m.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%2Fc5jfyheu0l5gd8ftqp2m.png" alt="IDE chooser" width="800" height="749"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spec&lt;/strong&gt; for a full feature, with requirements, design, and tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan&lt;/strong&gt; when you want an implementation plan and zero code changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bug Fix&lt;/strong&gt; to investigate, diagnose, and resolve one problem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick Spec&lt;/strong&gt; when you want Kiro to ask a couple of clarifying questions and then generate the requirements, design, and tasks for you&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bug Fix is the one I'd try first. It writes down current behavior, expected behavior, and unchanged behavior. I like how it doesn't change a bunch of files it shouldn't. It basically tells the agent what it is not allowed to touch. &lt;/p&gt;

&lt;p&gt;On a feature spec you also get a choice between requirements-first and tech design-first. If you already know your architecture, pick design-first. There's no reason to answer requirements questions you already have answers to.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Skills are slash commands
&lt;/h2&gt;

&lt;p&gt;Kiro activates a &lt;a href="https://kiro.dev/docs/skills/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;skill&lt;/a&gt; automatically when your prompt matches the skill's description. That works, but I feel like it doesn't always automatically activate.&lt;/p&gt;

&lt;p&gt;You can invoke one directly instead. Skills in &lt;code&gt;.kiro/skills/&lt;/code&gt; or &lt;code&gt;~/.kiro/skills/&lt;/code&gt; show up as slash commands, so a skill named &lt;code&gt;pr-review&lt;/code&gt; becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/pr-review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That loads the entire instruction file, not just the description. Works in the CLI and the IDE.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kiro.dev/docs/powers/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Powers&lt;/a&gt; are a different thing and worth knowing about separately. A power bundles MCP server config with steering and optional hooks, and Kiro loads it only when your task is relevant to it. Powers are keyword-activated, and they're free on every plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. You might be running an old build
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;Help &amp;gt; About&lt;/code&gt; and check your version against &lt;a href="https://kiro.dev/downloads/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;kiro.dev/downloads&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Kiro's own changelog says auto-updates were paused for IDE 1.0.x and tells you to download the latest release directly from the site. So if your version doesn't match what's on the downloads page, that's why, and no amount of restarting will fix it. Reinstalling keeps your settings, extensions, and sign-in state.&lt;/p&gt;

&lt;p&gt;On the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kiro-cli &lt;span class="nt"&gt;--version&lt;/span&gt;
kiro-cli update
kiro-cli doctor      &lt;span class="c"&gt;# install and config problems&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;/changelog&lt;/code&gt; in the CLI inside a chat session shows the release notes inline, which is a nicer way to find out what shipped than scrolling a webpage.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The work doesn't have to run on your laptop
&lt;/h2&gt;

&lt;p&gt;One subscription covers the IDE, the CLI, &lt;a href="https://kiro.dev/docs/web/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro on the web&lt;/a&gt;, ACP-compatible editors, and automation in CI/CD. Same credit pool.&lt;/p&gt;

&lt;p&gt;I really like the web version. You kick off a session, it runs in a cloud sandbox, and it opens the pull request when it's done. Your laptop can be closed. There are also Automations, which are cron-scheduled agent runs that open PRs on their own.&lt;/p&gt;

&lt;p&gt;Web is still in preview, gated to Pro and above, and it needs a connected GitHub account. There's an iOS app too, but it's early access via TestFlight, Pro or higher, and it's a surface for starting and reviewing cloud sessions rather than an editor. No Android yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The experimental stuff is opt-in and worth trying
&lt;/h2&gt;

&lt;p&gt;Three of these are behind a flag or a toggle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI v3.&lt;/strong&gt; Run &lt;code&gt;kiro-cli --v3&lt;/code&gt; and it runs alongside your existing 2.x setup without touching it. You get the spec agent in 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;kiro-cli &lt;span class="nt"&gt;--v3&lt;/span&gt;
/spec new my-feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus capability-based &lt;code&gt;permissions.yaml&lt;/code&gt;, standalone hooks in &lt;code&gt;.kiro/hooks/*.json&lt;/code&gt;, and markdown agent configs. See the &lt;a href="https://kiro.dev/docs/cli/v3/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;v3 docs&lt;/a&gt; for the full list of features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent Focus in the IDE.&lt;/strong&gt; Toggle it from the button in the top right. The layout flips: parallel sessions down the left, chat in the middle, specs and diffs in a panel on the right. It's a different job than editing files. You're directing several agents and reviewing what they produce. Settings, powers, MCP management, the terminal, and direct file editing all still live in the normal view, so you'll toggle back and forth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;/goal&lt;/code&gt;.&lt;/strong&gt; Give it an objective and a definition of done, and it loops implement-then-verify until the criteria are met:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/goal Migrate the auth module to the new SDK. Done when all tests pass and there are no TypeScript errors.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is available in v3 CLI.&lt;/p&gt;

&lt;p&gt;Default is five iterations, &lt;code&gt;--max &amp;lt;n&amp;gt;&lt;/code&gt; raises it, &lt;code&gt;/goal clear&lt;/code&gt; cancels. The docs are right that the definition of done is the part that matters. "All tests pass" works. "Make it better" does not. You can interrupt mid-loop, or nudge it without cancelling using queue steering with &lt;code&gt;Ctrl+S&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few more worth a mention
&lt;/h2&gt;

&lt;p&gt;While I'm listing CLI commands: &lt;code&gt;/rewind&lt;/code&gt; forks the conversation at an earlier turn instead of you fighting a thread that went off the rails, and the original session is preserved. &lt;code&gt;/compact&lt;/code&gt; summarizes history to free up context. &lt;code&gt;/tangent&lt;/code&gt; (&lt;code&gt;Ctrl+T&lt;/code&gt;) lets you run commands on the side (like /btw in Claude). &lt;code&gt;/guide&lt;/code&gt; is a docs-grounded agent for "how do I do X in Kiro" questions, and it can write agents, prompts, and steering files into &lt;code&gt;.kiro/&lt;/code&gt; for you.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;Shift+Enter&lt;/code&gt; for multi-line input has been annoying you, &lt;code&gt;/settings terminal&lt;/code&gt; fixes it and backs up your terminal config first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go when you're stuck
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://kiro.dev/discord/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro Discord&lt;/a&gt; is the main support channel, and there's an active subreddit at r/kiroIDE. Note the name, it's kiroIDE, not r/kiro. Billing lives in your account settings. Bugs and feature requests go to the &lt;a href="https://github.com/kirodotdev/Kiro/issues/new/choose" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;, and &lt;code&gt;/issue&lt;/code&gt; in the CLI opens that workflow for you. If you need to send logs along with a bug report, &lt;code&gt;/logdump&lt;/code&gt; zips them up.&lt;/p&gt;

&lt;p&gt;Which of these did you not know about? Let me know in the comments. Until next time.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
