<?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: Khoa Nguyen</title>
    <description>The latest articles on DEV Community by Khoa Nguyen (@khoavannguyen).</description>
    <link>https://dev.to/khoavannguyen</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F47956%2Fc59405a7-4d82-4980-8407-acdedeb20d19.jpeg</url>
      <title>DEV Community: Khoa Nguyen</title>
      <link>https://dev.to/khoavannguyen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/khoavannguyen"/>
    <language>en</language>
    <item>
      <title>I added 18 silly animations to my AI terminal because staring at 'Kneading...' was getting depressing</title>
      <dc:creator>Khoa Nguyen</dc:creator>
      <pubDate>Tue, 07 Apr 2026 15:25:03 +0000</pubDate>
      <link>https://dev.to/khoavannguyen/i-added-18-silly-animations-to-my-ai-terminal-because-staring-at-kneading-was-getting-2mbd</link>
      <guid>https://dev.to/khoavannguyen/i-added-18-silly-animations-to-my-ai-terminal-because-staring-at-kneading-was-getting-2mbd</guid>
      <description>&lt;p&gt;I shipped a feature this week that has zero productivity value, takes about 250ms to set up, and made me laugh out loud the first time I saw it work. It's a setting in &lt;a href="https://1devtool.com" rel="noopener noreferrer"&gt;1DevTool&lt;/a&gt; that puts a tiny cartoon manager next to your Claude Code terminal who shakes and screams "IS IT DONE YET?!" while the agent is generating.&lt;/p&gt;

&lt;p&gt;There are 17 other animations like this. A whip that cracks at the running text. A rubber duck that blinks at you. A cat that walks across the spinner. A firefighter who sprays foam at flames burning the "Kneading..." line. Donald Trump cycling through 20 catchphrases including "MAKE CODING GREAT AGAIN" and "NOBODY WRITES CODE BETTER THAN ME".&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.amazonaws.com%2Fuploads%2Farticles%2Frryuxtsw17xrxc1y5a9t.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frryuxtsw17xrxc1y5a9t.jpg" alt=" " width="800" height="691"&gt;&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2Fyxxwfxb2thoispk7eezr.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyxxwfxb2thoispk7eezr.jpg" alt=" " width="800" height="490"&gt;&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2Fyyhftt9u46ym9pg5v3fo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyyhftt9u46ym9pg5v3fo.jpg" alt=" " width="800" height="614"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I want to write about why I built this, how the interesting bit actually works (it's harder than it looks), and what the architecture ended up being. There's a real technical problem hiding inside what looks like a joke feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does this exist
&lt;/h2&gt;

&lt;p&gt;I use Claude Code, Codex, Gemini, and Amp every day inside &lt;a href="https://1devtool.com" rel="noopener noreferrer"&gt;1DevTool&lt;/a&gt;, which is the dev workbench I'm building. A normal session has me staring at "Kneading… (3s)" or "Pondering… (12s)" for a long time. Sometimes a really long time.&lt;/p&gt;

&lt;p&gt;A user joked in our Discord: "I wish I could crack a whip on Claude when it's slow." I laughed, then thought about it for ten more seconds, and realized: yeah, I could just &lt;em&gt;do that&lt;/em&gt;. As a setting. With a tiny SVG whip. It would be funny every single time.&lt;/p&gt;

&lt;p&gt;So I built it. Then I built 17 more. The full list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kind ✨&lt;/strong&gt; — Magic Wand sprinkling sparkles, Feed Coffee delivery with steam, a patient Rubber Duck that bobs and blinks, a Handshake, a head-petting hand with floating hearts, a Good Dog wagging its tail&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Motivational 💪&lt;/strong&gt; — Whip Crack, Hammer, Cooling Fan, Finger Poke, Electric Shock, FASTER Rain (Matrix-style), Manager Shouting, Trump Mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chaos 🌪️&lt;/strong&gt; — Bug Swarm crawling everywhere, Firefighting Mode, a Cat walking across the keyboard, an Agent Meeting where seven colored speech bubbles pop in arguing "ACTUALLY...", "NO BUT WAIT", "I DISAGREE", "TECHNICALLY!"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You pick one in Settings → Terminal → Fun Agent Animation. The animation appears the moment your AI agent starts generating and vanishes the instant it goes idle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting problem: where exactly is the AI thinking?
&lt;/h2&gt;

&lt;p&gt;Here's the thing that took the feature from "amusing toy" to "actually delightful." The first version put the whip in a fixed corner of the terminal pane. The animation played fine but it felt disconnected — like a sticker someone slapped onto the screen. The whip was over here. The "Kneading..." text was over there.&lt;/p&gt;

&lt;p&gt;The user pushed back with a screenshot. They drew a red box around the actual running status line and an arrow saying "the animation should point at the running text of AI like the section in image."&lt;/p&gt;

&lt;p&gt;That's the real problem. The animation needs to follow where the agent is currently rendering its spinner — and that location is dynamic. It moves when you scroll. It moves when the terminal resizes. It moves when the AI clears the line and redraws it. It moves between agents (Claude Code's TUI and Codex's TUI render their status lines in different places).&lt;/p&gt;

&lt;p&gt;So how do you find a moving text target inside a terminal buffer from React?&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the xterm.js buffer
&lt;/h2&gt;

&lt;p&gt;1DevTool uses &lt;a href="https://xtermjs.org/" rel="noopener noreferrer"&gt;xterm.js&lt;/a&gt; for terminal rendering. xterm.js exposes its buffer through an API, which means I can iterate through the visible rows and read the text on each one. The trick is knowing what to look for.&lt;/p&gt;

&lt;p&gt;I made a list of three signals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Spinner glyphs&lt;/strong&gt; — Most TUIs use one of the braille spinner characters like &lt;code&gt;⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏&lt;/code&gt;. Claude Code uses these. So does Codex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code's rotating verbs&lt;/strong&gt; — Claude has a charming detail where it cycles through verbs like "Kneading", "Pondering", "Boondoggling", "Brewing", "Conjuring", "Spelunking", "Noodling". I built a regex with all of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The seconds timer&lt;/strong&gt; — Almost every running indicator includes a parenthesized timer like &lt;code&gt;(3s)&lt;/code&gt; or &lt;code&gt;(47s)&lt;/code&gt;. That's a strong signal that's agent-agnostic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any line in the visible viewport matches any of those, that's the running line. Here's the actual hook:&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;// useAgentRunningLine.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SPINNER_CHARS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏◐◓◑◒✢✳✦✧⭒⭑●·&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RUNNING_VERBS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b(?:&lt;/span&gt;&lt;span class="sr"&gt;Kneading|Thinking|Contemplating|Pondering|Ruminating|Boondoggling|Dithering|Processing|Running|Wrangling|Simmering|Cogitating|Generating|Working|Musing|Brewing|Puzzling|Noodling|Incubating|Cooking|Churning|Fermenting|Mulling|Spelunking|Herding|Forging|Conjuring|Hatching|Sorting|Calculating|Weaving|Scheming|Chewing|Gnawing|Doodling|Wondering&lt;/span&gt;&lt;span class="se"&gt;)\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SECONDS_TIMER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\(\d&lt;/span&gt;&lt;span class="sr"&gt;+s&lt;/span&gt;&lt;span class="se"&gt;\)&lt;/span&gt;&lt;span class="sr"&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;useAgentRunningLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;terminalId&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="nx"&gt;active&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;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;anchorY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setAnchorY&lt;/span&gt;&lt;span class="p"&gt;]&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="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setAnchorY&lt;/span&gt;&lt;span class="p"&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;return&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;scan&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useTerminalStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;terminalId&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;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;xterm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;xterm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&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;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;containerHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;height&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;containerHeight&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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;return&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;xterm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;xterm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;viewportStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;viewportY&lt;/span&gt;

      &lt;span class="c1"&gt;// Scan bottom-up — the status line usually sits near the prompt&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;foundRow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
      &lt;span class="k"&gt;for &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;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;y&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;viewportStart&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;y&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;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translateToString&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;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;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&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;SPINNER_CHARS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;SECONDS_TIMER&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;RUNNING_VERBS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;foundRow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;
          &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// Fallback: status line typically sits a couple rows above the cursor&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;foundRow&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;foundRow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cursorY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// Convert row index to pixel Y&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cellHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;containerHeight&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;
      &lt;span class="nf"&gt;setAnchorY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;foundRow&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;cellHeight&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;cellHeight&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;scan&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;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;250&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&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;terminalId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;active&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;anchorY&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth pointing out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bottom-up scan.&lt;/strong&gt; The status line is almost always near the input prompt at the bottom. Scanning bottom-first means we usually find it within 2-3 line reads instead of iterating through the whole viewport.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polling at 250ms.&lt;/strong&gt; I tried &lt;code&gt;requestAnimationFrame&lt;/code&gt; first and it was wasteful. The status line moves at most a few times per second when the agent is generating. 250ms feels instant and costs basically nothing — reading 30 lines of text is cheap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cursor fallback.&lt;/strong&gt; If the regex misses (edge case: the agent is between status updates), the cursor's row is a decent approximation. Subtract two because the input prompt sits below the status line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No row → pixel coupling with xterm internals.&lt;/strong&gt; I don't reach into &lt;code&gt;_renderService&lt;/code&gt; or any private APIs. I just compute &lt;code&gt;containerHeight / rows&lt;/code&gt; for the cell height. It's a hair less accurate than xterm's internal calculation but it survives version upgrades.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The hook returns &lt;code&gt;null&lt;/code&gt; when inactive.&lt;/strong&gt; That's how the overlay knows to render nothing — no state, no flicker, no placeholder.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That value gets passed to the overlay, and every animation positions itself relative to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: one file per animation, plus a registry
&lt;/h2&gt;

&lt;p&gt;The first version of this feature was a single 1100-line &lt;code&gt;FunAnimationOverlay.tsx&lt;/code&gt; file with &lt;code&gt;{type === 'whip' &amp;amp;&amp;amp; (...)}&lt;/code&gt; blocks for every animation. By the time I had eight animations it was already painful to navigate. By eighteen it was untenable.&lt;/p&gt;

&lt;p&gt;I split it. Now there's a directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/renderer/components/terminal/
├── FunAnimationOverlay.tsx       # 40-line dispatcher
├── useAgentRunningLine.ts        # the hook above
└── animations/
    ├── types.ts                  # shared AnimationProps interface
    ├── keyframes.ts              # all @keyframes in one CSS string
    ├── registry.ts               # FunAnimationType → Component map
    ├── whip.tsx
    ├── hammer.tsx
    ├── trump.tsx
    └── ... (15 more)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dispatcher is now 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="c1"&gt;// FunAnimationOverlay.tsx&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;FunAnimationOverlay&lt;/span&gt;&lt;span class="p"&gt;({&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;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;anchorY&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="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;active&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&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="kc"&gt;null&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;anchorY&lt;/span&gt; &lt;span class="o"&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;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;   &lt;span class="c1"&gt;// wait for first scan&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Animation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FUN_ANIMATION_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kd"&gt;type&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;Animation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;absolute inset-0 z-20 pointer-events-none overflow-hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;FUN_ANIMATION_KEYFRAMES&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/style&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Animation&lt;/span&gt; &lt;span class="nx"&gt;anchorY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;anchorY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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 registry is just a map:&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;// animations/registry.ts&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;FUN_ANIMATION_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
  &lt;span class="nb"&gt;Exclude&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FunAnimationType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;ComponentType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AnimationProps&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;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;whip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WhipAnimation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;hammer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HammerAnimation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;trump&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TrumpAnimation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ... 15 more&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding a new animation is now four steps: write &lt;code&gt;animations/foo.tsx&lt;/code&gt;, add its keyframe to &lt;code&gt;keyframes.ts&lt;/code&gt;, register it in &lt;code&gt;registry.ts&lt;/code&gt;, add the variant to the &lt;code&gt;FunAnimationType&lt;/code&gt; union and the settings picker. No more hunting through one giant file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Each animation positions itself
&lt;/h2&gt;

&lt;p&gt;Every animation receives the &lt;code&gt;anchorY&lt;/code&gt; pixel value and decides what part of itself should land on that line. For most, the visual center sits on the line:&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;// animations/manager.tsx&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;ManagerAnimation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;anchorY&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;AnimationProps&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;
      &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flex items-start gap-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
        &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;absolute&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;anchorY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;translateY(-50%)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// center on the anchor line&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="cm"&gt;/* speech bubble + shaking manager avatar SVG */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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;But some animations need their &lt;em&gt;strike point&lt;/em&gt; on the line, not their center:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Whip&lt;/strong&gt; — The tail tip sits at the bottom of the SVG, so &lt;code&gt;translateY(-85%)&lt;/code&gt; lifts the whip up so the tail lands on the line&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hammer&lt;/strong&gt; — The head is at the bottom of the viewBox and the swing pivot is at the top. With &lt;code&gt;translateY(-77%)&lt;/code&gt;, the pivot sits above the line and the head arcs through the line as it swings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cat / Dog / Duck&lt;/strong&gt; — &lt;code&gt;translateY(-95%)&lt;/code&gt; puts the feet on the line so they look like they're standing on it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fire&lt;/strong&gt; — &lt;code&gt;translateY(-100%)&lt;/code&gt; puts the bottom of the flame row exactly on the line, so flames burn upward from the running text&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pet&lt;/strong&gt; — &lt;code&gt;translateY(-100%)&lt;/code&gt; puts the palm hovering above the line, so it looks like it's petting the running text&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rain&lt;/strong&gt; — Wraps the whole rain effect in a container with &lt;code&gt;height: anchorY&lt;/code&gt; and &lt;code&gt;overflow: hidden&lt;/code&gt;, so the falling motivational words stop right at the running line instead of falling past it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. There's no math. No physics. Just &lt;code&gt;translateY&lt;/code&gt; percentages picked by eyeballing each animation against a real terminal session. Took maybe 90 seconds per animation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trump catchphrase rotation
&lt;/h2&gt;

&lt;p&gt;This deserves a callout because it has its own state. The other animations are pure SVG + CSS keyframes — completely stateless. Trump Mode rotates through 20 catchphrases on its own timer:&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;TRUMP_CATCHPHRASES&lt;/span&gt; &lt;span class="o"&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;MAKE CODING GREAT AGAIN&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;YOU'RE ABSOLUTELY WRONG&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;WRONG. VERY WRONG.&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;FAKE CODE. SAD!&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;BAD CODE. VERY BAD.&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;HUGE PERFORMANCE BOOST&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;TREMENDOUS UPDATE&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;NOBODY WRITES CODE BETTER THAN ME&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ... 12 more&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;function&lt;/span&gt; &lt;span class="nf"&gt;TrumpAnimation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;anchorY&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;AnimationProps&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;phraseIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPhraseIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;floor&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;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;TRUMP_CATCHPHRASES&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="nf"&gt;useEffect&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;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&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;setPhraseIndex&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;i&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;TRUMP_CATCHPHRASES&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="mi"&gt;2500&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&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;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flex items-start gap-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="cm"&gt;/* ...anchored... */&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CartoonCaricature&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SpeechBubble&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;TRUMP_CATCHPHRASES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;phraseIndex&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/SpeechBubble&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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 starting index is randomized so two AI terminals running side-by-side don't say the same thing at the same time. The phrase changes every 2.5 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently if this got bigger
&lt;/h2&gt;

&lt;p&gt;If I were going to scale this past ~30 animations, I'd reach for &lt;a href="https://lottiefiles.com/" rel="noopener noreferrer"&gt;Lottie&lt;/a&gt; or &lt;a href="https://rive.app/" rel="noopener noreferrer"&gt;Rive&lt;/a&gt;. Hand-drawing SVG paths is meditative for a while and then it's tiring. The cat ended up okay. The Trump caricature is &lt;em&gt;decidedly&lt;/em&gt; "programmer art". A real designer working in After Effects could produce the same library in an afternoon and it would look ten times better.&lt;/p&gt;

&lt;p&gt;The tradeoff: that's another ~250 KB of bundle for &lt;code&gt;lottie-web&lt;/code&gt;, plus an asset pipeline. For 18 animations in an Electron app I already ship, the SVG-and-keyframes approach was the right call. But I'd switch in a heartbeat if I had a designer collaborator.&lt;/p&gt;

&lt;p&gt;The other thing I'd reconsider: polling. 250ms is fine but it's still polling. xterm.js has a &lt;code&gt;onWriteParsed&lt;/code&gt; event that fires after each chunk of buffer writes. I could subscribe to that and only re-scan when something actually changed in the buffer. I tried it briefly and got entangled in scroll-position edge cases, so I shipped the polling version. It's on my list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you use &lt;a href="https://1devtool.com" rel="noopener noreferrer"&gt;1DevTool&lt;/a&gt;, update to 1.11.4 and head to &lt;strong&gt;Settings → Terminal → Fun Agent Animation&lt;/strong&gt;. The picker is grouped into Off · Kind ✨ · Motivational 💪 · Chaos 🌪️. Pick one, fire up a Claude Code or Codex session, and watch the animation track the running spinner.&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.amazonaws.com%2Fuploads%2Farticles%2Fvpgf6os0kmiguk3dma5i.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvpgf6os0kmiguk3dma5i.jpg" alt=" " width="800" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're building your own terminal-adjacent feature on top of xterm.js and want to do anything similar — anchoring HUD elements to TUI text, injecting overlays at the cursor row, highlighting specific status lines — the &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;hook source&lt;/a&gt; is in &lt;code&gt;useAgentRunningLine.ts&lt;/code&gt; and it's about 80 lines including comments. The technique generalizes to any TUI that uses a spinner glyph or a parenthesized timer, which is most of them.&lt;/p&gt;

&lt;p&gt;The thing I love about this feature is that it's 100% useless and 100% delightful. The whip cracks, and Claude keeps Boondoggling, and somehow my afternoon is 1% better. Sometimes the best engineering work is the work that just makes you smile.&lt;/p&gt;

&lt;p&gt;If you build something fun on top of this idea — or if you have an animation suggestion I should add — I'd love to see it. The next one might be a tiny version of you, refreshing twitter.&lt;/p&gt;

&lt;p&gt;Happy coding.&lt;/p&gt;

</description>
      <category>react</category>
      <category>electron</category>
      <category>typescript</category>
      <category>ai</category>
    </item>
    <item>
      <title>I got tired of juggling terminal windows for every AI agent, so I built an all-in-one IDE</title>
      <dc:creator>Khoa Nguyen</dc:creator>
      <pubDate>Thu, 26 Mar 2026 08:09:58 +0000</pubDate>
      <link>https://dev.to/khoavannguyen/i-got-tired-of-juggling-terminal-windows-for-every-ai-agent-so-i-built-an-all-in-one-ide-3m99</link>
      <guid>https://dev.to/khoavannguyen/i-got-tired-of-juggling-terminal-windows-for-every-ai-agent-so-i-built-an-all-in-one-ide-3m99</guid>
      <description>&lt;p&gt;If you use AI coding agents like &lt;strong&gt;Claude Code&lt;/strong&gt;, &lt;strong&gt;Codex CLI&lt;/strong&gt;, &lt;strong&gt;Gemini CLI&lt;/strong&gt;, or &lt;strong&gt;Amp&lt;/strong&gt;, you probably have a mess of terminal windows scattered across your screen.&lt;/p&gt;

&lt;p&gt;Switching between projects means losing context, and there's no easy way to see what all your agents are doing at once. I kept closing everything at the end of a session and spending the first 10 minutes of the next one just getting back to where I was.&lt;/p&gt;

&lt;p&gt;So I built something to fix that.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is 1DevTool?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1DevTool&lt;/strong&gt; is an Electron app that gives you a single persistent workspace per project — multiple real terminals, a file explorer, code editor, HTTP client, database client, and an embedded browser, all in one window.&lt;/p&gt;

&lt;p&gt;The core idea: your AI agents shouldn't live in isolated terminal tabs. They should coexist in a layout you control, with full context preserved across sessions.&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.amazonaws.com%2Fuploads%2Farticles%2F0eed8lntp3f86yvnhw36.webp" 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.amazonaws.com%2Fuploads%2Farticles%2F0eed8lntp3f86yvnhw36.webp" alt="https://dqy38fnwh4fqs.cloudfront.net/UHNN7ND8LGD6NEPIJJND6NGQ7LKO/blog/blog-content-image-e9d7dbb5-1e3f-49d5-a3cf-08caf79aa6f3.webp" width="800" height="468"&gt;&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2Fugi8dtf5d16bisxne69u.webp" 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.amazonaws.com%2Fuploads%2Farticles%2Fugi8dtf5d16bisxne69u.webp" alt="https://dqy38fnwh4fqs.cloudfront.net/UHNN7ND8LGD6NEPIJJND6NGQ7LKO/blog/blog-content-image-90a8e372-5d0d-4f32-a2a3-580d2b8306eb.webp" width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Key features
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;🔲 Multi-agent layout&lt;/strong&gt;&lt;br&gt;
Run multiple AI agents simultaneously in a 2×2 grid, columns, or single-focus layout. See everything at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💾 Terminal persistence&lt;/strong&gt;&lt;br&gt;
Sessions survive app restarts via tmux. Your scrollback is always there — no more re-running setup scripts every morning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔔 AI Activity Feed&lt;/strong&gt;&lt;br&gt;
Live notifications when any agent finishes a task or generates files, across all your active projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;✂️ Send code to AI&lt;/strong&gt;&lt;br&gt;
Select code in the editor, right-click, and send it to any running agent — with file path and line numbers included automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔄 Resume AI sessions&lt;/strong&gt;&lt;br&gt;
Browse and continue past conversations from Claude Code, Codex, Gemini CLI, and Amp directly from the sidebar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🧠 Skills Manager&lt;/strong&gt;&lt;br&gt;
Browse, install, and manage AI agent skills (CLAUDE.md, agent rules, etc.) with built-in security scanning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🎨 VS Code themes&lt;/strong&gt;&lt;br&gt;
Import any &lt;code&gt;.vsix&lt;/code&gt; theme file and it applies across the entire workspace — editor, terminal, panels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🗄️ Database client&lt;/strong&gt;&lt;br&gt;
Connect to Postgres, MySQL, MongoDB, Redis, ClickHouse, and 8 more engines without leaving the window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📂 Drag &amp;amp; drop projects&lt;/strong&gt;&lt;br&gt;
Drop a folder from Finder (or Explorer) onto the sidebar to add it instantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🧩 Customizable layouts&lt;/strong&gt;&lt;br&gt;
Drag panels between sidebars, save layout presets, and show/hide header items to match your workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Availability &amp;amp; pricing
&lt;/h2&gt;

&lt;p&gt;Available on &lt;strong&gt;macOS&lt;/strong&gt;, &lt;strong&gt;Windows&lt;/strong&gt;, and &lt;strong&gt;Linux&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Free tier: 1 project, 4 terminals — enough to try it with a real workflow.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://1devtool.com" rel="noopener noreferrer"&gt;1devtool.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Happy to answer any questions or hear feedback in the comments. If you've run into the same terminal chaos with AI agents, I'd love to know how you're currently handling it.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>tooling</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
