<?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: babycat</title>
    <description>The latest articles on DEV Community by babycat (@babycat).</description>
    <link>https://dev.to/babycat</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%2F4022684%2Fe78be405-e521-4234-81bb-9fcbc006d5d1.jpg</url>
      <title>DEV Community: babycat</title>
      <link>https://dev.to/babycat</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/babycat"/>
    <language>en</language>
    <item>
      <title>Build an Accessible Agent Task Timeline, Not a Spinner</title>
      <dc:creator>babycat</dc:creator>
      <pubDate>Fri, 10 Jul 2026 10:08:24 +0000</pubDate>
      <link>https://dev.to/babycat/build-an-accessible-agent-task-timeline-not-a-spinner-32al</link>
      <guid>https://dev.to/babycat/build-an-accessible-agent-task-timeline-not-a-spinner-32al</guid>
      <description>&lt;p&gt;A spinner is honest for about two seconds. After that, it stops answering the questions a person actually has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is happening now?&lt;/li&gt;
&lt;li&gt;Did anything finish?&lt;/li&gt;
&lt;li&gt;Is the product waiting for me?&lt;/li&gt;
&lt;li&gt;Can I leave and come back?&lt;/li&gt;
&lt;li&gt;What will cancellation preserve?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Long-running agent tasks need two interface layers: a concise current-state summary and a durable event history. The summary helps someone decide what to do now. The history helps them understand what already happened without relying on animation or memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a state-to-message contract
&lt;/h2&gt;

&lt;p&gt;Do not let each backend event become raw UI copy.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Summary&lt;/th&gt;
&lt;th&gt;Primary action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;queued&lt;/td&gt;
&lt;td&gt;Waiting for an environment&lt;/td&gt;
&lt;td&gt;Cancel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;running&lt;/td&gt;
&lt;td&gt;Name the current verifiable step&lt;/td&gt;
&lt;td&gt;View details or cancel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;needs input&lt;/td&gt;
&lt;td&gt;State the decision and consequence&lt;/td&gt;
&lt;td&gt;Answer question&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;succeeded&lt;/td&gt;
&lt;td&gt;Name the artifacts ready for review&lt;/td&gt;
&lt;td&gt;Review patch/results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;failed&lt;/td&gt;
&lt;td&gt;State what is preserved and what can retry&lt;/td&gt;
&lt;td&gt;Retry or inspect logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;canceled&lt;/td&gt;
&lt;td&gt;State whether partial changes remain&lt;/td&gt;
&lt;td&gt;Review or discard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;“Working…” is not a useful running message. “Running unit tests: 42 of 68 complete” is useful because it has an object and progress that the system can actually verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  A single-file implementation
&lt;/h2&gt;

&lt;p&gt;Save the following as &lt;code&gt;task-timeline.html&lt;/code&gt; and open it in a browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Accessible task timeline&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.5&lt;/span&gt; &lt;span class="n"&gt;system-ui&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;44rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3rem&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nc"&gt;.summary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#555&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;.75rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;margin-block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;.75rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;time&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#555&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;.6rem&lt;/span&gt; &lt;span class="m"&gt;.9rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;.5rem&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;/style&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;h1&amp;gt;&lt;/span&gt;Refactor authentication&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"summary"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"summary"&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"status"&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-atomic=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Task queued. No action needed.
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ol&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"events"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Task event history"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"advance"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Simulate next update&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;id=&lt;/span&gt;&lt;span class="s"&gt;"cancel"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Cancel task&lt;span class="nt"&gt;&amp;lt;/button&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;script&amp;gt;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updates&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;running&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="s2"&gt;Environment ready; analyzing the repository.&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="s2"&gt;needs input&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="s2"&gt;Choose the target branch before changes continue.&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="s2"&gt;running&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="s2"&gt;Branch selected; running tests.&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="s2"&gt;succeeded&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="s2"&gt;Task complete; review the patch and test results.&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;let&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#summary&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;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#events&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;advance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#advance&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;cancel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#cancel&lt;/span&gt;&lt;span class="dl"&gt;"&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;addEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;state&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="nx"&gt;message&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;li&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;now&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;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;strong&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;br&amp;gt;&amp;lt;span&amp;gt;&amp;lt;/span&amp;gt; &amp;lt;time&amp;gt;&amp;lt;/time&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;strong&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;span&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;time&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLocaleTimeString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;time&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;dateTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&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="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;advance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="o"&gt;=&amp;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;index&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;addEvent&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="o"&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;index&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;advance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&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;cancel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&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="nx"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;addEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canceled&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="s2"&gt;Task canceled; partial changes require review.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;advance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&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;cancel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo deliberately keeps focus on the button the user activated. Moving focus to every new event would make a live interface difficult for keyboard and screen-reader users. The &lt;code&gt;role="status"&lt;/code&gt; summary announces a short, polite update; the ordered list remains available for deliberate review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility decisions hiding in the code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  One live region, not a live log
&lt;/h3&gt;

&lt;p&gt;Marking the whole event stream as live can produce a wall of announcements. Announce the newest decision-relevant summary once. Let users navigate history normally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Text carries the state
&lt;/h3&gt;

&lt;p&gt;Color can reinforce success or failure, but it cannot be the only signal. The demo uses visible state words and complete messages. Add icons only with accessible names or hide decorative icons from assistive technology.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cancellation has a result
&lt;/h3&gt;

&lt;p&gt;The button says “Cancel task,” but the resulting message says what remains: partial changes require review. In a real product, confirm whether cancellation stops future work, discards a workspace, preserves a patch, or merely requests termination.&lt;/p&gt;

&lt;h3&gt;
  
  
  Time is machine-readable
&lt;/h3&gt;

&lt;p&gt;The visible time is local and readable. The &lt;code&gt;dateTime&lt;/code&gt; value preserves an ISO timestamp for assistive technology and future formatting.&lt;/p&gt;

&lt;h2&gt;
  
  
  A documented product example
&lt;/h2&gt;

&lt;p&gt;The public MonkeyCode repository includes this screenshot of its AI task workspace:&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%2F19e5kvm9p0x03xmd1ccp.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%2F19e5kvm9p0x03xmd1ccp.png" alt="MonkeyCode AI task workspace showing a development task interface" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: &lt;a href="https://github.com/chaitin/MonkeyCode/blob/1ac778fdba1da1b353f7f5672d2e4550801cf46d/frontend/public/monkeycode-1.png" rel="noopener noreferrer"&gt;MonkeyCode repository at the reviewed commit&lt;/a&gt;, licensed with the project under AGPL-3.0.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The screenshot is useful context for a frontend review, but it is not an accessibility test. A serious review still needs keyboard navigation, focus behavior, semantic inspection, zoom and reflow, contrast, screen-reader announcements, reduced motion, error recovery, and tests with people who use assistive technology.&lt;/p&gt;

&lt;p&gt;For a task workspace such as &lt;a href="https://github.com/chaitin/MonkeyCode" rel="noopener noreferrer"&gt;MonkeyCode&lt;/a&gt;, I would verify these flows first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;returning to a task after the page reloads;&lt;/li&gt;
&lt;li&gt;receiving a question while focus is elsewhere;&lt;/li&gt;
&lt;li&gt;canceling while a command is running;&lt;/li&gt;
&lt;li&gt;distinguishing “cancel requested” from “canceled”;&lt;/li&gt;
&lt;li&gt;reviewing partial files after a failure;&lt;/li&gt;
&lt;li&gt;opening a preview without losing task context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article demonstrates an independent UI pattern. It does not claim that MonkeyCode currently implements the exact markup or behavior above.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disclosure: I contribute to the MonkeyCode project. Product observations are limited to the linked public screenshot and repository documentation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Frontend developers can join the &lt;a href="https://discord.gg/2pPmuyr4pP" rel="noopener noreferrer"&gt;MonkeyCode Discord&lt;/a&gt; to discuss the task interface with the team. Readers interested in trying the hosted workflow can also ask about current free model-credit availability, eligibility, and usage limits.&lt;/p&gt;

&lt;p&gt;The core pattern is simple: keep the current state short, keep the history durable, announce only what needs attention, and make every stop state explain what the user can recover next.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>a11y</category>
      <category>javascript</category>
      <category>ai</category>
    </item>
    <item>
      <title>An Accessible Streaming AI Interface Has More Than a Loading Spinner</title>
      <dc:creator>babycat</dc:creator>
      <pubDate>Fri, 10 Jul 2026 07:12:05 +0000</pubDate>
      <link>https://dev.to/babycat/llms-101-understanding-the-technology-behind-chatgpt-claude-and-gemini-5g46</link>
      <guid>https://dev.to/babycat/llms-101-understanding-the-technology-behind-chatgpt-claude-and-gemini-5g46</guid>
      <description>&lt;p&gt;Streaming text makes an AI feature feel responsive, but it also creates an awkward interface: the result is visible before it is complete, changes many times per second, and may fail after the user has already started reading.&lt;/p&gt;

&lt;p&gt;An accessible implementation needs more than a spinner and an &lt;code&gt;aria-live&lt;/code&gt; attribute. It needs an understandable state model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every state a user-facing meaning
&lt;/h2&gt;

&lt;p&gt;Start with explicit states:&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;type&lt;/span&gt; &lt;span class="nx"&gt;GenerationState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&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="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="s2"&gt;submitting&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="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="s2"&gt;streaming&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;partialText&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;|&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="s2"&gt;complete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;text&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;|&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="s2"&gt;cancelling&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;partialText&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;|&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="s2"&gt;cancelled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;partialText&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;|&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="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;partialText&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="nl"&gt;retryable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents contradictory UI such as an enabled Generate button beside a result that is still arriving. It also forces product decisions: Does partial text remain after cancellation? Can a failed request be retried? Is a streamed draft safe to copy before validation?&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate visual output from announcements
&lt;/h2&gt;

&lt;p&gt;Do not place the rapidly changing token stream inside an assertive live region. A screen reader may attempt to announce every update, producing noise that is impossible to follow.&lt;/p&gt;

&lt;p&gt;Use two regions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"generation-status"&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"status"&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="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;aria-labelledby=&lt;/span&gt;&lt;span class="s"&gt;"result-heading"&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;h2&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"result-heading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Generated draft&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"result-output"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the visible output as text arrives. Update the status region only at meaningful transitions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Generating draft.”&lt;/li&gt;
&lt;li&gt;“Draft complete.”&lt;/li&gt;
&lt;li&gt;“Generation cancelled. Partial text is available.”&lt;/li&gt;
&lt;li&gt;“Could not generate the draft. Your input was preserved.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set &lt;code&gt;aria-busy="false"&lt;/code&gt; when the operation reaches a terminal state. The WAI-ARIA specification defines the &lt;a href="https://www.w3.org/TR/wai-aria-1.2/#status" rel="noopener noreferrer"&gt;&lt;code&gt;status&lt;/code&gt; role&lt;/a&gt; as a live region with polite announcements; reserve interruptive alerts for information that genuinely needs immediate attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep focus under the user’s control
&lt;/h2&gt;

&lt;p&gt;Starting generation should not move focus to the output. The user may want to review or edit the input while waiting. When generation completes, announce completion and provide a clear way to reach the result.&lt;/p&gt;

&lt;p&gt;Move focus only when it supports recovery, for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;to a validation error after submission;&lt;/li&gt;
&lt;li&gt;to an error summary after a failed operation when the current control no longer explains what happened;&lt;/li&gt;
&lt;li&gt;to a confirmation dialog for a consequential next action.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not focus every new paragraph or token. Dynamic content can update without taking the keyboard away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement real cancellation
&lt;/h2&gt;

&lt;p&gt;The Cancel button should be a button, stay keyboard reachable, and describe its operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&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="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"cancel-generation"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Cancel generation
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the browser, connect it to an &lt;code&gt;AbortController&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&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;AbortController&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="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="s2"&gt;/api/drafts&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="s2"&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;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="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&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="s2"&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="s2"&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="nx"&gt;cancelButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&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;AbortController&lt;/code&gt; stops the browser-side fetch, as documented by MDN’s &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController" rel="noopener noreferrer"&gt;&lt;code&gt;AbortController&lt;/code&gt;&lt;/a&gt;. If the backend starts a separate job, send a cancellation request there too; disconnecting the browser does not necessarily stop server work.&lt;/p&gt;

&lt;p&gt;While cancellation is pending, change the button label to “Cancelling…” and disable repeated activation. After cancellation, state clearly whether partial output was kept and whether it has been validated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve the input and the partial result
&lt;/h2&gt;

&lt;p&gt;A generic “Something went wrong” message creates unnecessary work. A useful error state answers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What happened?&lt;/li&gt;
&lt;li&gt;Was the user’s input saved?&lt;/li&gt;
&lt;li&gt;Is partial output available?&lt;/li&gt;
&lt;li&gt;Can the operation be retried safely?&lt;/li&gt;
&lt;li&gt;Is there another path to complete the task?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The draft stopped before completion. Your source text and the partial draft
are still here. Try again, or continue editing the partial draft manually.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When retrying, avoid deleting the partial result until the replacement has started successfully. If a retry might create a duplicate action, the server needs an idempotency key; the UI alone cannot guarantee safety.&lt;/p&gt;

&lt;h2&gt;
  
  
  Label uncertainty at the right level
&lt;/h2&gt;

&lt;p&gt;A permanent banner saying “AI may be wrong” quickly becomes wallpaper. Put guidance next to the decision it affects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mark streamed output as “Draft—still generating”;&lt;/li&gt;
&lt;li&gt;show source links beside claims when the feature has retrieval evidence;&lt;/li&gt;
&lt;li&gt;use an editable field for content the user is expected to review;&lt;/li&gt;
&lt;li&gt;require explicit confirmation before publishing, sending, or changing data;&lt;/li&gt;
&lt;li&gt;distinguish “no source found” from “the request failed.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interface should make correction normal, not frame it as an exceptional failure by the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test more than the happy path
&lt;/h2&gt;

&lt;p&gt;Run the component with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keyboard-only navigation;&lt;/li&gt;
&lt;li&gt;a screen reader at normal and very fast stream rates;&lt;/li&gt;
&lt;li&gt;reduced motion enabled;&lt;/li&gt;
&lt;li&gt;200% and 400% zoom;&lt;/li&gt;
&lt;li&gt;a slow response, an empty response, malformed data, and a mid-stream error;&lt;/li&gt;
&lt;li&gt;cancellation before the first token and after substantial text;&lt;/li&gt;
&lt;li&gt;a retry after reconnecting to the network.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also test the DOM announcement sequence. A snapshot can confirm labels and roles, but listening to the interaction is what reveals repeated or interrupted announcements.&lt;/p&gt;

&lt;p&gt;Streaming is a presentation technique, not a complete interaction model. When status, output, focus, cancellation, and recovery are designed separately, the interface can feel fast without becoming confusing or exhausting.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>frontend</category>
      <category>ai</category>
      <category>ux</category>
    </item>
    <item>
      <title>Ollama Just Raised $65M — Here's Why Open-Source AI Tools Are Winning</title>
      <dc:creator>babycat</dc:creator>
      <pubDate>Fri, 10 Jul 2026 04:36:17 +0000</pubDate>
      <link>https://dev.to/babycat/ollama-just-raised-65m-heres-why-open-source-ai-tools-are-winning-1lki</link>
      <guid>https://dev.to/babycat/ollama-just-raised-65m-heres-why-open-source-ai-tools-are-winning-1lki</guid>
      <description>&lt;p&gt;Ollama just raised $65 million. The open-source tool for running AI models locally now has nearly 9 million users. That's massive for a developer tool that most non-technical people have never heard of.&lt;/p&gt;

&lt;p&gt;But if you're a developer who cares about privacy, cost, and control, you probably already know why Ollama is winning.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Ollama actually does
&lt;/h2&gt;

&lt;p&gt;In case you haven't used it: Ollama lets you run large language models on your own computer. No API calls, no cloud, no monthly bills. Download a model, run it locally, use it for whatever you want.&lt;/p&gt;

&lt;p&gt;The experience is dead simple:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;That's it. You now have a local AI assistant that runs entirely on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why developers love it
&lt;/h2&gt;

&lt;p&gt;Three things drive Ollama's adoption:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Privacy&lt;/strong&gt;&lt;br&gt;
Your code never leaves your machine. For companies with strict data policies (healthcare, finance, government), this isn't a nice-to-have — it's a requirement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Cost&lt;/strong&gt;&lt;br&gt;
API calls add up fast. If you're using GPT-4 for development, you might spend $50-200/month. Ollama is free. The models are free. Your electricity bill is the only cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Control&lt;/strong&gt;&lt;br&gt;
No rate limits. No downtime. No "we're experiencing high traffic" messages. Your model runs when you want it, how you want it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The numbers tell the story
&lt;/h2&gt;

&lt;p&gt;9 million users is huge for a developer tool. For context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker took years to reach similar adoption&lt;/li&gt;
&lt;li&gt;VS Code had about 10M users in its first couple years&lt;/li&gt;
&lt;li&gt;Ollama hit 9M in under 2 years&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The growth is driven by one thing: local AI models got good enough.&lt;/p&gt;

&lt;p&gt;A year ago, running Llama locally meant poor quality output and complex setup. Now, models like Llama 3, Mistral, and Phi-3 are genuinely useful for coding, writing, and analysis. And Ollama makes them accessible with one command.&lt;/p&gt;
&lt;h2&gt;
  
  
  What $65M buys you
&lt;/h2&gt;

&lt;p&gt;With this funding, Ollama can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hire more engineers&lt;/strong&gt;: The core team is small; they need more hands&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimize performance&lt;/strong&gt;: Running models locally still requires decent hardware&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expand model support&lt;/strong&gt;: More models, better compatibility&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build enterprise features&lt;/strong&gt;: SSO, team management, compliance tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The enterprise angle is where the money is. Individual developers are great for adoption, but enterprises pay the bills.&lt;/p&gt;
&lt;h2&gt;
  
  
  The local AI movement
&lt;/h2&gt;

&lt;p&gt;Ollama isn't alone. The entire local AI ecosystem is growing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LM Studio&lt;/strong&gt;: GUI for running local models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPT4All&lt;/strong&gt;: Another local model runner&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;llama.cpp&lt;/strong&gt;: The engine behind many local AI tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hugging Face&lt;/strong&gt;: Hub for downloading models&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This movement is driven by distrust of cloud AI providers and the desire for control. After seeing companies change pricing, restrict usage, and train on user data, developers want alternatives.&lt;/p&gt;
&lt;h2&gt;
  
  
  My local setup
&lt;/h2&gt;

&lt;p&gt;I run Ollama with a few models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Llama 3 8B&lt;/strong&gt;: For general coding questions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mistral 7B&lt;/strong&gt;: For faster responses when quality matters less&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CodeLlama&lt;/strong&gt;: For code-specific tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combined with tools like &lt;a href="https://github.com/chaitin/MonkeyCode" rel="noopener noreferrer"&gt;MonkeyCode&lt;/a&gt; for code review, I have a mostly-local development workflow. The only cloud AI I use regularly is Claude for complex reasoning tasks that local models can't handle yet.&lt;/p&gt;
&lt;h2&gt;
  
  
  The hardware question
&lt;/h2&gt;

&lt;p&gt;The main barrier to local AI is hardware. Running a 7B parameter model requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;8GB+ RAM (16GB recommended)&lt;/li&gt;
&lt;li&gt;A decent GPU helps but isn't required&lt;/li&gt;
&lt;li&gt;SSD storage for model files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most developer laptops from the last 2-3 years can handle it. Desktop users with dedicated GPUs get the best experience.&lt;/p&gt;
&lt;h2&gt;
  
  
  What this means for the market
&lt;/h2&gt;

&lt;p&gt;Ollama's success proves there's a market for local AI tools. Expect more investment in this space:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hardware companies&lt;/strong&gt; will optimize for local AI (Apple already does with Neural Engine)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model makers&lt;/strong&gt; will prioritize smaller, efficient models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud providers&lt;/strong&gt; will offer hybrid options (cloud + local)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise tools&lt;/strong&gt; will add local-first AI features&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The $65M isn't just for Ollama. It's a bet that the future of AI isn't all-cloud.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you haven't tried Ollama, now's a good time:&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="c"&gt;# Install&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://ollama.ai/install.sh | sh

&lt;span class="c"&gt;# Run a model&lt;/span&gt;
ollama run llama3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The barrier to entry is basically zero. And with 9 million users, the community support is solid.&lt;/p&gt;

&lt;p&gt;What's your experience with local AI tools? Are they good enough for your daily workflow yet?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>ollama</category>
      <category>funding</category>
    </item>
    <item>
      <title>Meta's Muse Image Generator Uses Your Photos — And the Opt-Out Is Designed to Fail</title>
      <dc:creator>babycat</dc:creator>
      <pubDate>Thu, 09 Jul 2026 11:44:37 +0000</pubDate>
      <link>https://dev.to/babycat/metas-muse-image-generator-uses-your-photos-and-the-opt-out-is-designed-to-fail-h0p</link>
      <guid>https://dev.to/babycat/metas-muse-image-generator-uses-your-photos-and-the-opt-out-is-designed-to-fail-h0p</guid>
      <description>&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%2Fimages.unsplash.com%2Fphoto-1633356122544-f134324a6cee%3Fw%3D800" 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%2Fimages.unsplash.com%2Fphoto-1633356122544-f134324a6cee%3Fw%3D800" alt="AI Privacy" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Meta just launched &lt;strong&gt;Muse Image&lt;/strong&gt;, their new AI image generator. And within hours, users discovered something disturbing: &lt;strong&gt;it's trained on your photos.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The backlash was immediate. But Meta's response reveals a deeper problem with how big tech companies approach AI training data — and why their "opt-out" mechanisms are designed to be as inconvenient as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Happened
&lt;/h2&gt;

&lt;p&gt;Meta rolled out Muse Image as part of their AI ecosystem. The tool generates images from text prompts, similar to DALL-E or Midjourney. But users quickly noticed that Muse could generate images that looked suspiciously similar to their private photos.&lt;/p&gt;

&lt;p&gt;The issue? Meta's AI training data includes photos from Facebook and Instagram — &lt;strong&gt;your photos&lt;/strong&gt; — unless you specifically opt out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Opt-Out Illusion
&lt;/h2&gt;

&lt;p&gt;Here's where it gets infuriating. Meta technically allows you to opt out of having your data used for AI training. But they've designed the process to be as painful as possible:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You have to find the form&lt;/strong&gt; — it's buried deep in Meta's privacy settings, not linked from any obvious location&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need to provide specific URLs&lt;/strong&gt; — you can't just say "don't use my photos." You need to provide exact URLs of the content you want excluded&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's per-item&lt;/strong&gt; — you have to submit separate requests for each photo, album, or piece of content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No bulk opt-out&lt;/strong&gt; — there's no "exclude all my content" button&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you have 1,000 photos on Instagram, you need to submit 1,000 individual requests. &lt;strong&gt;That's not a choice. That's a punishment for caring about privacy.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters for Developers
&lt;/h2&gt;

&lt;p&gt;This isn't just a Meta problem. It's a pattern across the entire AI industry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI&lt;/strong&gt; trained on the entire internet without explicit consent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stability Diffusion&lt;/strong&gt; used LAION-5B, which contains billions of scraped images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google&lt;/strong&gt; uses your Gmail, Docs, and Search history for AI training&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The default is always &lt;strong&gt;opt-in by default, opt-out by design&lt;/strong&gt;. And the opt-out is always just inconvenient enough that most people won't bother.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Solution: AI-Native Privacy Tools
&lt;/h2&gt;

&lt;p&gt;The problem isn't that AI uses data. The problem is that users have no control over how their data is used.&lt;/p&gt;

&lt;p&gt;This is where tools like &lt;a href="https://github.com/chaitin/MonkeyCode/" rel="noopener noreferrer"&gt;MonkeyCode&lt;/a&gt; take a fundamentally different approach. Instead of building AI that hoovers up everything and asks questions later, MonkeyCode is designed with privacy as a core feature:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On-premise deployment&lt;/strong&gt; — your code and data stay on your infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparent processing&lt;/strong&gt; — you know exactly what data is being used&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User control&lt;/strong&gt; — you decide what gets processed, not an algorithm&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lesson from Meta's Muse debacle is clear: &lt;strong&gt;AI tools need to be built with privacy from day one, not bolted on after the backlash.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Can Do Right Now
&lt;/h2&gt;

&lt;p&gt;If you're concerned about your data being used for AI training:&lt;/p&gt;

&lt;h3&gt;
  
  
  For Meta/Facebook/Instagram:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://www.facebook.com/privacy/center" rel="noopener noreferrer"&gt;Meta's privacy center&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Look for "AI data usage" settings&lt;/li&gt;
&lt;li&gt;Submit opt-out requests (prepare to spend hours if you have lots of content)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  For a better approach:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use tools that are privacy-first by design&lt;/li&gt;
&lt;li&gt;Choose AI assistants that run locally or on your own infrastructure&lt;/li&gt;
&lt;li&gt;Support open-source AI projects that respect user consent&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;Meta's Muse controversy isn't about one image generator. It's about the fundamental question of who owns your digital identity in the age of AI.&lt;/p&gt;

&lt;p&gt;Right now, the answer is: &lt;strong&gt;not you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your photos, your writing, your code, your conversations — they're all training data. And the companies using them have made it deliberately difficult to say no.&lt;/p&gt;

&lt;p&gt;It's time to demand better. Not just better opt-out forms, but better defaults. AI should be opt-in, not opt-out. Privacy should be the starting point, not an afterthought.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Have you checked your Meta privacy settings recently?&lt;/strong&gt; What's your take on AI training data consent? Let's discuss below. 👇&lt;/p&gt;

</description>
      <category>ai</category>
      <category>privacy</category>
      <category>meta</category>
      <category>ethics</category>
    </item>
  </channel>
</rss>
