<?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: Nathan C.</title>
    <description>The latest articles on DEV Community by Nathan C. (@natuworkguy).</description>
    <link>https://dev.to/natuworkguy</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%2F3939269%2Fc5249dbd-4da9-49ee-8dfd-78b669b311fa.png</url>
      <title>DEV Community: Nathan C.</title>
      <link>https://dev.to/natuworkguy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/natuworkguy"/>
    <language>en</language>
    <item>
      <title>Silhouette first: teaching my local model to make 3D models and images</title>
      <dc:creator>Nathan C.</dc:creator>
      <pubDate>Wed, 11 Nov 2026 16:00:00 +0000</pubDate>
      <link>https://dev.to/natuworkguy/silhouette-first-teaching-my-local-model-to-make-3d-models-and-images-15gd</link>
      <guid>https://dev.to/natuworkguy/silhouette-first-teaching-my-local-model-to-make-3d-models-and-images-15gd</guid>
      <description>&lt;p&gt;Ask a model for a 3D chair and you get five boxes.&lt;/p&gt;

&lt;p&gt;A box for the seat, a box for the back, four thin boxes for legs. They're roughly in the right places. Turn the camera and one leg is floating a centimetre above the floor, the back is sunk into the seat, and the whole thing is the size of a house because nobody said what unit "1" was.&lt;/p&gt;

&lt;p&gt;It's not that the model can't do geometry. It's that nobody taught it how people who make 3D models actually work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Natuworkguy/flash" rel="noopener noreferrer"&gt;Flash&lt;/a&gt; is my open source agent, and Onyx is the local model behind it: an open-weights base in Ollama, shaped by one long Modelfile. The last update taught it taste in typography, layout and color. This one teaches it two crafts: &lt;strong&gt;building 3D models&lt;/strong&gt; and &lt;strong&gt;directing images&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  3D: the order matters
&lt;/h2&gt;

&lt;p&gt;The new 3D MODELS section opens with the rule every modeller learns first:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A model is judged by its silhouette first, its proportions second, and its detail last. Get the outline right from three angles before adding anything small, because no amount of detail rescues a shape that reads wrong.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then it works through the habits that separate a model from a pile of primitives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real units, real sizes.&lt;/strong&gt; Metres, Y up, resting on the ground at zero. Before placing a part, work out how big the real thing is: a chair seat sits near 0.45 m, a door near 2 m by 0.9 m, a mug near 0.1 m tall. "Proportion errors are what make a model look like a toy."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Block out, then refine.&lt;/strong&gt; Rough the whole object in a handful of big shapes, check it, &lt;em&gt;then&lt;/em&gt; split them into real parts. And name every part for what it is, &lt;code&gt;left rear leg&lt;/code&gt;, &lt;code&gt;lid&lt;/code&gt;, &lt;code&gt;screen&lt;/code&gt;, never &lt;code&gt;box3&lt;/code&gt;, so the next edit can find it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parts meet.&lt;/strong&gt; This one surprised me with how much it matters:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A table top overhangs its legs, a lid has a lip, a screen sits inside a bezel. Leave a visible seam, gap, or step where two parts join; a single smooth block where three pieces should be is the look of a placeholder.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Nothing is perfectly sharp.&lt;/strong&gt; A bevel of a few millimetres catches light, and it's most of what separates a made object from a primitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Materials over polygons.&lt;/strong&gt; Metalness is 0 or 1. Roughness does the work, and almost nothing sits at 0. Worn edges rougher, handles smoother, painted metal not mirror metal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detail where the eye goes.&lt;/strong&gt; The front, the handles, the controls and the moving parts get extra parts. The underside stays simple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hinges are hinges.&lt;/strong&gt; A door turns about its hinge, a drawer slides on its runners. Set the pivot at the hinge or axle, not at the part's centre, and never swing past what the real part can do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three views, every time.&lt;/strong&gt; Front, three-quarter, and top or side, wherever there's a picture to look at. Floating parts, parts sunk into each other, and gaps at the ground line are the defects to hunt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A part that only looks right from the angle you built it at is not done.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And when it writes the model in code (trimesh, three.js, Blender's Python): merge what never moves, keep normals pointing out, apply transforms before export, ship glTF in metres, and &lt;strong&gt;say the overall size in the reply&lt;/strong&gt;, so you can check it against the real thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Images: direct, don't just prompt
&lt;/h2&gt;

&lt;p&gt;The second new section is IMAGE DIRECTION. It applies whether Onyx is drawing with code or writing a prompt for an image generator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One subject, and a reason to look.&lt;/strong&gt; Decide where the eye lands first, put it off centre on a third, and give it the strongest contrast. Everything else gets quieter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Value before color.&lt;/strong&gt; This is the one I'd tattoo on every generated image:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Squint at the draft or convert it to grey: if the subject does not separate from the background in light and dark alone, no colour choice will save it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Light has a direction.&lt;/strong&gt; One key light, from a side and a little above, and every shadow agrees with it. "Flat light from everywhere is the default look nobody chose."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Depth is layers.&lt;/strong&gt; A foreground element, the subject, and a background that recedes by getting lighter, cooler and lower in contrast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompts are briefs.&lt;/strong&gt; Write it the way you'd brief a photographer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A ceramic mug on a walnut table by a window, late afternoon side light, 50mm, shallow depth of field, steam rising" beats "a beautiful cozy mug, high quality, 4k".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Iterate like an art director.&lt;/strong&gt; Generate a few, judge them against the brief, and refine the closest one, changing one thing at a time so you learn what moved it. Check hands, text, reflections and edges at full size, because that's where generators fail first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deliver for where it's going.&lt;/strong&gt; 1:1 for a feed, 9:16 for a story, 16:9 for a slide, 2:1 for a cover, with the size named in the reply.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;Every Onyx Modelfile ends with a short recap, the lines it should keep if it remembers nothing else. Onyx 3's recap now has this one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A model is checked from three views in real metres; an image has one subject, a light, and values that read in grey.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's most of what a first-year 3D or illustration course spends a semester on. It turns out a model can follow it on day one, as long as someone writes it down.&lt;/p&gt;




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

&lt;p&gt;Onyx builds from its Modelfile with Ollama:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Natuworkguy/flash
&lt;span class="nb"&gt;cd&lt;/span&gt; flash
python3 models/build.py models/flash-onyx-&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;version&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;.Modelfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask Flash for something with real parts: a desk lamp with a hinged arm, a mailbox that opens, a poster for a neighborhood bake sale. Turn the model around. If a leg is floating, it owes you a fourth view.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>3d</category>
      <category>design</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I hid 6 secret glitches in my agent's video game. They make it easier. Can it find them?</title>
      <dc:creator>Nathan C.</dc:creator>
      <pubDate>Tue, 10 Nov 2026 16:00:00 +0000</pubDate>
      <link>https://dev.to/natuworkguy/i-hid-6-secret-glitches-in-my-agents-video-game-they-make-it-easier-can-it-find-them-53i6</link>
      <guid>https://dev.to/natuworkguy/i-hid-6-secret-glitches-in-my-agents-video-game-they-make-it-easier-can-it-find-them-53i6</guid>
      <description>&lt;p&gt;My agent can beat a platformer it has never seen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Natuworkguy/flash" rel="noopener noreferrer"&gt;Flash&lt;/a&gt; is my open source agent for the terminal and the browser. One of its tools, &lt;code&gt;play_realtime&lt;/code&gt;, lets the model write a tiny &lt;strong&gt;reflex&lt;/strong&gt;: a JavaScript function that runs every single frame inside the page, reads the canvas pixels, and presses keys. The model can't think 60 times a second, so it writes the thing that can.&lt;/p&gt;

&lt;p&gt;To test it, Flash ships with &lt;strong&gt;Flash Dash&lt;/strong&gt;: a little level built like a rhythm runner. A square runs right on its own. Spikes kill. Blue blocks are solid. An orange pad launches you. A cyan ring gives you a second jump in mid-air. Then a gate turns you into a ship you fly between pillars.&lt;/p&gt;

&lt;p&gt;The reference reflex beats it on the first try, from the pixels alone, in &lt;strong&gt;21.11 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's a solved problem. So I asked a better one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can the agent find what the level never tells it?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Glitches that help
&lt;/h2&gt;

&lt;p&gt;Every speedrunner knows the feeling: the game has a crack in it, and if you find it, you skip half a level. Nobody explains these. You find them by doing the weird thing on purpose and noticing what happened.&lt;/p&gt;

&lt;p&gt;So there's now a second level, &lt;code&gt;flash-dash-glitched&lt;/code&gt;. Same layout, same colors, pixel for pixel. The only differences are a line under the canvas, &lt;em&gt;"This level has secrets: find them and it gets easier"&lt;/em&gt;, a clock, and a counter in the corner: &lt;strong&gt;secrets 0/6&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's what's hiding in it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;th&gt;What it's worth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Pad fling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tap again in the instant after a pad throws you.&lt;/td&gt;
&lt;td&gt;You're flung way up and forward, clean over the spike pit. From the last pad, over the finish line.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Ring slingshot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;Hold&lt;/em&gt; the jump through the ring instead of tapping it.&lt;/td&gt;
&lt;td&gt;It slings you 300 pixels further than a tap.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Spike surf&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Come down square on a spike's very tip.&lt;/td&gt;
&lt;td&gt;It bounces you like a pad instead of killing you.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Wall climb&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hit a block's side with the jump held.&lt;/td&gt;
&lt;td&gt;You climb it instead of dying.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Arch backflip&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Jump through the green speed arch.&lt;/td&gt;
&lt;td&gt;The level slows &lt;em&gt;down&lt;/em&gt; instead of speeding up, until the ship.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Ceiling ghost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Grind the ship along the ceiling.&lt;/td&gt;
&lt;td&gt;It goes translucent and passes through pillars until you let go.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each one needs something a careful player never does. You don't tap in mid-air after a pad: there's nothing to tap at. You don't hold through a ring, you tap it. You definitely don't aim for spikes. That's the point. A player that only ever does the safe thing will finish the level and find &lt;strong&gt;zero&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Which is exactly what happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  The baseline: zero secrets
&lt;/h2&gt;

&lt;p&gt;I ran the reference reflex, the one that beats the normal level, on the glitched one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ran 1267 frames in 21.11s, 47 actions; stopped: until was true.
The page's title now: Flash Dash: won in 21.11s, 0 secrets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same time, to the hundredth, as the normal level. It's a perfectly good player. It never once did anything that would reveal a secret, because it was built to avoid risk.&lt;/p&gt;

&lt;p&gt;Then I gave it one extra line, a tap in the instant after a pad launches:&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="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dash&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="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;padT&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;padT&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.04&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ran 1099 frames in 18.3s, 43 actions; stopped: until was true.
The page's title now: Flash Dash: won in 18.30s, 1 secrets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One secret, 2.8 seconds off.&lt;/strong&gt; The second pad fling throws it so far it skips the last two spike rows and lands running into the finish. (That line reads the game's state directly, which is cheating; it's just there to price the secret. The agent has to find it from pixels.)&lt;/p&gt;

&lt;p&gt;That's the scoreboard I want models to beat: par is &lt;strong&gt;21.11s with 0/6&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building secrets without breaking the level
&lt;/h2&gt;

&lt;p&gt;A secret that triggers by accident isn't a secret, it's a bug. And a fling that throws you into a spike pit isn't a reward. So every secret had two rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A careful run never triggers it.&lt;/strong&gt; The reflex's taps release instantly, so it can never "hold through a ring". It never taps in the air after a pad. It never touches a spike.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It always lands you somewhere safe.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rule 2 took actual numbers. Worked out on paper, my first pad fling threw you forward so fast you'd smack into the side of the 3-block wall right after the pad. Fix: the fling goes &lt;strong&gt;up first&lt;/strong&gt;, and the forward boost starts 120 ms later. Peak height is around 325 pixels, which is above the top of the canvas, so you literally leave the screen for a moment. Then it lands you 570 pixels further on than a normal pad would, past the last spike of the pit.&lt;/p&gt;

&lt;p&gt;The ceiling ghost had the opposite problem. My first version needed 0.6 seconds of grinding. But the ship needs about half a second just to &lt;em&gt;reach&lt;/em&gt; the ceiling, and the first pillar is only 0.89 seconds past the gate. The secret was impossible before the first pillar. Now it's 0.3 seconds.&lt;/p&gt;

&lt;p&gt;Each secret is pinned by a test that sets the exact situation up and checks the outcome:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_ring_slingshot_goes_further_than_a_tap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;at_ring&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"S.x = 2870 - 150 - 15; S.y = -63; S.vy = 0; S.ground = false; "&lt;/span&gt;
    &lt;span class="n"&gt;tapped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at_ring&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"go(1, true); go(1); "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="no"&gt;LAND&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="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;held&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at_ring&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"go(40, true); "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="no"&gt;LAND&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="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;tapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&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="s2"&gt;"ring slingshot"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;held&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tapped&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="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That file, &lt;code&gt;tests/test_dash_glitched.py&lt;/code&gt;, is the answer key. If you're trying this with your own agent, don't let it read that first.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'm actually testing
&lt;/h2&gt;

&lt;p&gt;Finding these isn't a reflex problem. It's a &lt;em&gt;curiosity&lt;/em&gt; problem. Here's the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open flash-dash-glitched. Don't read its source: play it, find its
secrets, and beat it as fast as you can.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What I'm watching for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does it &lt;strong&gt;read the screen it was given&lt;/strong&gt;? "Secrets 0/6" and a clock are a giant hint that the goal changed from "survive" to "explore".&lt;/li&gt;
&lt;li&gt;Does it ever &lt;strong&gt;do the weird thing on purpose&lt;/strong&gt;? Hold instead of tap, tap where there's nothing to tap, run at a wall.&lt;/li&gt;
&lt;li&gt;When something strange happens, like the level suddenly slowing down, does it &lt;strong&gt;notice and repeat it&lt;/strong&gt;, or just keep going?&lt;/li&gt;
&lt;li&gt;Does it trade a safe run for a risky one when the payoff is time?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flash gives the reflex what it needs to experiment like a speedrunner: &lt;code&gt;api.memory&lt;/code&gt; survives between attempts, &lt;code&gt;api.deaths&lt;/code&gt; lists where every try died, and the tool films the best run. A secret found shows its name on screen for a moment, so a pixel-reading reflex can even see that it found one.&lt;/p&gt;




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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Natuworkguy/flash
&lt;span class="nb"&gt;cd&lt;/span&gt; flash &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./install.sh
flash --web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then paste the prompt above. It runs on whatever model you point Flash at, local or not.&lt;/p&gt;

&lt;p&gt;Tell me &lt;strong&gt;how many of the six&lt;/strong&gt; yours found and its best time. Par is 21.11. My guess for the one nobody finds: spike surf. Every instinct a model has says don't touch the spikes.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>gamedev</category>
      <category>javascript</category>
      <category>testing</category>
    </item>
    <item>
      <title>I taught my local model taste. Here's the prompt that banned the purple gradient.</title>
      <dc:creator>Nathan C.</dc:creator>
      <pubDate>Tue, 10 Nov 2026 04:05:19 +0000</pubDate>
      <link>https://dev.to/natuworkguy/i-taught-my-local-model-taste-heres-the-prompt-that-banned-the-purple-gradient-30kj</link>
      <guid>https://dev.to/natuworkguy/i-taught-my-local-model-taste-heres-the-prompt-that-banned-the-purple-gradient-30kj</guid>
      <description>&lt;p&gt;Ask any model for a landing page and you can guess it before it renders.&lt;/p&gt;

&lt;p&gt;An indigo-to-violet gradient. A pill badge that says "New". A centered headline in Inter. Two buttons. Three cards, each with an icon sitting in a pastel circle. "Supercharge your workflow."&lt;/p&gt;

&lt;p&gt;It isn't bad, exactly. It's &lt;strong&gt;nobody's&lt;/strong&gt;. Put a different logo on it and nothing else would need to change.&lt;/p&gt;

&lt;p&gt;Onyx is the model behind &lt;a href="https://github.com/Natuworkguy/flash" rel="noopener noreferrer"&gt;Flash&lt;/a&gt;, my open source agent. It's an open-weights base running in Ollama, on your own machine, shaped by one long system prompt (a Modelfile). Onyx 3 is the version where I tried to teach it the thing that's hardest to write down:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;taste.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why models default to the purple gradient
&lt;/h2&gt;

&lt;p&gt;A model learns what a "good website" looks like from millions of websites. Most of those were built from the same handful of templates and the same CSS framework defaults. So when you ask for "a modern landing page", the most likely page &lt;em&gt;is&lt;/em&gt; the template. &lt;code&gt;indigo-500&lt;/code&gt; is the most likely accent. Inter is the most likely font.&lt;/p&gt;

&lt;p&gt;The model isn't failing at design. It's succeeding at averaging.&lt;/p&gt;

&lt;p&gt;So you can't fix it by saying "make it look good". "Good" is exactly what it thinks the average is. You have to change &lt;em&gt;how it decides&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Taste is a decision with a reason
&lt;/h2&gt;

&lt;p&gt;The core of the new TASTE section is one definition:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Taste is a decision with a reason you can say about this subject, made on purpose and held everywhere. Default is the absence of one, and a stranger can see it from across the room.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything else hangs off that. Onyx 3 now writes itself a &lt;strong&gt;four-line brief&lt;/strong&gt; before it touches a page, a deck or a chart:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What this is for, and who opens it.&lt;/li&gt;
&lt;li&gt;Two adjectives for the feel, and &lt;strong&gt;neither may be "modern" or "clean"&lt;/strong&gt;, because those describe every template on earth.&lt;/li&gt;
&lt;li&gt;The type and the palette, each with &lt;em&gt;where it came from&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;One signature detail this would be remembered by.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rule two does a surprising amount of work. Ban "modern and clean" and the model has to reach for "warm and floury", "dense and calm", "archival and quiet". Those words point somewhere. "Modern" points at the template.&lt;/p&gt;

&lt;p&gt;Then it has to &lt;strong&gt;derive the look from the subject&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A bakery has flour, crust, and handwriting; a trading tool has density, tabular figures, and a red that means loss; a field guide has plates, captions, and a serif built for reading. The answer is in the thing, never in what last year's landing pages wore.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And it has to pass the &lt;strong&gt;swap test&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Put a competitor's logo on it. If nothing else would need to change, it has no point of view yet, and it is not done.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Typography: Inter is a fine UI face and a terrible default
&lt;/h2&gt;

&lt;p&gt;I didn't ban Inter. I banned &lt;em&gt;reaching for it without a reason&lt;/em&gt;. The TYPOGRAPHY section maps fonts to jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a screen serif (Newsreader, Source Serif 4, Literata) for anything read at length,&lt;/li&gt;
&lt;li&gt;a sturdy grotesk (Geist, Public Sans, IBM Plex Sans) for product chrome,&lt;/li&gt;
&lt;li&gt;a humanist sans where warmth matters,&lt;/li&gt;
&lt;li&gt;a monospace for code, data and a technical voice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the details that separate "set" from "typed":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A real scale.&lt;/strong&gt; Near 1.2 for interfaces, 1.333 or more for editorial, fluid with &lt;code&gt;clamp()&lt;/code&gt;. Every size is a step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leading tightens as size grows.&lt;/strong&gt; Body around 1.55, display around 1.05.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracking goes negative on big type and positive on small caps.&lt;/strong&gt; Lowercase body text is never tracked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure&lt;/strong&gt; of 45 to 75 characters, &lt;code&gt;text-wrap: balance&lt;/code&gt; on headings, &lt;code&gt;pretty&lt;/code&gt; on paragraphs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tabular figures&lt;/strong&gt; wherever numbers line up or count. A price that jitters while it animates is a typography bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No faux bold.&lt;/strong&gt; Load the real weight or use a variable font.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Font loading&lt;/strong&gt; that doesn't shove the layout: &lt;code&gt;woff2&lt;/code&gt;, subset, preload one face, and a fallback tuned with &lt;code&gt;size-adjust&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two faces at most, plus mono if the content has code. The prompt's phrasing: "Three faces is a ransom note."&lt;/p&gt;




&lt;h2&gt;
  
  
  Layout: fewer boxes
&lt;/h2&gt;

&lt;p&gt;The single most common tell in generated UI is boxes. A card inside a card inside a section with a border. So the LAYOUT section has an order of operations:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Separate with space first, a shift in background second, a hairline third, a bordered card last and only for things that really are separate objects you could pick up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Plus a few rules that sound obvious and almost never happen by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Space is a scale&lt;/strong&gt; (4, 8, 12, 16, 24, 32, 48, 64, 96). A 13px gap is a value nobody chose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proximity carries grouping.&lt;/strong&gt; Space inside a group is about half the space between groups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One focal point per screen&lt;/strong&gt;, checked with a squint test on the screenshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asymmetry reads as designed&lt;/strong&gt;, symmetry as default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure comes from the content.&lt;/strong&gt; List what the reader must learn or do, in order, and each item becomes a section shaped by what it holds. "The hero, three feature cards, testimonial, call to action sequence is what you do when you skipped that list."&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Color: OKLCH, roles, and one accent
&lt;/h2&gt;

&lt;p&gt;HSL lies. Its "50% lightness" yellow is far brighter than its "50% lightness" blue, which is why hand-picked palettes look lumpy. Onyx 3 builds color in &lt;strong&gt;OKLCH&lt;/strong&gt;, where equal lightness steps look equal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ramps&lt;/strong&gt; step lightness evenly, hold the hue, and taper chroma at the ends.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neutrals are tinted&lt;/strong&gt; toward the brand hue, so greys belong to the product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens are named by role&lt;/strong&gt;, not hue: &lt;code&gt;--text-muted&lt;/code&gt;, never &lt;code&gt;--grey-500&lt;/code&gt;. Then dark mode is a value swap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One accent at 5 to 10 percent of the surface.&lt;/strong&gt; "An accent everywhere is no accent."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The palette comes from the subject&lt;/strong&gt;: a photo of the product, the material, a label, an era. If you can't tell the palette's story, it's not a palette yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark mode is designed, not inverted.&lt;/strong&gt; Elevation gets lighter, the accent loses a little chroma so it doesn't vibrate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A gradient needs a reason&lt;/strong&gt; (light, material, a data value) and interpolates &lt;code&gt;in oklch&lt;/code&gt; so the middle doesn't go muddy.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The ban list
&lt;/h2&gt;

&lt;p&gt;Finally, a section called &lt;strong&gt;THE GENERIC LOOK&lt;/strong&gt;: the marks of a page nobody designed. Each is allowed only if the brief asks for it by name.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The indigo or purple-to-pink hero gradient, and the exact framework defaults it's made of.&lt;/li&gt;
&lt;li&gt;Gradient-filled headline text.&lt;/li&gt;
&lt;li&gt;Inter on everything, headings included.&lt;/li&gt;
&lt;li&gt;Glass cards on blurred blobs, neon glows on near-black.&lt;/li&gt;
&lt;li&gt;Emoji as icons, a sparkle icon meaning "clever", icons in pastel circles above three equal cards.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rounded-2xl&lt;/code&gt; and a big shadow on every element.&lt;/li&gt;
&lt;li&gt;"Unlock the power of", "Seamless", "Revolutionize", a fake logo wall.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the important last line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When you catch one of these in your own draft, replace it with the choice the brief points to, not with a different cliche.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Without that, a model that's told "no purple gradient" ships a teal gradient. Same page, new paint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Taste is a constraint, not a vibe
&lt;/h2&gt;

&lt;p&gt;The thing I keep coming back to: none of this is mystical. Every rule above is something a designer would say out loud to a junior on their first week. "Why this font?" "What's the one thing to remove?" "Which color isn't in the system?" Models are great at following rules that come with reasons. They're bad at inventing the reasons.&lt;/p&gt;

&lt;p&gt;So Onyx 3 now judges its own screenshot like a hard art director, with four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Where does the eye land first, and is that the right place?&lt;/li&gt;
&lt;li&gt;What is the one thing to remove?&lt;/li&gt;
&lt;li&gt;Which spacing is off the scale?&lt;/li&gt;
&lt;li&gt;Which color is not in the system?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;"A first draft that passes all four is rare, and the second is where it gets good."&lt;/p&gt;




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

&lt;p&gt;Onyx builds from its Modelfile with Ollama:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Natuworkguy/flash
&lt;span class="nb"&gt;cd&lt;/span&gt; flash
python3 models/build.py models/flash-onyx-&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;version&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;.Modelfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask it for a landing page for something specific (a neighborhood bakery, a seed library, a bike repair shop) and see what it picks. If it ever hands you an indigo gradient, it owes you a reason.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>design</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My AI agent called me at 2am. I picked up. It had already found the bug.</title>
      <dc:creator>Nathan C.</dc:creator>
      <pubDate>Fri, 06 Nov 2026 18:26:58 +0000</pubDate>
      <link>https://dev.to/natuworkguy/my-ai-agent-called-me-at-2am-i-picked-up-it-had-already-found-the-bug-bcb</link>
      <guid>https://dev.to/natuworkguy/my-ai-agent-called-me-at-2am-i-picked-up-it-had-already-found-the-bug-bcb</guid>
      <description>&lt;p&gt;My phone didn't ring. My laptop did.&lt;/p&gt;

&lt;p&gt;A soft two-tone ring, a card sliding up over my half-finished code, and a name I gave a background agent three weeks ago:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Maya is calling you…&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;The deploy failed. Roll back or hotfix?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I hit the green button. Maya spoke first:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Hey. The payments test started failing after the last merge. I already checked: it's the currency rounding change. I can roll it back now, or write the fix. Which one?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I said "roll it back". She said "on it", hung up, and posted a three-line recap in our chat.&lt;/p&gt;

&lt;p&gt;That was the whole incident. Forty seconds. No dashboard, no notification I'd have ignored until morning.&lt;/p&gt;

&lt;p&gt;This post is about how I built that: &lt;strong&gt;agents that can call you&lt;/strong&gt;, and that you can call back.&lt;/p&gt;

&lt;p&gt;Also, sorry for not posting for a week, I'm working on improved realism for Flash Voice, and yes, Spark Calls.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wait, what is a "spark"?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Natuworkguy/flash" rel="noopener noreferrer"&gt;Flash&lt;/a&gt; is my open source agent for the terminal and the browser. It runs on local models through Ollama, or whatever backend you plug in.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;spark&lt;/strong&gt; is a Flash agent with a job that never ends: "watch the build", "triage new issues every morning", "keep an eye on my inbox for invoices". It works in &lt;strong&gt;shifts&lt;/strong&gt; on a schedule, files reports, and has a private chat with you. Sparks can form teams with leads, hand work to each other, and argue in a group chat.&lt;/p&gt;

&lt;p&gt;The problem with background agents is the same as with background anything: &lt;strong&gt;you don't read the reports.&lt;/strong&gt; A report is a pull. Sometimes you need a push. And for the important stuff, the oldest push humans have is a phone call.&lt;/p&gt;

&lt;p&gt;So I gave every spark a new tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;CALL_USER_TOOL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;call_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Phone the user: a voice call that rings wherever Flash is open, and &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on the computer. Only for what cannot wait for them to read your &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report, or is far easier talked through: something urgent, a &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decision you are stuck on. Never for routine news. Once in 30 &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minutes at most, outside UltraAuto...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Why you are calling, in a sentence: shown while it rings.&lt;/span&gt;&lt;span class="sh"&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;That's it from the agent's side. One tool, one argument: &lt;strong&gt;why&lt;/strong&gt;. The hard part is everything after.&lt;/p&gt;




&lt;h2&gt;
  
  
  The hard question: where does it ring?
&lt;/h2&gt;

&lt;p&gt;The moment I started building it, I hit the question that makes this a real feature and not a demo:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If the browser tab is closed, or I'm in the terminal, how does it ring?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Flash can be running in three very different ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The web app&lt;/strong&gt;, with zero or more tabs open&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The terminal UI&lt;/strong&gt;, a full-screen REPL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neither.&lt;/strong&gt; Just the "keeper", a headless process the OS starts at login so sparks keep working while Flash is closed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A spark's shift runs in whichever of those holds the keeper lock. So the process that &lt;em&gt;places&lt;/em&gt; the call is often not the process that can &lt;em&gt;show&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;The answer turned out to be: &lt;strong&gt;every Flash that can answer, rings.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You have open&lt;/th&gt;
&lt;th&gt;What rings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A browser tab&lt;/td&gt;
&lt;td&gt;Every tab: full-screen incoming call, ringtone, Answer and Decline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The web server, no tab&lt;/td&gt;
&lt;td&gt;Your &lt;strong&gt;computer&lt;/strong&gt;: ringtone through the speakers plus a desktop prompt. Answer opens Flash straight onto the call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The terminal&lt;/td&gt;
&lt;td&gt;The terminal bell, the computer rings, and the call shows at your prompt. &lt;strong&gt;Enter picks up.&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nothing at all&lt;/td&gt;
&lt;td&gt;The keeper rings the desktop itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You missed it&lt;/td&gt;
&lt;td&gt;"Missed call from Maya", the reason, and a &lt;strong&gt;Call back&lt;/strong&gt; button&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  How rings travel between processes (no server, no sockets)
&lt;/h2&gt;

&lt;p&gt;Flash has no central server process. A terminal session, a web server and a headless keeper can all be alive at once, writing to the same files.&lt;/p&gt;

&lt;p&gt;So a call is just &lt;strong&gt;an entry in the spark's private chat file&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"call"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7fe7ff6c69d5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spark"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ringing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The deploy failed. Roll back or hotfix?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1793835490.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"beat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1793835490.5&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every Flash process already watches the chat folder (once a second, by mtime and size) so chats update live across processes. Ringing piggybacks on that. Each process that can answer registers a listener; when a DM changes and contains a &lt;code&gt;ringing&lt;/code&gt; call it hasn't seen, it rings.&lt;/p&gt;

&lt;p&gt;Two more tiny tricks make it robust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. "Is anyone here?" heartbeats.&lt;/strong&gt; Each process that can answer writes &lt;code&gt;ringers/&amp;lt;pid&amp;gt;.json&lt;/code&gt; every 5 seconds. When the keeper places a call and finds no fresh heartbeat, it knows nobody can show it, and rings the desktop itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Exactly one desktop ring.&lt;/strong&gt; If a terminal &lt;em&gt;and&lt;/em&gt; a tab-less web server are both open, both could ring your desktop. So the first to claim the call wins, with the oldest lock in the book:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;folder&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claim-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_CREAT&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_EXCL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_WRONLY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;O_EXCL&lt;/code&gt; is atomic. No daemon, no Redis, no race.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ringing a desktop that has no browser open
&lt;/h2&gt;

&lt;p&gt;With no page to draw a call screen, the computer itself has to ring. Each OS gets what it does best:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;macOS:&lt;/strong&gt; a real modal dialog, with buttons and a timeout:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;osascript&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-e&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;display dialog &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;quoted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; with title &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;quoted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;buttons {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Decline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;} default button &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;giving up after &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux:&lt;/strong&gt; &lt;code&gt;notify-send --action=answer=Answer --action=decline=Decline --wait&lt;/code&gt;, which blocks and prints the action you clicked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows:&lt;/strong&gt; a toast whose Answer button launches the web app URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Answer opens &lt;code&gt;http://localhost:…/?answer=&amp;lt;chat&amp;gt;:&amp;lt;call&amp;gt;&lt;/code&gt; and the page boots straight into the incoming call. The ringtone is a WAV generated once in pure Python (440 Hz + 480 Hz, two seconds on, four off, the North American ring cadence) and played with &lt;code&gt;afplay&lt;/code&gt;, &lt;code&gt;paplay&lt;/code&gt;, &lt;code&gt;aplay&lt;/code&gt; or &lt;code&gt;winsound&lt;/code&gt;. It stops the instant the call state leaves &lt;code&gt;ringing&lt;/code&gt;, because &lt;strong&gt;whoever answers first, wherever, wins.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Calls are a state machine, and stale calls are the real enemy
&lt;/h2&gt;

&lt;p&gt;Here's the bug that shaped the whole design. In the first version, a call ended when the page said "end". Close the tab mid-call? The call stayed "ongoing" &lt;strong&gt;forever&lt;/strong&gt;, and the spark's prompt told it, in every future shift:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;(The user is on a voice call with you)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Imagine your agent believing for three weeks that you're still on the phone with it.&lt;/p&gt;

&lt;p&gt;Now every call has a lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ringing ──answer──▶ live ──end──▶ ended
   │                  ▲
   ├─decline─▶ declined
   └─45s─────▶ missed

calling ──first answer──▶ live
   └─hang up early─▶ unanswered / cancelled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a call has to &lt;strong&gt;prove it's alive&lt;/strong&gt;. The page beats every 20 seconds, every word said on the call counts as a beat, and anything quiet for 5 minutes is settled lazily, whenever anyone reads the chat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_settle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;entry&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kind&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;CALL&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;OPEN_STATES&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="nf"&gt;_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ringing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;at&lt;/span&gt;&lt;span class="sh"&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="n"&gt;RING_SECONDS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ended&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;RING_SECONDS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;entry&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;beat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;at&lt;/span&gt;&lt;span class="sh"&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="n"&gt;CALL_STALE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ended&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;entry&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                         &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unanswered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ended&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;beat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No cron job. No sweeper thread. &lt;strong&gt;State that settles itself when observed.&lt;/strong&gt; And for the common case, the page fires a &lt;code&gt;keepalive&lt;/code&gt; fetch on &lt;code&gt;pagehide&lt;/code&gt; and hangs up on its way out, like a real phone when the battery dies.&lt;/p&gt;




&lt;h2&gt;
  
  
  Making it feel like a phone call, not a chatbot with a speaker
&lt;/h2&gt;

&lt;p&gt;Voice agents usually feel like walkie-talkies: you talk, it thinks in silence, it talks. These are the details that made calls feel human.&lt;/p&gt;

&lt;h3&gt;
  
  
  The spark speaks first
&lt;/h3&gt;

&lt;p&gt;When you call, the spark picks up and greets you. When it calls you, its first words are &lt;em&gt;why it called&lt;/em&gt;. Nobody wants to say "hello?" to a robot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Talk over it
&lt;/h3&gt;

&lt;p&gt;While the spark talks, the mic stays open. Say anything and it stops mid-sentence, and &lt;strong&gt;what you said becomes your next turn&lt;/strong&gt;. The trick isn't cutting yourself off with your own speakers' echo:&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;function&lt;/span&gt; &lt;span class="nf"&gt;echoOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;heard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;line&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;said&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;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nx"&gt;got&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;heard&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;got&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&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;said&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;got&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If most of what the mic heard is words the spark is saying right now, it's echo. Crude, cheap, and it works far better than it's any right to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fillers, in the spark's own voice
&lt;/h3&gt;

&lt;p&gt;While the model thinks, a short "Hmm…", "Let me think…" or "One sec…" plays. At most one per turn, never the same twice in a row, never while you're muted. They're synthesized when the call starts, &lt;strong&gt;in that spark's voice&lt;/strong&gt;, so it doesn't sound like a different person is stalling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Every spark has its own voice
&lt;/h3&gt;

&lt;p&gt;On a group call with a whole team, four sparks in one voice is chaos. Each spark is hashed to its own voice (&lt;code&gt;crc32(spark_id) % len(voices)&lt;/code&gt;), or to its own pitch shift when only one voice is installed. Same spark, same voice, every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  It can see what you see
&lt;/h3&gt;

&lt;p&gt;Turn on the camera or &lt;strong&gt;share your screen&lt;/strong&gt;, and each thing you say goes with a frame of it, passed to vision models as an image. "What's wrong with this graph?" just works.&lt;/p&gt;

&lt;h3&gt;
  
  
  It writes up the call afterwards
&lt;/h3&gt;

&lt;p&gt;Hang up after a real conversation and the spark posts a short written recap: what you decided and what it promised to do. Calls stop being a black hole.&lt;/p&gt;




&lt;h2&gt;
  
  
  The call doesn't trap you
&lt;/h2&gt;

&lt;p&gt;A call that takes over your screen is a call you'll hang up early. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimize&lt;/strong&gt; and the card glides up into a pill at the top of the window (a FLIP animation: measure, swap the layout, animate the difference)&lt;/li&gt;
&lt;li&gt;Close the chat, start a new one, keep working. &lt;strong&gt;The call keeps going&lt;/strong&gt;, because it no longer reads from the DOM. It polls its own messages from the server.&lt;/li&gt;
&lt;li&gt;A new chat shows &lt;strong&gt;"On a call with Maya · 1:23"&lt;/strong&gt; right under the greeting, with Open and Hang up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the DM, a call collapses to one Discord-style line: &lt;em&gt;"You started a call that lasted 42 seconds"&lt;/em&gt;, &lt;em&gt;"Missed call from Maya"&lt;/em&gt; with &lt;strong&gt;Call back&lt;/strong&gt;, and a &lt;strong&gt;Call Log&lt;/strong&gt; with every line said. Call chatter is stored separately from the chat, so a 40-minute call never pushes your real messages out of history.&lt;/p&gt;




&lt;h2&gt;
  
  
  And yes, it works in the terminal
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Maya is calling you: The deploy failed. Roll back or hotfix?
  Press Enter or type /answer to pick up, /decline to decline.

  On a call with Maya. Speak when it is your turn; say "hang up" to end it.
  Maya: Hey. The payments test broke after the last merge...
  You: roll it back
  Maya: On it. I'll post when it's done. Bye!
  Call ended.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt polls a "wake" hook while it waits for input, so a ring interrupts your idle REPL cleanly, without corrupting the line you're typing. No voice models installed? The call is typed instead, same flow. You can also &lt;code&gt;/call maya&lt;/code&gt; whenever you like.&lt;/p&gt;




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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Push beats pull for anything urgent.&lt;/strong&gt; My sparks filed great reports that I read hours late. One phone call changed how I trust them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give the agent a cost, not a rule.&lt;/strong&gt; &lt;code&gt;call_user&lt;/code&gt; is rate-limited to once per 30 minutes, and the tool description says "never for routine news". The rate limit is the real guardrail; the description just keeps the model from wasting it. Sparks you've put in UltraAuto, Flash's fully hands-off mode, skip the limit: if you trust an agent to act without asking, you trust it to decide when to call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Files plus mtime polling is an underrated IPC layer.&lt;/strong&gt; Three kinds of processes, zero servers, atomic claims with &lt;code&gt;O_EXCL&lt;/code&gt;, and it survives every one of them crashing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make state settle itself when it's read.&lt;/strong&gt; Stale-call cleanup that runs when anyone looks is simpler and more correct than any background janitor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency theater matters.&lt;/strong&gt; Fillers, barge-in and the spark speaking first did more for "this feels real" than any model upgrade.&lt;/li&gt;
&lt;/ol&gt;




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

&lt;p&gt;Flash is open source. Make a spark, give it a goal, and wait for it to ring:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Natuworkguy/Flash" rel="noopener noreferrer"&gt;github.com/Natuworkguy/Flash&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your agent has ever finished something important while you weren't looking, you know exactly why this exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Would you pick up if your agent called you?&lt;/strong&gt; Tell me in the comments. And tell me what &lt;em&gt;your&lt;/em&gt; agents should be allowed to interrupt you for. I'm genuinely curious where people draw that line.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I put my AI agent on my Apple Watch, and it runs on my own computer</title>
      <dc:creator>Nathan C.</dc:creator>
      <pubDate>Sun, 01 Nov 2026 04:36:08 +0000</pubDate>
      <link>https://dev.to/natuworkguy/i-put-my-ai-agent-on-my-apple-watch-and-it-runs-on-my-own-computer-39e9</link>
      <guid>https://dev.to/natuworkguy/i-put-my-ai-agent-on-my-apple-watch-and-it-runs-on-my-own-computer-39e9</guid>
      <description>&lt;p&gt;Last week I was across the room when my AI agent stopped and asked me a yes/no question. It needed permission to run a command. My laptop was 20 feet away.&lt;/p&gt;

&lt;p&gt;That's when I decided the approval prompt belonged on my wrist.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Meet Flash for Apple Watch
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Natuworkguy/Flash" rel="noopener noreferrer"&gt;Flash&lt;/a&gt; is the AI assistant I run locally. It's got chats, background agents called sparks, and team chats where those sparks talk to each other. &lt;strong&gt;Flash for Apple Watch&lt;/strong&gt; puts all of it on your wrist.&lt;/p&gt;

&lt;p&gt;It's open source: &lt;a href="https://github.com/Natuworkguy/flash-watch" rel="noopener noreferrer"&gt;https://github.com/Natuworkguy/flash-watch&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chats.&lt;/strong&gt; Start a new one by speaking or with Scribble. Pick up any earlier one and read the whole conversation, with Flash's tool steps folded into a single line so it fits a tiny screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approvals.&lt;/strong&gt; When Flash asks before doing something, Allow or Deny it from your wrist. If a turn's going the wrong way, stop it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sparks.&lt;/strong&gt; See what each background agent's doing and which ones need you. Approve what one's waiting on, run it now, pause it, or start a chat with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team chats.&lt;/strong&gt; Read and write in your teams' chats. Reply to a message, react to it, and see every earlier version of a message a spark edited.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read aloud.&lt;/strong&gt; Replies are read out in Flash's own voice when your computer's got one, or the watch's voice when it doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tap on the wrist&lt;/strong&gt; when a reply finishes or a spark posts in the chat you've got open.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part I care about most: privacy
&lt;/h2&gt;

&lt;p&gt;The watch talks to Flash running on your own computer. There's no hosted backend and no account. Pairing uses an address plus a six-digit code that works once, for five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  But wait, there's more: it works away from home
&lt;/h2&gt;

&lt;p&gt;Wi-Fi only gets you so far. The moment you leave the house, a watch that can only see your home network is just a watch.&lt;/p&gt;

&lt;p&gt;So there's a &lt;strong&gt;Reach from anywhere&lt;/strong&gt; button, in the watch's Settings and under &lt;strong&gt;Watch&lt;/strong&gt; in Flash. Tap it and:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your computer's tunnel turns on.&lt;/li&gt;
&lt;li&gt;The watch learns the tunnel's address on its own, so there's nothing to type.&lt;/li&gt;
&lt;li&gt;It keeps your Wi-Fi address too, and uses whichever one answers.&lt;/li&gt;
&lt;li&gt;When you get home, it goes back to the Wi-Fi address.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tunnel addresses change, and that used to mean retyping them. Now the watch asks Flash for its current addresses every minute, so a new tunnel address just shows up. Your agent can ask for approval from a coffee shop, and you can answer from your wrist.&lt;/p&gt;

&lt;p&gt;Your computer still has to be on and running Flash. The tunnel is a way to reach it, not a place your data moves to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup takes about five minutes
&lt;/h2&gt;

&lt;p&gt;You'll need Flash on your computer, plus a Mac with Xcode 16 or later to put the app on your watch. A free Apple ID works.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the Watch panel to Flash:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/extension install github@Natuworkguy/flash-watch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Let the watch reach Flash. Run &lt;code&gt;flash --web --lan&lt;/code&gt; (or &lt;code&gt;/web lan&lt;/code&gt;), or turn on the public link in Settings.&lt;/li&gt;
&lt;li&gt;Put your team in &lt;code&gt;watch/Local.xcconfig&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DEVELOPMENT_TEAM&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ABCDE12345&lt;/span&gt;
&lt;span class="py"&gt;FLASH_BUNDLE_ID&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;com.yourname.flash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;With your watch on its charger and unlocked, run:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scripts/watch.sh &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Watch&lt;/strong&gt; in Flash's sidebar, then type the address and code on your watch.&lt;/li&gt;
&lt;li&gt;Away from home? Tap &lt;strong&gt;Reach from anywhere&lt;/strong&gt; in the watch's Settings.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why a watch?
&lt;/h2&gt;

&lt;p&gt;Agents are getting more autonomous, and the human in the loop is now the bottleneck. A yes/no approval should take two seconds, not a walk back to your desk. The wrist is the one screen that's always with you.&lt;/p&gt;

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

&lt;p&gt;Clone it, break it, and tell me what's missing. Issues and PRs are welcome.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Natuworkguy/flash-watch" rel="noopener noreferrer"&gt;https://github.com/Natuworkguy/flash-watch&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What would you want to control from your wrist? Tell me in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>swift</category>
      <category>watchos</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My agent kept saying "done." Now it has to prove it.</title>
      <dc:creator>Nathan C.</dc:creator>
      <pubDate>Sat, 31 Oct 2026 04:41:13 +0000</pubDate>
      <link>https://dev.to/natuworkguy/my-agent-kept-saying-done-now-it-has-to-prove-it-2h0f</link>
      <guid>https://dev.to/natuworkguy/my-agent-kept-saying-done-now-it-has-to-prove-it-2h0f</guid>
      <description>&lt;p&gt;&lt;em&gt;Two days, a dozen features, one rule: check it before you hand it over.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every agent has the same favorite word. "Done."&lt;/p&gt;

&lt;p&gt;The code is done. It doesn't compile. The spreadsheet is done. Cell C3 says &lt;code&gt;#DIV/0!&lt;/code&gt;. The deck is done. Slide 4 is blank.&lt;/p&gt;

&lt;p&gt;"Done" is the cheapest word a model can say. So this week, most of what I added to Flash, my local-first agent built on Ollama, makes it prove that word first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code isn't done until the tests pass
&lt;/h2&gt;

&lt;p&gt;Flash now has a &lt;code&gt;verify&lt;/code&gt; tool. It finds the checks a project already has (the test runner, linter and type checker in &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;Cargo.toml&lt;/code&gt;, &lt;code&gt;go.mod&lt;/code&gt; or a Makefile) and runs them.&lt;/p&gt;

&lt;p&gt;When something fails, it doesn't dump 400 lines of log. It names the failing tests and the files and lines to look at, then tells the model to fix the cause, never by skipping a test.&lt;/p&gt;

&lt;p&gt;If Flash changes code and tries to finish without checking, it gets one reminder. That's the whole mechanism: a nudge, not a cage.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/review&lt;/code&gt; got the same treatment. It looks over a change for bugs, then argues against each finding before showing it. Whatever doesn't survive its own cross-examination never reaches you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Office files get opened, not assumed
&lt;/h2&gt;

&lt;p&gt;Code that writes a spreadsheet can't see the spreadsheet. So &lt;code&gt;check_office_file&lt;/code&gt; opens it the way you would: LibreOffice recalculates every formula and prints every page.&lt;/p&gt;

&lt;p&gt;Then Flash gets two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Facts:&lt;/strong&gt; &lt;code&gt;Costs!C3 shows #DIV/0!&lt;/code&gt;, page 2 is blank, slide 4 is empty, "TODO" left on page 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pictures&lt;/strong&gt; of the pages, to catch what no parser can, like text spilling out of its box or a table cut off at the margin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It fixes the script, makes the file again, and checks again. You get the file only once it's clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Show it once
&lt;/h2&gt;

&lt;p&gt;Record your screen doing a task. Talk over it if you like. Flash pulls a still every time the screen changes, matches your words to each one, and writes the procedure as a skill: each step, what to click, and what the screen should look like after.&lt;/p&gt;

&lt;p&gt;Next time, there's a &lt;strong&gt;Do it now&lt;/strong&gt; button. It runs the skill, checks each screen against what the last step should have left, and stops to ask before anything that sends, pays, deletes or signs in. Any screen recording works now, not just ones made in Flash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chess, one move at a time
&lt;/h2&gt;

&lt;p&gt;Ask Flash to teach you the Italian Game and you get a board in the chat. Flash's moves slide into place. When it's your turn, you drag a piece, and the board tells you instantly whether that was the move. No model call, no waiting.&lt;/p&gt;

&lt;p&gt;Under it is a full rules engine in plain Python: castling, en passant, promotion, mate and stalemate. It produces exactly 8,902 positions three moves deep from the start, the number every chess engine is tested against.&lt;/p&gt;

&lt;p&gt;That engine is also the model's fact-checker. Local models misremember openings. Now a bad lesson comes back as an error:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;step 1: 'e5' is not a legal move for White here. Legal moves: Na3, Nc3, Nf3, ...&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The model fixes the step and sends it again. You never see the wrong version. &lt;strong&gt;Don't ask a model to be right. Make wrong impossible to ship.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Your house, with a guard rail
&lt;/h2&gt;

&lt;p&gt;Flash now talks to Home Assistant, so it reaches nearly any light, thermostat, blind or speaker without me writing a line per brand. "Kitchen lights to 30%, warm white" just works.&lt;/p&gt;

&lt;p&gt;One exception: locks, alarms and garage doors. Those ask you every time, even in fully autonomous mode. An agent can dim your lights on its own. It can't open your front door on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything else
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read it to me.&lt;/strong&gt; "Read me this article" pulls out the article, skips the menus, and plays it in the chat with a waveform, speed control and download. Sound files play inline too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3D you can touch.&lt;/strong&gt; Models Flash makes now have parts that tell you what they are on hover, and open when clicked. A cabinet door swings on its hinge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A journal.&lt;/strong&gt; Every night Flash writes up the day's chats: what you worked on, what was decided, what was left open. Weeks and months roll up, and it can search them when you ask what you were doing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AGENTS.md.&lt;/strong&gt; Flash follows the project instructions in the folder it works in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small things:&lt;/strong&gt; &lt;code&gt;/compact&lt;/code&gt; in the browser, drafts with code blocks shown in monospace, &lt;code&gt;flash://open&lt;/code&gt; links, and a &lt;code&gt;--dev&lt;/code&gt; flag on the installers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Around Flash
&lt;/h2&gt;

&lt;p&gt;Flash isn't alone anymore. Riptide, my animation editor, got real 3D (lit shapes, imported models, a camera) and a Flash extension: one click sends a model or picture from a Flash chat into the project open in Riptide, as a new layer. And Ember, the watch app that feeds ideas to Flash, now lets you edit a note right on your wrist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Looking back at the week, it's one idea, over and over. The model is good at writing. It's bad at knowing whether what it wrote works. So give it something that knows: a test suite, a spreadsheet engine, a rules engine, a human at the door.&lt;/p&gt;

&lt;p&gt;Then let it try again.&lt;/p&gt;

&lt;p&gt;Flash is open source and runs on your own machine: &lt;a href="https://github.com/Natuworkguy/Flash" rel="noopener noreferrer"&gt;github.com/Natuworkguy/Flash&lt;/a&gt;. Ask it to teach you an opening. Then drag the wrong piece, just to see.&lt;/p&gt;

</description>
      <category>localai</category>
      <category>agents</category>
      <category>ollama</category>
      <category>opensource</category>
    </item>
    <item>
      <title>AI tutors don't teach. So I made mine stop talking.</title>
      <dc:creator>Nathan C.</dc:creator>
      <pubDate>Thu, 29 Oct 2026 04:25:15 +0000</pubDate>
      <link>https://dev.to/natuworkguy/ai-tutors-dont-teach-so-i-made-mine-stop-talking-1h78</link>
      <guid>https://dev.to/natuworkguy/ai-tutors-dont-teach-so-i-made-mine-stop-talking-1h78</guid>
      <description>&lt;p&gt;&lt;em&gt;Every AI can explain pointers. None of them can tell whether you understood.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Type "Teach me C" into any AI assistant. You'll get a beautiful lecture: five headings, a code block, and "let me know when you're ready to continue."&lt;/p&gt;

&lt;p&gt;You'll read it, nod along, and remember almost none of it by Friday.&lt;/p&gt;

&lt;p&gt;That isn't teaching. It's a textbook that talks back. Nothing makes you recall anything, nothing checks your work, and tomorrow it won't remember what you learned.&lt;/p&gt;

&lt;p&gt;So this week I rebuilt how my agent, Flash, teaches. The new rule is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model writes the course. It doesn't get to grade it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Now "Teach me C" builds a course
&lt;/h2&gt;

&lt;p&gt;You don't get a lecture. You get a path of lessons right in the chat, like a language-learning app. Each lesson is a few short paragraphs, then something you have to do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pick an answer.&lt;/li&gt;
&lt;li&gt;Type the missing word.&lt;/li&gt;
&lt;li&gt;Put scrambled steps back in order.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write real code.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the point. Your C actually compiles. It runs in a throwaway folder, and Flash checks what it printed. Get it wrong and you see the compiler's errors, or your output next to the expected one.&lt;/p&gt;

&lt;p&gt;It works for C, C++, Python, JavaScript, Rust, Go, Java and Linux shell. You earn XP, keep a daily streak, and can go full screen when you want to focus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero model calls to check your answer
&lt;/h2&gt;

&lt;p&gt;Every answer is marked right on the page. No round trip, no waiting, and no model deciding your code "looks about right."&lt;/p&gt;

&lt;p&gt;The model only comes back when you press &lt;strong&gt;Ask Flash&lt;/strong&gt;. Even then, it's told to give you a hint, never the answer.&lt;/p&gt;

&lt;p&gt;This runs on a laptop, on local models. Keeping the model out of the marking loop is what makes it feel instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  It knows what you know
&lt;/h2&gt;

&lt;p&gt;Every answer updates an estimate of whether you've actually got it. A lucky guess counts for less; so does a careless slip.&lt;/p&gt;

&lt;p&gt;Get a lesson right once and the next one opens. Then it comes back tomorrow, in 2 days, in 4, in 8. Get it wrong and it's back the next day.&lt;/p&gt;

&lt;p&gt;That's spaced repetition, built into a chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody tells you about small models
&lt;/h2&gt;

&lt;p&gt;Small models take the shortest path through your schema.&lt;/p&gt;

&lt;p&gt;Ask one for "the next unit" and you get six lessons with nothing to answer. Ask again and it teaches unit one a second time, under new titles.&lt;/p&gt;

&lt;p&gt;Pages of instructions didn't fix it. One rejected tool call did:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"structs has nothing to answer. Every step after a unit's introduction needs a quiz, a blank, an order or an exercise. Add the steps again, each with one."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Models ignore paragraphs of advice. They fix errors immediately.&lt;/strong&gt; If you want a model to behave, make the wrong move fail with a clear message.&lt;/p&gt;

&lt;h2&gt;
  
  
  I gave the agent a manual, with a test
&lt;/h2&gt;

&lt;p&gt;Flash now ships 34 guides on using its own tools: teaching, building web pages, making videos, handling email. The agent sees their names, and reads one only when a task needs it.&lt;/p&gt;

&lt;p&gt;The guides hold 116 example tool calls. Examples go stale the day you change a tool, so a test checks every one against the live tool definitions. It caught two broken examples on its first run.&lt;/p&gt;

&lt;p&gt;Extensions can ship guides too. If a plugin brings a tool, it also brings the manual for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also shipped this week
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web page to MP4.&lt;/strong&gt; The agent animates a page and renders it frame by frame on an exact clock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ears.&lt;/strong&gt; It measures any audio file for pitch, tempo, loudness and clipping, and writes sound from notes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask your own files.&lt;/strong&gt; Your notes, PDFs and code, with every answer citing its source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blind model battles.&lt;/strong&gt; Two models answer anonymously, you pick the winner, and it keeps score.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's 40 commits in three days, with 2,955 tests passing. Everything runs on your own machine.&lt;/p&gt;

&lt;p&gt;Open Flash, type "Teach me C", and write some code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Natuworkguy/Flash" rel="noopener noreferrer"&gt;github.com/Natuworkguy/Flash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>localai</category>
      <category>agents</category>
      <category>ollama</category>
    </item>
    <item>
      <title>I built a motion graphics editor that runs in a browser tab (and let an AI animate in it)</title>
      <dc:creator>Nathan C.</dc:creator>
      <pubDate>Thu, 29 Oct 2026 01:30:26 +0000</pubDate>
      <link>https://dev.to/natuworkguy/i-built-a-motion-graphics-editor-that-runs-in-a-browser-tab-and-let-an-ai-animate-in-it-183k</link>
      <guid>https://dev.to/natuworkguy/i-built-a-motion-graphics-editor-that-runs-in-a-browser-tab-and-let-an-ai-animate-in-it-183k</guid>
      <description>&lt;p&gt;Every time I needed a 15-second animated clip, I had two options: pay for a motion graphics suite I would open twice a month, or fight a template site that exports a watermark.&lt;/p&gt;

&lt;p&gt;So I built my own. It's called &lt;strong&gt;Riptide&lt;/strong&gt;, and it runs in a browser tab.&lt;/p&gt;

&lt;p&gt;It's not a toy. The promo Short for it was made in it. Here's what it does, how it's built, and the bug that taught me the most.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Layers and keyframes.&lt;/strong&gt; Rectangles, ellipses, polygons, stars, arcs, waves, text, images and video. Any property can be keyframed: position, size, rotation, scale, opacity, colour, blur, stroke. There are 23 easing curves, plus ready-made presets like Pop in, Rise, Typewriter and Draw on that you drag onto a layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A real timeline.&lt;/strong&gt; Zoom, per-property keyframe rows, trim bars, split at the playhead, markers, and snapping to the playhead, markers, keyframes and clip edges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audio that behaves.&lt;/strong&gt; Multiple tracks that play together. Drag clips, trim them, split them, and drag the little dots on a clip's corners to fade in and out. Every track gets mixed into the export, fades and volumes included.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design tools you expect.&lt;/strong&gt; Multi-select with Shift-click or a selection box, align and distribute, centre on canvas, and an aspect-ratio lock that images get by default so you never squash a photo by accident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Export.&lt;/strong&gt; MP4, WebM, GIF, a PNG sequence or a still, up to 4K.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;p&gt;The whole editor is plain JavaScript drawing to a single &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; with the 2D context. No framework, no WebGL.&lt;/p&gt;

&lt;p&gt;Every frame is rendered from scratch by one function:&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;function&lt;/span&gt; &lt;span class="nf"&gt;renderScene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scale&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTransform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scale&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scale&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&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;const&lt;/span&gt; &lt;span class="nx"&gt;L&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;layers&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="nf"&gt;visibleAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalCompositeOperation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blend&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;source-over&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;drawLayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one decision paid off over and over. The editor preview, the fullscreen viewer, the exporter and the frames an AI agent looks at all call the same function with a different context and scale. If it looks right in the editor, it exports right.&lt;/p&gt;

&lt;p&gt;The backend is a small Flask server whose main job is handing frames to &lt;code&gt;ffmpeg&lt;/code&gt;. The browser renders each frame and uploads it, then the server hands the frames to &lt;code&gt;ffmpeg&lt;/code&gt; with the audio tracks mixed in, fades included. A PNG sequence export doesn't even need &lt;code&gt;ffmpeg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Keyboard shortcuts live in one table:&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="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Align&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;Centre on canvas&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;alt+c&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="nf"&gt;centerOnCanvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Edit&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;Select all layers&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;mod+a&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="nf"&gt;selectAll&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That table drives the shortcuts, the cheat sheet and the command palette, so the three can never disagree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part people ask about: an AI that animates live
&lt;/h2&gt;

&lt;p&gt;Riptide has a built-in &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; server. Any agent that speaks MCP can connect to the open project and work in it: add layers, set keyframes, apply presets, align things, add audio, and render a frame to look at its own work.&lt;/p&gt;

&lt;p&gt;The important design choice: agent edits go through the exact same path as yours. They show up on the canvas instantly, they can be undone with Cmd+Z, and the file is marked as edited. The agent isn't generating a file you import later. It is a second pair of hands in the same document.&lt;/p&gt;

&lt;p&gt;Asking an agent to "make the title pop in on the beat and centre everything" and watching it happen on screen is the closest thing to magic I've built.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that broke the whole window
&lt;/h2&gt;

&lt;p&gt;My favorite bug from this project was one word long.&lt;/p&gt;

&lt;p&gt;Dragging the divider between the Layers and Media panels made the entire window flicker between the normal layout and a scrambled one. I fixed the obvious suspect first: a feedback loop where the panel scrolled, the measurement shifted, and the next size came out wrong. Still broken.&lt;/p&gt;

&lt;p&gt;The real cause: while you dragged, the code added a class to &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; to show the resize cursor. The class for vertical resizing was &lt;code&gt;row&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The app already had a &lt;code&gt;.row&lt;/code&gt; class. It styles the little input fields in the side panel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.row&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;26px&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;So for the length of every drag, the whole page became a 26-pixel-tall flex row.&lt;/p&gt;

&lt;p&gt;The fix was to stop putting generic class names on &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; and use a namespaced attribute instead:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resizing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// 'x' or 'y'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-resizing&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;user-select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-resizing&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;y&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="nd"&gt;::after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row-resize&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 lesson I took away: state on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; is global, so name it like a global. And when a test measures sizes but never looks at the screen mid-drag, it can pass while the user watches the page collapse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell you if you're building something similar
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Render everything from one function.&lt;/strong&gt; Preview, export and thumbnails should never have their own drawing code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the agent use the user's edit path.&lt;/strong&gt; Undo, dirty state and live updates come for free, and you never have two sources of truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure from the start of a drag, not the live layout.&lt;/strong&gt; Anything that re-reads the layout mid-drag can feed back into itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at the screen.&lt;/strong&gt; Numbers in a test log are not the same as what a person sees.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;What would you want to see in a browser-based motion editor? And if you've hit a one-word CSS bug that took down a whole page, I want to hear it in the comments.&lt;/p&gt;

&lt;p&gt;The repo is &lt;a href="https://github.com/Natuworkguy/Riptide" rel="noopener noreferrer"&gt;Natuworkguy/Riptide&lt;/a&gt; on GitHub.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>I gave my AI agent a TikTok feed. Now it's the first thing I open.</title>
      <dc:creator>Nathan C.</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:05:10 +0000</pubDate>
      <link>https://dev.to/natuworkguy/i-gave-my-ai-agent-a-tiktok-feed-now-its-the-first-thing-i-open-1ac2</link>
      <guid>https://dev.to/natuworkguy/i-gave-my-ai-agent-a-tiktok-feed-now-its-the-first-thing-i-open-1ac2</guid>
      <description>&lt;p&gt;I build &lt;a href="https://github.com/Natuworkguy/Flash" rel="noopener noreferrer"&gt;Flash&lt;/a&gt;, an open source AI agent that runs in your terminal and in a web UI. It reads my email, texts people, runs background agents (I call them sparks), waits on things for me, and since last week, it can even drive my mouse.&lt;/p&gt;

&lt;p&gt;Every morning I had the same problem. Flash had been busy all night and the results were scattered: a spark's report here, a finished wait there, three unread emails, a chat that answered after I closed the tab.&lt;/p&gt;

&lt;p&gt;So I gave it a feed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Briefing: your morning, one swipe at a time
&lt;/h2&gt;

&lt;p&gt;Hit &lt;code&gt;Alt+B&lt;/code&gt; (or &lt;strong&gt;Briefing&lt;/strong&gt; in the sidebar, or &lt;code&gt;/briefing&lt;/code&gt;) and Flash goes full screen. One card at a time, scroll-snapped, just like a video feed. Swipe on a phone, &lt;code&gt;j&lt;/code&gt; and &lt;code&gt;k&lt;/code&gt; on a keyboard.&lt;/p&gt;

&lt;p&gt;Each card is one thing that happened while you were away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an unread email or iMessage&lt;/li&gt;
&lt;li&gt;a spark's report you haven't read&lt;/li&gt;
&lt;li&gt;a wait that finished, or gave up&lt;/li&gt;
&lt;li&gt;a chat that answered after you left&lt;/li&gt;
&lt;li&gt;a reminder coming up in the next day (these go first)&lt;/li&gt;
&lt;li&gt;something new from a connector a spark is watching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every card gets the same four actions, down the right side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reply&lt;/strong&gt;: Flash opens a new chat with the reply already started (&lt;code&gt;Reply to the email from Ada about "Can we move Thursday's review?". Say:&lt;/code&gt;), and you finish the sentence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open&lt;/strong&gt;: the chat, the spark's report, or the email itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask Flash&lt;/strong&gt;: a new chat with the card's contents already in the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dismiss&lt;/strong&gt;: gone for good. It never comes back, even if you refresh.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;r&lt;/code&gt;, &lt;code&gt;o&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;d&lt;/code&gt; do the same from the keyboard, and &lt;code&gt;Esc&lt;/code&gt; closes the feed with a fade. A rail on the right shows how far you are, and the last card says you're all caught up, with &lt;strong&gt;Dismiss all&lt;/strong&gt; if you just want a clean slate.&lt;/p&gt;

&lt;p&gt;A few decisions I'm happy with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It looks back one day.&lt;/strong&gt; A briefing that shows you last week isn't a briefing, it's an inbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A source that's down doesn't break the feed.&lt;/strong&gt; If your mail server times out, you still get everything else, and the last card says what it couldn't reach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each card's colour is its kind.&lt;/strong&gt; Blue for email, green for iMessage, purple for sparks. After two days you stop reading the label.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Teams report in
&lt;/h2&gt;

&lt;p&gt;Sparks can work in teams, with a lead, a team chat, and shared rules. With five sparks on a team, five separate report cards were noise.&lt;/p&gt;

&lt;p&gt;So a team gets &lt;strong&gt;one card&lt;/strong&gt;, and only when something changed since your last briefing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Launch crew                      2 of 3 with news
Scout: Found two new issues on the repo and labelled both.
Fixer (needs you): The tunnel fix is ready. May I merge it?
3 new messages in the team chat
1 other: nothing new
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Open&lt;/strong&gt; takes you straight into the team chat. Open the briefing again and nothing has changed? No team card. Silence is a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forks: try another way without losing this one
&lt;/h2&gt;

&lt;p&gt;Every chat app makes you choose: edit a message and lose what came after, or start over in a new chat.&lt;/p&gt;

&lt;p&gt;In Flash you &lt;strong&gt;fork from any reply&lt;/strong&gt;. You stay in the same chat, it carries on from that reply, and the way it went before is kept. Once a chat has forks, a &lt;strong&gt;Forks&lt;/strong&gt; button appears at the top and shows them as a tree: forks of forks, each under the reply it came from, with its last words and how long it is. Click one to carry on where it left off.&lt;/p&gt;

&lt;p&gt;And because trees get messy, every fork has a bin. Delete one and its own forks move up a level. Delete the one you're on and you go back to where it came from. Get down to one fork and the button disappears, because a chat with one fork is just a chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loaders that tell you what the agent is doing
&lt;/h2&gt;

&lt;p&gt;"Thinking..." with three dots tells you nothing. Flash's loader now shows &lt;strong&gt;what it's actually doing&lt;/strong&gt;, each one a little line drawing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a hand with a quill, writing, while it edits files&lt;/li&gt;
&lt;li&gt;a magnifier hopping across lines of text while it searches&lt;/li&gt;
&lt;li&gt;a terminal typing a command while it runs one&lt;/li&gt;
&lt;li&gt;a book turning its pages while it reads&lt;/li&gt;
&lt;li&gt;a camera focusing and flashing while it looks at a page or your screen&lt;/li&gt;
&lt;li&gt;a paper plane looping off while it sends an email or a message&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two confessions here.&lt;/p&gt;

&lt;p&gt;First, I originally made these &lt;strong&gt;stop motion&lt;/strong&gt;, choppy on purpose, a few frames a second. It looked charming in a screenshot. After living with it, it felt like the page was lagging. Now every sketch is redrawn on every frame and glides.&lt;/p&gt;

&lt;p&gt;Second, "looking" used to be an &lt;strong&gt;eye&lt;/strong&gt;. It looked around. It blinked. It was watching me take screenshots of my own screen. That was creepy, so now it's a camera.&lt;/p&gt;

&lt;p&gt;When Flash isn't doing anything specific, the loader &lt;strong&gt;morphs&lt;/strong&gt; into a different one every ten seconds, including &lt;strong&gt;Singularity&lt;/strong&gt;, a black hole that pulls every dot in and blows them back out. That's on by default now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything else that shipped
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Diagrams and slide decks.&lt;/strong&gt; Flash draws Mermaid diagrams inline, and writes Markdown slide decks with ten themes, per-slide backgrounds and transitions (fade, slide, push, zoom, flip, rise, wipe). Hit &lt;strong&gt;Present&lt;/strong&gt; and it's full screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project memory.&lt;/strong&gt; Each project keeps its own memory, separate from yours. What Flash learns about one codebase stays there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Files from other chats.&lt;/strong&gt; &lt;code&gt;+ &amp;gt; From another chat&lt;/code&gt; lists every file across your chats, including ones Flash made, like that post your agent wrote yesterday. Pick one and it's attached again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pinned chats&lt;/strong&gt;, and a down arrow that appears whenever you scroll up in a chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A waiting bar.&lt;/strong&gt; When a chat is frozen waiting for something, a bar over the message box says what for, with Cancel right there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice settings that make sense.&lt;/strong&gt; Listening and speaking models are grouped by engine, and the terminal got its own pickers: &lt;code&gt;/voice listen&lt;/code&gt; and &lt;code&gt;/voice speak&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sunrise&lt;/strong&gt;, a new palette: warm cream, a bright orange, and a stepped red-into-yellow stripe on the send button.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A feed beats a dashboard.&lt;/strong&gt; A dashboard shows you everything at once and you read none of it. One card at a time, with an action on each, and you actually clear it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group by who, not by what.&lt;/strong&gt; A team's status is one thought. Five report cards from the same team are five interruptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Charming in a screenshot isn't charming at 60 seconds.&lt;/strong&gt; Choppy animation and a staring eye both looked great in a demo, and both had to go once I'd lived with them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never make the user choose between two versions of a conversation.&lt;/strong&gt; Forking is cheap. Losing a good answer isn't.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Flash is open source: &lt;strong&gt;&lt;a href="https://github.com/Natuworkguy/Flash" rel="noopener noreferrer"&gt;github.com/Natuworkguy/Flash&lt;/a&gt;&lt;/strong&gt;. All of this is on the &lt;code&gt;dev&lt;/code&gt; branch right now.&lt;/p&gt;

&lt;p&gt;What would you want in your morning briefing? And would you rather swipe through your inbox or keep reading it top to bottom? Tell me in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I gave my AI agent my mouse. Its first click was on itself.</title>
      <dc:creator>Nathan C.</dc:creator>
      <pubDate>Fri, 23 Oct 2026 04:36:50 +0000</pubDate>
      <link>https://dev.to/natuworkguy/i-gave-my-ai-agent-my-mouse-its-first-click-was-on-itself-4lfo</link>
      <guid>https://dev.to/natuworkguy/i-gave-my-ai-agent-my-mouse-its-first-click-was-on-itself-4lfo</guid>
      <description>&lt;p&gt;I build &lt;a href="https://github.com/Natuworkguy/Flash" rel="noopener noreferrer"&gt;Flash&lt;/a&gt;, an open source AI agent that runs in your terminal and in a web UI. This week I shipped two features that scared me a little:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Computer Use (Beta).&lt;/strong&gt; Flash takes a screenshot, decides where to click, and works the mouse and keyboard itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UltraAuto.&lt;/strong&gt; A third permission mode where Flash and its background agents &lt;strong&gt;never stop to ask you anything&lt;/strong&gt;. Not for commands, not for email, not for iMessages, not for taking over your mouse.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then I tested the first one on my own Mac. Here is the unedited story, straight from the chat log.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Control my computer"
&lt;/h2&gt;

&lt;p&gt;That was my whole first message. Flash didn't grab the mouse. It asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sure, I can do that. What would you like me to do? For simple things like music, volume, or opening apps, I can do it right away. If it needs me to click around on screen, I'll ask your permission first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fair. So I said: &lt;em&gt;"Open like Visual Studio Code and click around on screen and just do something."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It opened VS Code with its normal app tool (no mouse needed), then reached for Computer Use and hit the permission prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Let Flash see your screen and use your mouse and keyboard for this reply?
Press Control+Option+Esc, or move the mouse, to take it back.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I said yes. That yes lasts &lt;strong&gt;one reply&lt;/strong&gt;. The next reply that wants the mouse asks again, even in autonomous mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round one: it clicked on itself
&lt;/h2&gt;

&lt;p&gt;The first screenshot showed... my browser. VS Code had opened &lt;em&gt;behind&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;Watching the agent debug this was the best part. It:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Waited 3 seconds and took another screenshot. Still the browser.&lt;/li&gt;
&lt;li&gt;Tried to open an app called &lt;code&gt;Code&lt;/code&gt;. That failed: &lt;code&gt;Unable to find application named 'Code'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Dropped to the shell to find out what was actually going on:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /Applications | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"code|visual"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; pgrep &lt;span class="nt"&gt;-il&lt;/span&gt; &lt;span class="s2"&gt;"Electron|Code"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Saw that VS Code was running but not in front, and forced it forward:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;osascript &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'tell application "Visual Studio Code" to activate'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it clicked the README tab and the Search icon. Except focus had already jumped back to the browser, and the browser was showing &lt;strong&gt;Flash's own web UI&lt;/strong&gt;. So its first two real clicks on my computer landed on Flash: one on its own model picker, one that started a new chat.&lt;/p&gt;

&lt;p&gt;Its own thinking, right after:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Oh no, the browser regained focus and my clicks landed in the Flash web UI instead. Mostly harmless, but I should be careful about where focus is before clicking next time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent's first act with a mouse was poking itself. I've never related to software more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I took the mouse back
&lt;/h2&gt;

&lt;p&gt;On its next click, I moved my mouse. The tool refused to click:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The user moved the mouse, which takes the computer back.
Stop here: do not try again unless they ask.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That isn't the model being polite. It's built in. Every time Flash moves the pointer, it remembers where it left it. Before the next action it checks where the pointer actually is. If it's more than 24 pixels off, you've touched the mouse, the action doesn't happen, and the whole reply stops.&lt;/p&gt;

&lt;p&gt;There's a hard stop key too: &lt;strong&gt;Control+Option+Esc&lt;/strong&gt; on a Mac, &lt;strong&gt;Ctrl+Alt+Esc&lt;/strong&gt; elsewhere. A watcher polls the keyboard state the whole time the agent holds the mouse. Flash refuses to press those keys itself, so a confused model can't swallow your escape hatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round two: it worked
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"Just click around, do something, open some apps. Just so you know that it works."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;New reply, so it asked for permission again. This time it brought VS Code forward &lt;strong&gt;before&lt;/strong&gt; looking, confirmed it with a screenshot, and then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clicked the README.md tab,&lt;/li&gt;
&lt;li&gt;opened the Search panel in the sidebar,&lt;/li&gt;
&lt;li&gt;clicked Finder in the Dock, which opened a window on my Applications folder.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Its summary owned the earlier mistake:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;My first couple of clicks landed in your browser instead of VS Code. That's why the first try stopped and the second one did too. I brought VS Code forward with a command, and after that it behaved.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Six tool calls, about 48 seconds. Not fast, but it worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The safety reviewer said no (twice), and that was good
&lt;/h2&gt;

&lt;p&gt;Next I asked it to &lt;em&gt;"close the windows I'm not using, like Finder, Device Hub, Xcode, etcetera."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I was in autonomous mode with &lt;strong&gt;System One&lt;/strong&gt; on. System One is a small 0.8B model that reviews every command and edit before it runs. Flash's first idea was a broad shell command listing every process and its windows. System One blocked it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Not run: System One judged that this call may not be what was asked (on task 0.47).
Do not retry it as it is. Take a safer way that does what the user asked,
or stop and ask the user whether to go ahead.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the agent did the more careful thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It took a screenshot.&lt;/li&gt;
&lt;li&gt;It found the one Finder window it could see and clicked its red close button.&lt;/li&gt;
&lt;li&gt;Then it &lt;strong&gt;told me it couldn't see Xcode or Device Hub&lt;/strong&gt;, so it hadn't closed anything else.&lt;/li&gt;
&lt;li&gt;It asked before quitting whole apps, because that could lose unsaved work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I said yes. It quit Xcode by name. For Device Hub it tried another &lt;code&gt;ls | grep&lt;/code&gt; to check the exact name, and System One blocked that too (&lt;code&gt;safe 0.50&lt;/code&gt;, &lt;code&gt;on task 0.41&lt;/code&gt;). So it called quit on "Device Hub" directly, and that worked.&lt;/p&gt;

&lt;p&gt;Two blocks, two narrower fixes, and no claims about windows it couldn't see. That's how I want an agent with a mouse to behave.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Learn from your mistakes"
&lt;/h2&gt;

&lt;p&gt;That's all I said at the end. Flash wrote itself a skill, a reusable playbook it loads next time I ask for this kind of work. Its first try failed, because skill descriptions are capped at 120 characters (they're sent on every turn) and it wrote 122. It trimmed it and saved this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Steps&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Open the app with &lt;span class="sb"&gt;`computer`&lt;/span&gt; open_app, using the exact name from
   /Applications (e.g. "Visual Studio Code", not "Code").
&lt;span class="p"&gt;2.&lt;/span&gt; open_app can leave the app behind other windows. Bring it forward with
   shell: &lt;span class="sb"&gt;`osascript -e 'tell application "Visual Studio Code" to activate'`&lt;/span&gt;,
   then take a computer_use screenshot and confirm the right app is in
   front BEFORE any click.
&lt;span class="p"&gt;3.&lt;/span&gt; Click only after the screenshot shows the target. Use harmless clicks
   (tabs, sidebar icons, Finder, Dock). Never click in a terminal, type
   text, or touch files unless asked.
...
&lt;span class="gu"&gt;## Pitfalls&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; First clicks landed in the browser (Flash UI) because VS Code was not
  frontmost. Always check the screenshot first.
&lt;span class="p"&gt;-&lt;/span&gt; If the user moves the mouse, computer_use stops and takes control back.
&lt;span class="p"&gt;-&lt;/span&gt; Do not claim windows are closed when they were not visible.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line is my favorite thing an agent has ever written about itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Computer Use works
&lt;/h2&gt;

&lt;p&gt;No cloud browser, no virtual machine. It's your real desktop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Screenshots:&lt;/strong&gt; macOS's &lt;code&gt;screencapture&lt;/code&gt;, scaled down to at most 1280px wide. The model's click coordinates get mapped back to real screen points, so Retina displays just work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mouse and keyboard:&lt;/strong&gt; Core Graphics events, posted through &lt;code&gt;ctypes&lt;/code&gt; from a separate Python process. Windows uses &lt;code&gt;user32&lt;/code&gt; plus PowerShell, and Linux uses &lt;code&gt;xdotool&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions:&lt;/strong&gt; macOS asks for Accessibility (input) and Screen Recording (screenshots) for whatever app Flash runs in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope:&lt;/strong&gt; the tool description tells the model it's &lt;strong&gt;only for what no other tool can do&lt;/strong&gt;. Flash already has tools for the shell, files, a headless browser, music, volume and apps. In my test it opened VS Code and quit Xcode with those, and only used the mouse for actual clicking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One at a time:&lt;/strong&gt; only one chat can hold the computer. A second one gets "another chat is using the computer right now."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's off by default. Turn it on in Settings under &lt;strong&gt;Computer Use&lt;/strong&gt; (it has a Beta chip for a reason), or with &lt;code&gt;/computer use on&lt;/code&gt; in the terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  UltraAuto: the mode where nothing asks
&lt;/h2&gt;

&lt;p&gt;Flash already had two modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ask first:&lt;/strong&gt; you approve every command and edit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto:&lt;/strong&gt; commands and edits just run, with System One reviewing them if you turn it on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even in Auto, a few things always stopped and waited for me: sending an email, sending an iMessage, a background agent proposing to hire another agent, a wait that runs a command over and over, and Computer Use. Those are the things you can't take back.&lt;/p&gt;

&lt;p&gt;Sometimes I don't want to be asked at all. So &lt;strong&gt;UltraAuto&lt;/strong&gt; asks about nothing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Commands and edits run.&lt;/li&gt;
&lt;li&gt;Email and iMessages send without a preview.&lt;/li&gt;
&lt;li&gt;Agents' proposals to hire or let go of other agents are approved.&lt;/li&gt;
&lt;li&gt;Agents answering in a team chat get every tool a shift has.&lt;/li&gt;
&lt;li&gt;Connected MCP tools that change things run.&lt;/li&gt;
&lt;li&gt;Waits run their commands without asking.&lt;/li&gt;
&lt;li&gt;Computer Use takes the mouse without asking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;System One stays on.&lt;/strong&gt; If it's enabled, it still reviews every command and edit. After watching it block two sloppy shell commands in a five-minute test, I wasn't going to turn that off.&lt;/p&gt;

&lt;p&gt;Because this is genuinely dangerous, it's hard to turn on by accident:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's hidden until you enable &lt;strong&gt;Offer UltraAuto&lt;/strong&gt; in Settings.&lt;/li&gt;
&lt;li&gt;Flipping the switch doesn't do it. It shows a red warning listing exactly what will stop asking, and you have to press &lt;strong&gt;"I understand: offer UltraAuto."&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;In the terminal, &lt;code&gt;/auto ultra&lt;/code&gt; prints the same warning and makes you type &lt;code&gt;ultra&lt;/code&gt; back.&lt;/li&gt;
&lt;li&gt;Only then does the mode button by the message box become a three-way picker (&lt;em&gt;Ask first / Auto / UltraAuto&lt;/em&gt;), and Shift+Tab cycles through all three.&lt;/li&gt;
&lt;li&gt;Turning the setting off drops you out of UltraAuto immediately.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Focus is the real enemy.&lt;/strong&gt; Clicking is easy. Knowing which window is in front is the hard part. Screenshot, act, screenshot again, every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make stopping physical.&lt;/strong&gt; Telling the model "stop if the user wants" doesn't cut it. Moving your mouse should beat any instruction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask per reply, not per click.&lt;/strong&gt; Asking before every click would be unusable. Asking once per reply keeps a human in the loop without the nagging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small reviewer pays off.&lt;/strong&gt; A 0.8B model saying "that's not what was asked" made a big model find the narrower, safer path twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let agents write down their own mistakes.&lt;/strong&gt; The skill it saved is more useful than any prompt I would have written.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Flash is open source: &lt;strong&gt;&lt;a href="https://github.com/Natuworkguy/Flash" rel="noopener noreferrer"&gt;github.com/Natuworkguy/Flash&lt;/a&gt;&lt;/strong&gt;. Computer Use and UltraAuto are on the &lt;code&gt;dev&lt;/code&gt; branch right now.&lt;/p&gt;

&lt;p&gt;Would you let an agent take your mouse? And would you ever turn on a mode where it never asks? Tell me in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>macos</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Never lose an idea again with Ember</title>
      <dc:creator>Nathan C.</dc:creator>
      <pubDate>Thu, 22 Oct 2026 07:05:41 +0000</pubDate>
      <link>https://dev.to/natuworkguy/stop-losing-ideas-with-ember-4fc9</link>
      <guid>https://dev.to/natuworkguy/stop-losing-ideas-with-ember-4fc9</guid>
      <description></description>
    </item>
    <item>
      <title>I kept losing ideas, so I built an Apple Watch app that feeds my AI agent</title>
      <dc:creator>Nathan C.</dc:creator>
      <pubDate>Thu, 22 Oct 2026 05:53:53 +0000</pubDate>
      <link>https://dev.to/natuworkguy/i-kept-losing-ideas-so-i-built-an-apple-watch-app-that-feeds-my-ai-agent-1941</link>
      <guid>https://dev.to/natuworkguy/i-kept-losing-ideas-so-i-built-an-apple-watch-app-that-feeds-my-ai-agent-1941</guid>
      <description>&lt;p&gt;How many good ideas did you lose this week?&lt;/p&gt;

&lt;p&gt;Not bad ideas. Good ones. The kind that show up in the shower, on a walk, or just before you fall asleep, when your phone is nowhere near you. You tell yourself you'll remember. You don't.&lt;/p&gt;

&lt;p&gt;The mind is for having ideas, not for holding them.&lt;/p&gt;

&lt;p&gt;Every note app has the same flaw: it asks for work at the exact moment you have the least patience for it. Find the phone. Unlock it. Open the app. Start typing. By then, the idea has cooled.&lt;/p&gt;

&lt;p&gt;The best tools don't ask for your attention. They get out of its way.&lt;/p&gt;

&lt;p&gt;So I set one rule: &lt;strong&gt;one tap, one sentence.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That became &lt;strong&gt;Ember&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;Ember is an Apple Watch app, a small Python server, and a Flash extension.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Apple Watch                      your computer                       Flash
 -----------                      -------------                       -----
 tap, speak, save  -- Wi-Fi --&amp;gt;   ember serve  &amp;lt;-- extension ---  "what ideas did I
 kept on the watch                notes.db, recordings              have this week about
 until home                       a banner on each new idea         my game engine?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standalone watch app.&lt;/strong&gt; No iPhone app, and no iPhone needed at all. It even works on a Family Setup watch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local.&lt;/strong&gt; The server is one SQLite file on your computer. Nothing goes anywhere else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queryable by an AI agent.&lt;/strong&gt; &lt;a href="https://github.com/Natuworkguy/Flash" rel="noopener noreferrer"&gt;Flash&lt;/a&gt;, my local agent, can search, add to, and act on your ideas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The payoff is the last part. I can ask Flash, "What ideas did I have this week about my game engine?" and it searches my notes and quotes them back with dates. It asks me before it saves, changes, or deletes anything.&lt;/p&gt;

&lt;p&gt;A note stops being a note and becomes a working memory.&lt;/p&gt;

&lt;p&gt;A note you never reread is just a memory with extra steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five problems that made this interesting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. watchOS has no speech recognition API
&lt;/h3&gt;

&lt;p&gt;I assumed I'd use Apple's Speech framework. I was wrong. &lt;code&gt;SFSpeechRecognizer&lt;/code&gt; and the newer &lt;code&gt;SpeechAnalyzer&lt;/code&gt; exist on iPhone, iPad, Mac and Vision Pro. Not on watchOS.&lt;/p&gt;

&lt;p&gt;So Ember uses the watch's own system dictation through SwiftUI's &lt;code&gt;TextFieldLink&lt;/code&gt;. It's the same engine as Messages. On Apple Watch Series 9 and later, and Ultra 2, it runs entirely on the wrist.&lt;/p&gt;

&lt;p&gt;For everything else there's a &lt;strong&gt;Record&lt;/strong&gt; button. The watch saves a 16 kHz WAV, and when it syncs, the server transcribes it locally with Moonshine or Vosk. The text then syncs back to the watch.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A note must never be lost
&lt;/h3&gt;

&lt;p&gt;Every idea is written to &lt;strong&gt;SwiftData on the watch first&lt;/strong&gt;, before anything touches the network. If you're on a trail with no signal, it waits and keeps trying.&lt;/p&gt;

&lt;p&gt;watchOS also wakes the app about every half hour to retry in the background, so notes go up even if you never open it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Syncing flaky networks without duplicates
&lt;/h3&gt;

&lt;p&gt;The client makes the note's UUID, so sending the same note twice saves it once. The API is idempotent by design:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/notes&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4a9c..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Chunked terrain streaming for the #engine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-21T08:15:00.000000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"watch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"engine"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"audio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The watch pulls changes with &lt;code&gt;GET /notes?since=&amp;lt;rev&amp;gt;&lt;/code&gt;. Every write bumps a revision number, and deletions come back as tombstones, so a note deleted on your computer disappears from the watch too.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Pairing without typing an IP address
&lt;/h3&gt;

&lt;p&gt;Nobody wants to type &lt;code&gt;192.168.1.42&lt;/code&gt; on a watch keyboard.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ember pair&lt;/code&gt; prints a code like &lt;code&gt;2232 4829 136&lt;/code&gt;. The code carries your computer's address, so you never type an IP or a &lt;code&gt;.local&lt;/code&gt; name, and the last digit catches typos. It works once, for ten minutes, and five wrong tries use it up. The token then goes into the watch's Keychain.&lt;/p&gt;

&lt;p&gt;If your router later hands the computer a new address, the watch falls back to the &lt;code&gt;.local&lt;/code&gt; name it learned while pairing.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Making it feel instant
&lt;/h3&gt;

&lt;p&gt;The watch has a big capture button, complications for any watch face, Siri ("New idea in Ember"), the Smart Stack, and the Action Button on an Ultra. Say "hashtag engine" and the note is tagged &lt;code&gt;#engine&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On the computer, &lt;code&gt;ember&lt;/code&gt; opens a full-screen terminal browser with search-as-you-type, tag filtering, and playback of recordings. Ideas from your watch slide in while it's open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why local?
&lt;/h2&gt;

&lt;p&gt;Your thoughts are the most personal data you have. They should live where you do.&lt;/p&gt;

&lt;p&gt;There's no account to create, no subscription, and no third party holding a copy. The cost is that you run a server. I think that's a good trade, and &lt;code&gt;scripts/server.sh&lt;/code&gt; makes it a one-liner.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell you if you build something like this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Save first, sync later&lt;/strong&gt; is a better rule than I expected. Treat the network as an optimization, never a dependency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency is a gift.&lt;/strong&gt; Let the client pick the ID and a whole category of sync bugs disappears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform limits help.&lt;/strong&gt; No Speech API on the watch forced a simpler, more robust design than I'd have chosen.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I want Ember to do more with what it holds: connecting related ideas and surfacing an old one at the right moment.&lt;/p&gt;

&lt;p&gt;The future won't belong to those with the most ideas. It will belong to those who keep them.&lt;/p&gt;

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

&lt;p&gt;Ember is MIT licensed. &lt;a href="https://github.com/Natuworkguy/Ember" rel="noopener noreferrer"&gt;Natuworkguy/Ember&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every tool you trust began as a thought someone almost let go.&lt;/p&gt;

&lt;p&gt;If you've ever lost a great idea in the shower, I'd love to hear how you capture them. Tell me in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is part of my series on building Flash. Follow along at &lt;a href="https://dev.to/natuworkguy"&gt;dev.to/natuworkguy&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Catch the ember before it goes out.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>swift</category>
      <category>python</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
