<?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: Dor Lugasi-Gal</title>
    <description>The latest articles on DEV Community by Dor Lugasi-Gal (@dorlugasigal).</description>
    <link>https://dev.to/dorlugasigal</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%2F2871459%2F93e430cf-ea88-44c6-886a-7a9a44470699.png</url>
      <title>DEV Community: Dor Lugasi-Gal</title>
      <link>https://dev.to/dorlugasigal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dorlugasigal"/>
    <language>en</language>
    <item>
      <title>My PR demo videos make themselves now, in my voice</title>
      <dc:creator>Dor Lugasi-Gal</dc:creator>
      <pubDate>Wed, 04 Nov 2026 09:54:08 +0000</pubDate>
      <link>https://dev.to/dorlugasigal/my-pr-demo-videos-make-themselves-now-in-my-voice-505i</link>
      <guid>https://dev.to/dorlugasigal/my-pr-demo-videos-make-themselves-now-in-my-voice-505i</guid>
      <description>&lt;p&gt;&lt;em&gt;How I built a free pipeline that runs on my MacBook: it films the real app, narrates in a clone of my voice and can put a lip-synced webcam tile of me in the corner. Plus the benchmarks, the bugs, and the 23-minute render that nearly killed the whole idea.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc8wfn92dzhg6815oiury.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc8wfn92dzhg6815oiury.gif" alt="A script draws on the Annotator website while a small webcam tile of me talks in the bottom-left corner." width="760" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nothing in this clip was recorded live. A script moves the cursor, the voice is a clone of mine, and the tile in the corner is a two-minute webcam take of me with new lips. &lt;a href="https://youtu.be/emS83GzNiHM" rel="noopener noreferrer"&gt;Full 76-second video&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every PR in my projects ships with a one-to-three-minute video of the real app, narrated in my voice. I don't record them anymore.&lt;/li&gt;
&lt;li&gt;I'm cheap, so it's all free and runs locally on one MacBook Pro: a voice clone (Voicebox), a lip-synced webcam tile (MuseTalk), and a screen recording driven through macOS accessibility APIs.&lt;/li&gt;
&lt;li&gt;The avatar used to take 23 minutes to render one minute of video. It now takes about one.&lt;/li&gt;
&lt;li&gt;Most of the time went into unglamorous bugs: pauses in the audio, a toolbar that hides itself after three seconds, and my Hebrew keyboard layout.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I wanted this
&lt;/h2&gt;

&lt;p&gt;Most of my PRs are written with a coding agent, usually on autopilot. A demo video in the PR makes it much easier to understand what the author and their agent were going for. Bigger PRs stop being scary, and you can see the before and after for yourself instead of picturing it from the diff.&lt;/p&gt;

&lt;p&gt;Before AI coding, when you didn't understand someone's change, you'd call them and ask them to show you. A demo video does exactly that, without the call.&lt;/p&gt;

&lt;p&gt;Recording them myself didn't scale. With agents writing the code, sometimes I can't keep up with my own PRs. On top of that I had to prepare the environment, record, edit, add zooms and do it all again after every fix. Now the agent does that part.&lt;/p&gt;

&lt;p&gt;The obvious answer was a hosted avatar product like HeyGen. It would look better than anything in this post. But I'm cheap, and I didn't want to pay for it. So I set four rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free.&lt;/strong&gt; No subscriptions, no credits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local.&lt;/strong&gt; My face and voice don't go to anyone's servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mac only.&lt;/strong&gt; My work machine is a MacBook Pro with an M4 Max and 36 GB of memory. There's no NVIDIA GPU, so anything that needs CUDA was out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real app.&lt;/strong&gt; No mock-ups and no clips cut from a marketing site. If the video shows a click, the app got clicked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I didn't write most of the code. I worked with a coding agent (GitHub Copilot) that had computer use on my Mac. It installed the models, ran the benchmarks, drove the app and showed me what came out. My job was to watch and say what was wrong. A lot of this post is that list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9de6lq70n280ij7dt5va.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9de6lq70n280ij7dt5va.png" alt="Pipeline: a script goes to the voice clone and to the screen take; the voice drives the avatar and gives timing cues to the screen take; the screen take and avatar are composed into the final demo video." width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One script in, one video out. The voice's word timings tell the screen recording when to act.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: The face
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Hosted tools hit a wall fast
&lt;/h3&gt;

&lt;p&gt;Hosted avatar products like HeyGen look great, and their free plans are small. When I checked, HeyGen's free plan gave you three one-minute videos a month with a watermark. They also need your face on their servers. That broke two of my rules on day one, so I looked for models that run on a laptop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three lip-sync models, one clip, one scoreboard
&lt;/h3&gt;

&lt;p&gt;Lip-sync means you start from a real video of yourself and regenerate only the mouth to match new audio. Your face, your room and your head movement stay real. I picked three models that can run on Apple Silicon and gave each the same 17-second clip of me (cut from a tech talk I'd given) and the same voice track.&lt;/p&gt;

&lt;p&gt;I didn't want to judge by eye alone, so the agent scored them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SyncNet confidence:&lt;/strong&gt; a model trained to spot out-of-sync video rates how sure it is that lips and audio match. Higher is better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offset:&lt;/strong&gt; how many frames the lips run ahead of or behind the voice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Still looks like me:&lt;/strong&gt; face-recognition similarity to the original. 1.0 means identical.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;SyncNet confidence&lt;/th&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Still looks like me&lt;/th&gt;
&lt;th&gt;Time for the 17 s clip&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No lip-sync (baseline)&lt;/td&gt;
&lt;td&gt;0.38&lt;/td&gt;
&lt;td&gt;11 frames&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MuseTalk 1.5&lt;/td&gt;
&lt;td&gt;2.66&lt;/td&gt;
&lt;td&gt;1 frame&lt;/td&gt;
&lt;td&gt;0.92&lt;/td&gt;
&lt;td&gt;96 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FaceFusion (EDTalk)&lt;/td&gt;
&lt;td&gt;4.65&lt;/td&gt;
&lt;td&gt;3 frames (120 ms)&lt;/td&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;td&gt;92 s, CPU only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LatentSync 1.5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.55&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.97&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;about 9 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7pt7s85myri5obo04djj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7pt7s85myri5obo04djj.gif" alt="The same clip side by side: no lip-sync, MuseTalk 1.5, FaceFusion EDTalk and LatentSync 1.5, with mouth close-ups and scores under each." width="599" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Same clip, same voice, three models. (It's a GIF, so no sound.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The numbers matched what I saw. This chart made it obvious:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnxo3vkbar86fuic12cm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnxo3vkbar86fuic12cm.png" alt="Four rows, one per model. Blue shows voice loudness, red shows how open the mouth is. With no lip-sync and with MuseTalk the red line is almost flat; FaceFusion twitches; LatentSync opens and closes along with the voice and closes on p, b and m sounds." width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Red is how far my mouth opens, blue is how loud the voice is. Dotted lines mark words starting with p, b or m, where lips have to close.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;LatentSync won. Its newer 1.6 version works at 512 px, but one attention step alone needed about 8.6 GB and my Mac started swapping, so I settled on 1.5 at 256 px with a few small patches to run on Apple's GPU.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recording the source take
&lt;/h3&gt;

&lt;p&gt;The model needs footage of you to put new lips on. The agent built me a small recording page with a hands-free teleprompter: a neutral pause, a greeting, a bit of explaining, 30 seconds of just listening, and a sign-off. Each part gives the avatar different raw material to draw from.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flerxced4tc6su5nfk5s5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flerxced4tc6su5nfk5s5.png" alt="The recording kit page: three shirts, about three minutes each, with a setup checklist." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I recorded two takes in two shirts. Then it turned out the microphone had recorded pure noise, because QuickTime had picked the wrong input. It didn't matter. The model replaces the mouth anyway, and for timing the agent read my lip movement straight from the video with face landmarks instead of from the audio.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting the head to move with the voice
&lt;/h3&gt;

&lt;p&gt;Good lips weren't enough. My head would nod during a pause in the new voice and sit still in the middle of a sentence, which looks like a bad dub.&lt;/p&gt;

&lt;p&gt;The fix was to nudge the source footage's speed a little (between 0.85× and 1.17×) and line up pauses in the new voice with moments where I'd paused on camera. One number tracks it: the correlation between voice loudness and mouth activity. It went from −0.08 without the nudge to 0.40 with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feedback that shaped the look
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Circle to webcam tile.&lt;/strong&gt; A round bubble looked like a sticker. A rounded 16:9 tile showing head, shoulders and a bit of room looks like someone on a call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Colour.&lt;/strong&gt; My camera's white balance turned a black T-shirt green. The fix is baked into the render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No nodding.&lt;/strong&gt; Nodding while nobody is talking reads as agreeing with no one. The avatar only uses the calmest stretch of the take.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smaller, twice.&lt;/strong&gt; The tile went from 352 px to about 260 px wide on a 1920 px video.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Part 2: The voice
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://voicebox.sh" rel="noopener noreferrer"&gt;Voicebox&lt;/a&gt; is an open-source app that clones a voice from a short sample and runs it locally. My first clone came from a meeting recording and sounded flat, a bit robotic. My second came from four lively bits of a tech talk where I was presenting, and the difference was huge. That's the one I use.&lt;/p&gt;

&lt;p&gt;What I learned about getting a cloned voice to sound like me on a good day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Style instructions don't work on a cloned voice.&lt;/strong&gt; "Say this enthusiastically" did nothing. Livelier lines did: a couple of exclamation marks, shorter sentences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate the whole script in one go.&lt;/strong&gt; Line by line, the energy jumped around: an excited first line, a sleepy third one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate several takes and measure them.&lt;/strong&gt; The agent makes six takes and keeps the one that matches the script word for word (checked with a transcript), has the most movement in pitch and loudness, and drops in pitch on the last word so the ending sounds finished.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read every transcript.&lt;/strong&gt; One take said "and then do anything" instead of "undo anything". That one sentence got swapped in from another take.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't respell names.&lt;/strong&gt; Spelling "Annotator" phonetically to help the model backfired: it came out sounding French. The plain spelling plus picking the best take worked better.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Three bugs hiding in the pauses
&lt;/h3&gt;

&lt;p&gt;The screen needs room between sentences, so the voice gets short pauses. Those pauses caused three bugs in a row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 1: digital silence.&lt;/strong&gt; The first version inserted silence between sentences. My feedback: "the hard pauses are too snappy". The sound dropped from my voice to absolute zero, and real rooms are never that quiet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 2: the stuck record.&lt;/strong&gt; The next version filled the gaps by looping a slice of the quiet bit between words. "It sounds snappy, repeated, like a stuck disk." Short loops are easy to hear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 3: clipped words.&lt;/strong&gt; Sentences were cut at the word edges a speech-to-text model (Whisper) gave us. In flowing speech those edges drift by about 0.3 seconds, so word endings got chopped.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fls3qr6qn2oz2ggahp95m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fls3qr6qn2oz2ggahp95m.png" alt="Two audio level plots around the end of a sentence. Before: the level falls off a cliff to digital silence. After: it settles to a steady low room tone, then rises into the next word." width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The version that works cuts at the quietest point measured in the audio itself, fills gaps with smooth, non-repeating noise at about −66 dB, and never loops. An automated check now flags any drop of more than 30 dB within 15 ms anywhere in the track.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: The screen
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why it has to be the real app
&lt;/h3&gt;

&lt;p&gt;An early version stitched together clips from the product website. I rejected it: it had to be a recording of the installed app. The website shows what the app should do. A recording shows that it does.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the computer drives itself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A separate screen.&lt;/strong&gt; &lt;a href="https://github.com/Stengo/DeskPad" rel="noopener noreferrer"&gt;DeskPad&lt;/a&gt; creates a virtual 1920×1200 display. The recording happens there, so my real screens stay mine, and ffmpeg records only that display.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buttons by name.&lt;/strong&gt; A small Swift tool posts mouse and keyboard events. It finds controls through the macOS accessibility API by their names ("Pen", "Arrow", "Laser pointer") instead of pixel positions, so a moved button doesn't break the take.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cursor that moves like a hand.&lt;/strong&gt; Moves take longer for far or small targets (Fitts's law), overshoot slightly and follow a curve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing from the voice.&lt;/strong&gt; The voice comes with word timings, so the plan can say "draw the box when I say &lt;em&gt;box&lt;/em&gt;":
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;at 16.20        # a beat before the word "box"
tool Pen
path 0.8 -1528 512 -1450 506 -1360 505 ...
mark box        # log when it really happened
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every action logs when it really happened. If any lands more than about half a second from its word, the agent records another take. The first version took about 15 takes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnq55hea7cj8c353ffwkx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnq55hea7cj8c353ffwkx.jpg" alt="Six frames from one take, each captioned with the words being spoken: a messy box around the download button, a curvy arrow and a &amp;quot;Start here!&amp;quot; label, a laser loop around the Applications icon, numbered steps, the arrow turning blue, and the page scrolled back to the top." width="799" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Six moments from one take, with what the voice is saying at each.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What broke
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I saw&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stray "4" and "5" counters later in the video&lt;/td&gt;
&lt;td&gt;The toolbar hides itself after 3 seconds. A click meant for it hit the page while the counter tool was still on.&lt;/td&gt;
&lt;td&gt;Check the button is still there right before clicking. Switch back to Select straight after the last counter.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Colour keys did nothing, the arrow stayed blue&lt;/td&gt;
&lt;td&gt;My keyboard was on Hebrew, and the app's shortcuts read the typed character.&lt;/td&gt;
&lt;td&gt;The recorder won't start unless the layout is English.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every click missed&lt;/td&gt;
&lt;td&gt;I'd rearranged my monitors overnight.&lt;/td&gt;
&lt;td&gt;A setup script finds the virtual display and shifts every coordinate.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The laser pointer looked robotic&lt;/td&gt;
&lt;td&gt;Constant speed between points makes sharp corners.&lt;/td&gt;
&lt;td&gt;About 120 points along an eased curve, with equal time per point.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Things changed like magic&lt;/td&gt;
&lt;td&gt;Fast moments use keyboard shortcuts, which you can't see.&lt;/td&gt;
&lt;td&gt;Show the keys on screen as keycaps.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Undo looked like a glitch&lt;/td&gt;
&lt;td&gt;Undoing a move made the arrow snap back.&lt;/td&gt;
&lt;td&gt;Undo a throwaway scribble instead.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The label landed 2 seconds late&lt;/td&gt;
&lt;td&gt;A trip to the toolbar costs about 2 seconds.&lt;/td&gt;
&lt;td&gt;Use the keyboard shortcut.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq1mpfvxl8zoo8yijbryf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq1mpfvxl8zoo8yijbryf.png" alt="Four keycaps: control + 2 &amp;quot;close&amp;quot;, ⌘ + Z &amp;quot;undo&amp;quot; (pressed), L &amp;quot;laser&amp;quot; and B &amp;quot;blue&amp;quot;." width="760" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Keycaps appear at the right edge when a shortcut is pressed, so nothing changes by magic.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few smaller touches came from watching drafts. A card with the URL slides up when the voice says it. Captions come from the voice's word timings. The video ends on the website's promo reel, synced through Chrome's DevTools Protocol, so the last shot lands on "Draw over any app."&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 4: The 23-minute problem
&lt;/h2&gt;

&lt;p&gt;LatentSync's quality came at a price. It runs 20 diffusion steps per frame, and one minute of video took 23 minutes to render. I said it plainly at the time: "Even 23 minutes for 1 minute is too much. Sometimes we render 10 minutes and I don't want to wait 5 hours."&lt;/p&gt;

&lt;p&gt;So I went back to MuseTalk. It had the weakest lips in the benchmark, but it makes each frame in one network pass instead of 20 steps. Profiling it showed most of the time wasn't in the model at all: turning its output back into pixels took about 70% of GPU time. Three changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Swap the full image decoder for &lt;a href="https://github.com/madebyollin/taesd" rel="noopener noreferrer"&gt;TAESD&lt;/a&gt;, a tiny decoder built for the same kind of output.&lt;/li&gt;
&lt;li&gt;Prepare the face once per take (landmarks, encoded frames, blend masks) and cache it.&lt;/li&gt;
&lt;li&gt;Write frames straight into the video encoder in a single pass, with no files in between.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqj26anrbgq6xpntgm8ro.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqj26anrbgq6xpntgm8ro.png" alt="Bar chart of minutes of compute to render one minute of avatar video: LatentSync 1.5 23.5 minutes, MuseTalk 1.5 with the stock decoder 2.6 minutes, MuseTalk 1.5 with the tiny decoder and cache 1 minute." width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Minutes to render 1 minute of video&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LatentSync 1.5, 20 diffusion steps&lt;/td&gt;
&lt;td&gt;23.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MuseTalk 1.5, stock decoder&lt;/td&gt;
&lt;td&gt;2.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MuseTalk 1.5, tiny decoder and cache&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 59-second video now renders in 61 seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5n3on9rb1ygzy75cj3e4.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5n3on9rb1ygzy75cj3e4.gif" alt="Left: LatentSync, 23 minutes to render this minute. Right: fast MuseTalk, 61 seconds. Below each, the same video at the real tile size." width="719" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Old renderer on the left, new on the right, the same ten seconds at full size and at tile size. (No sound in the GIF.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At full size, the fast version's lips are softer. At the size of the tile, nobody noticed. The slow model is still there behind a &lt;code&gt;--quality best&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;In the benchmark I judged the models full screen. The product is a 260-pixel tile. If I'd judged at the size people actually see, I'd have picked the fast model a week earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 5: How I use it on PRs
&lt;/h2&gt;

&lt;p&gt;The pipeline exists for pull requests. These are the rules I settled on, most of them from me watching a video and saying "no, not like that":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every PR gets a demo in its description&lt;/strong&gt;, without anyone asking. Stacked PRs each get their own, showing only their own changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open on the app.&lt;/strong&gt; No title slide, no agenda and no "Hey". End on a plain "Thanks".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show, then explain.&lt;/strong&gt; The real app in the browser, the cursor working, my voice explaining as it goes, like showing a colleague.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One short zoom-out, only when it's needed.&lt;/strong&gt; For things you can't see in the UI: where new data is saved, who can read it, a new API route or background job. Taken from the code, not from memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Before and Now side by side&lt;/strong&gt; for anything visible. The agent runs the base branch from a second worktree on another port, with the same demo data, the same clicks and the same pace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain, casual English.&lt;/strong&gt; Every script goes through a no-AI-slop pass before the voice is generated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo data only.&lt;/strong&gt; A made-up demo user, never real client data, and nothing from the recording gets committed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short and light.&lt;/strong&gt; About a minute per main change and two to four minutes in total, at −16 LUFS loudness and under 90 MB so GitHub plays it inline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The PR description stays short. My words to the agent were roughly "only the intent, what's necessary for a human to read, and the visuals":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Two or three lines: what changed and why.
Figma: &lt;span class="nt"&gt;&amp;lt;link&amp;gt;&lt;/span&gt;

&lt;span class="gs"&gt;**Demo**&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;video&amp;gt;&lt;/span&gt;

|                | Before    | After     |
| -------------- | --------- | --------- |
| Template editor | &lt;span class="nt"&gt;&amp;lt;image&amp;gt;&lt;/span&gt;  | &lt;span class="nt"&gt;&amp;lt;image&amp;gt;&lt;/span&gt;   |
| PDF export      | &lt;span class="nt"&gt;&amp;lt;image&amp;gt;&lt;/span&gt;  | &lt;span class="nt"&gt;&amp;lt;image&amp;gt;&lt;/span&gt;   |

&lt;span class="gs"&gt;**Notes for reviewers**&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; the one or two things worth a closer look
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;It isn't free in tokens.&lt;/strong&gt; A full demo cost the agent 28 to 99 million tokens (about 59 million on average) and 44 to 128 minutes. An updated screenshot costs about 2 million tokens and five minutes. So after a review fix, the default is a new screenshot in the Before | After table. The agent re-records one beat only when a fix changes what the video shows, and the whole video only when I ask. Recording, voice and rendering also load the CPU and GPU, so only one demo runs on my machine at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The avatar is off by default for PR demos.&lt;/strong&gt; My voice stays on. A face on screen means something when you're showing stakeholders, not when a reviewer is checking a change. Leaving it off also saves the processing power when it isn't needed. The same skill makes complete demo videos too, and that's where the avatar comes in: sprint demos and public videos like the Annotator one above, where a human face is worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Judge at the size people watch.&lt;/strong&gt; It would have saved the whole LatentSync detour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record the source take with a working mic.&lt;/strong&gt; Clean audio from the same session would have made a better voice clone than an old meeting recording.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume the UI will move.&lt;/strong&gt; Toolbars hide, keyboards switch layouts and monitors get rearranged. Check the state before every step instead of trusting coordinates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I still have an open question. The avatar loops back and forth over the same calm stretch of footage, which is fine for two minutes, but I don't have a good answer yet for natural head movement in a 10-minute video. If you've solved that on a laptop, I'd love to hear how.&lt;/p&gt;

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

&lt;p&gt;Everything here is free and runs on a Mac with Apple Silicon:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Voice:&lt;/strong&gt; &lt;a href="https://voicebox.sh" rel="noopener noreferrer"&gt;Voicebox&lt;/a&gt; (Qwen3-TTS 1.7B).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lip-sync:&lt;/strong&gt; &lt;a href="https://github.com/TMElyralab/MuseTalk" rel="noopener noreferrer"&gt;MuseTalk 1.5&lt;/a&gt; with the &lt;a href="https://github.com/madebyollin/taesd" rel="noopener noreferrer"&gt;TAESD&lt;/a&gt; decoder. &lt;a href="https://github.com/bytedance/LatentSync" rel="noopener noreferrer"&gt;LatentSync&lt;/a&gt; if you have the time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recording:&lt;/strong&gt; &lt;a href="https://github.com/Stengo/DeskPad" rel="noopener noreferrer"&gt;DeskPad&lt;/a&gt; for a virtual display, &lt;code&gt;ffmpeg&lt;/code&gt; to record it, and macOS accessibility APIs to drive the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The app in the demo:&lt;/strong&gt; &lt;a href="https://getannotator.app" rel="noopener noreferrer"&gt;Annotator&lt;/a&gt;, a free screen-drawing app for Mac and Windows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the full Annotator video from the top of the post:&lt;/p&gt;

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

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>showdev</category>
      <category>codereview</category>
    </item>
    <item>
      <title>One app, two native codebases: how I keep Swift and C# in step, and ship both from one commit</title>
      <dc:creator>Dor Lugasi-Gal</dc:creator>
      <pubDate>Fri, 23 Oct 2026 08:32:18 +0000</pubDate>
      <link>https://dev.to/dorlugasigal/one-app-two-native-codebases-how-i-keep-swift-and-c-in-step-and-ship-both-from-one-commit-4k5b</link>
      <guid>https://dev.to/dorlugasigal/one-app-two-native-codebases-how-i-keep-swift-and-c-in-step-and-ship-both-from-one-commit-4k5b</guid>
      <description>&lt;p&gt;Annotator is a free, open-source app for drawing on your screen while you explain something. You pick a tool from a small floating toolbar and draw over any app: circle a number, bend an arrow to a button, highlight a line. The marks stay on screen while you keep working in the app underneath, and you can move, restyle or undo them later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foy5ykq9d01k6v0a3g29s.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foy5ykq9d01k6v0a3g29s.jpg" alt="Annotator's toolbar under a sample page: a hand-drawn ellipse around a time and a highlighted line" width="800" height="775"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Arrows are placed point by point, so you can bend them around things. Smart Draw turns a quick sketch into a neat box or arrow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwtk1p14djhrft48tsu17.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwtk1p14djhrft48tsu17.jpg" alt="Annotator's toolbar editing a curved arrow from a Draft box to a Review ellipse" width="800" height="626"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also zooms around the pointer, puts a spotlight on it, blurs a busy picture for a moment, and shares one part of your screen in a meeting with your drawings in it. There are about 40 tools in all, and there's a short demo on &lt;a href="https://getannotator.app" rel="noopener noreferrer"&gt;getannotator.app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvag5lck2pp2qbgabbda1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvag5lck2pp2qbgabbda1.jpg" alt="The spotlight settings in Annotator's toolbar" width="784" height="564"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No account, nothing leaves your computer, and usage counts are off unless you turn them on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why two native apps
&lt;/h3&gt;

&lt;p&gt;It started on the Mac in Swift. When people asked for Windows, I had to decide between a cross-platform UI toolkit and a second native app.&lt;/p&gt;

&lt;p&gt;I went native twice. The overlay has to sit above every other window, pass clicks through to the app underneath, and read the screen for zoom and blur. Those are the parts where each OS has its own rules. So the Mac app is Swift, AppKit and Metal, and the Windows app is C# on .NET 10 with WinUI 3, compiled ahead of time with Native AOT.&lt;/p&gt;

&lt;p&gt;Two codebases drift apart. Here's what keeps them together, and how one GitHub Actions run ships both.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Mac is the reference
&lt;/h3&gt;

&lt;p&gt;Every behavior is defined by the Mac app first. The Windows app matches it, and "matches" has to be something a test can check, not a feeling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A conformance corpus.&lt;/strong&gt; A small Swift tool, &lt;code&gt;ConformanceExport&lt;/code&gt;, runs the Mac's annotation core and writes the results to &lt;code&gt;reference/conformance&lt;/code&gt;: how a shape is stored, where a hit test lands, how a rough ellipse is drawn for a given seed. A manifest records a SHA-256 for every file. The Windows test project reads the same files and runs the C# port against them. If the Mac changes a rule and nobody ports it, the Windows tests fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A UX reference.&lt;/strong&gt; The Mac's native UI tests can capture each panel and menu: labels, accessibility identifiers, values, order and frames. A script packs those into &lt;code&gt;reference/ux-reference&lt;/code&gt;. The Windows tests compare their own surfaces with that capture, within 1 DIP, with exact labels and order.&lt;/p&gt;

&lt;p&gt;Both folders are generated on the Mac and never edited by hand. A Mac change that affects them regenerates them in the same pull request, so the diff shows exactly what Windows now has to match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain rules in review.&lt;/strong&gt; Every user-visible change ships on both platforms in the same release. If something can't work the same way on Windows, it gets a written deviation in the parity docs instead of a silent difference.&lt;/p&gt;

&lt;h3&gt;
  
  
  One commit, every channel
&lt;/h3&gt;

&lt;p&gt;A release has a lot of destinations: a signed and notarized Mac disk image and ZIP, a Sparkle update feed, a Homebrew cask, a Microsoft Store bundle, portable Windows ZIPs for winget, and the website. I wanted all of them to come from one commit, built once.&lt;/p&gt;

&lt;p&gt;The release notes live in &lt;code&gt;releases/notes.json&lt;/code&gt;. That one file produces the changelog, the GitHub release text and the app's own "What's changed" screen. When a version is ready, its entry records the exact source commit. A check then fails if any app code changed after that commit, so the thing I tested is the thing I ship.&lt;/p&gt;

&lt;p&gt;The Release workflow only starts by hand, on &lt;code&gt;main&lt;/code&gt;. It runs, roughly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Preflight&lt;/strong&gt; checks that the commit, the version and the frozen notes agree, and stops before anything is built if they don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate&lt;/strong&gt; runs the release and packaging guards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign&lt;/strong&gt; builds the Mac app, signs it with a Developer ID, notarizes and staples it, and builds the disk image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows packages&lt;/strong&gt; builds the x64 and ARM64 Native AOT packages, the MSIX bundle and the portable ZIPs. The Windows package version is derived from the shared one, &lt;code&gt;(Major + 1).Minor.Build.0&lt;/code&gt;, because the Store wants a version that only goes up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish&lt;/strong&gt; creates the GitHub release and updates the Homebrew tap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store&lt;/strong&gt; sends the bundle to the Microsoft Store.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After that, a website workflow deploys the site and the update feeds from the published release.&lt;/p&gt;

&lt;p&gt;Secrets sit in GitHub environments, each visible only to the job that needs it, and each environment waits for my approval. Pull requests, including ones from forks, get no secrets at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Things that bit me
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A signing step failed with "a base64 signing secret is invalid". The secret it pointed at was fine. The real problem was a public key variable missing its final &lt;code&gt;=&lt;/code&gt;. Now I check every value when signing fails, not only the one I suspect.&lt;/li&gt;
&lt;li&gt;The pull request checks used &lt;code&gt;cancel-in-progress&lt;/code&gt;. Opening a labelled pull request fires two events at once, one run got cancelled, and its cancelled checks blocked the merge. The checks take seconds, so every event now gets its own run.&lt;/li&gt;
&lt;li&gt;The Microsoft Store CLI refuses every command until it has been signed in with &lt;code&gt;reconfigure&lt;/code&gt;, even setting its own options. The order matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Try it, or help
&lt;/h3&gt;

&lt;p&gt;Annotator is free and Apache-2.0: &lt;a href="https://github.com/dorlugasigal/annotator" rel="noopener noreferrer"&gt;https://github.com/dorlugasigal/annotator&lt;/a&gt;. On the Mac, &lt;code&gt;brew install --cask dorlugasigal/annotator/annotator&lt;/code&gt;. On Windows, it's in the Microsoft Store.&lt;/p&gt;

&lt;p&gt;The open issues are written so anyone can pick one up. Several need real hardware I don't have: an Intel Mac, an ARM64 PC, a pen display, a second screen at another scale. If you have one of those, an hour of testing helps a lot.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/dorlugasigal" rel="noopener noreferrer"&gt;
        dorlugasigal
      &lt;/a&gt; / &lt;a href="https://github.com/dorlugasigal/annotator" rel="noopener noreferrer"&gt;
        annotator
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Draw on your screen as you explain. Native apps for macOS and Windows. Free and open source.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/dorlugasigal/annotator/.github/readme/icon.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdorlugasigal%2Fannotator%2FHEAD%2F.github%2Freadme%2Ficon.png" width="112" height="112" alt=""&gt;&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Annotator&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Draw on your screen as you explain.&lt;/strong&gt;&lt;br&gt;
Use it in demos, reviews and lessons. Every mark stays editable
Native apps for macOS and Windows.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://getannotator.app/downloads/" rel="nofollow noopener noreferrer"&gt;&lt;strong&gt;Download&lt;/strong&gt;&lt;/a&gt; &amp;nbsp;·&amp;nbsp;
&lt;a href="https://getannotator.app/" rel="nofollow noopener noreferrer"&gt;Website&lt;/a&gt; &amp;nbsp;·&amp;nbsp;
&lt;a href="https://getannotator.app/docs/" rel="nofollow noopener noreferrer"&gt;Help&lt;/a&gt; &amp;nbsp;·&amp;nbsp;
&lt;a href="https://github.com/dorlugasigal/annotator/releases" rel="noopener noreferrer"&gt;Release notes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/dorlugasigal/annotator/releases" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/90b31842f2bcba8fa9c38750143ec1d78860a28fd83d973aaf2a0c2c2cdd1eb0/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f72656c656173652f646f726c756761736967616c2f616e6e6f7461746f723f696e636c7564655f70726572656c6561736573266c6162656c3d72656c6561736526636f6c6f723d666662353863266c6162656c436f6c6f723d313131393238" alt="Latest release"&gt;&lt;/a&gt;
&lt;a href="https://github.com/dorlugasigal/annotator#install" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f5908ed36b00e63cfb7541c3220decb5a361799aa952bcfcad1ce553b339b62e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61634f532d31342532422d6538656566393f6c6162656c436f6c6f723d313131393238" alt="macOS 14 or later"&gt;&lt;/a&gt;
&lt;a href="https://github.com/dorlugasigal/annotator#install" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/779f198b99b7826ffa42ba03c5bf207e4767399347f362b4ad67adc2e917bbc2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f57696e646f77732d31312d6538656566393f6c6162656c436f6c6f723d313131393238" alt="Windows 11"&gt;&lt;/a&gt;
&lt;a href="https://github.com/dorlugasigal/annotator/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/4bb52dd5fb7798e5b433baae5a3968029b621df8ad5ad6a454f31469faeafec4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d417061636865253230322e302d6538656566393f6c6162656c436f6c6f723d313131393238" alt="License: Apache 2.0"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/dorlugasigal/annotator/actions/workflows/validate.yml" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/edeada7df0aedf2861331368a93b653857521bd1e694e64229a6a1401231d174/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f646f726c756761736967616c2f616e6e6f7461746f722f76616c69646174652e796d6c3f6272616e63683d6d61696e266c6162656c3d4349266c6162656c436f6c6f723d313131393238" alt="CI"&gt;&lt;/a&gt;
&lt;a href="https://scorecard.dev/viewer/?uri=github.com/dorlugasigal/annotator" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/85ec20bc78087b45d306a9098b43a50cbc7b12a9322a6fd9ea4488f49d04e659/68747470733a2f2f696d672e736869656c64732e696f2f6f7373662d73636f7265636172642f6769746875622e636f6d2f646f726c756761736967616c2f616e6e6f7461746f723f6c6162656c3d4f70656e53534625323053636f726563617264266c6162656c436f6c6f723d313131393238" alt="OpenSSF Scorecard"&gt;&lt;/a&gt;
&lt;a href="https://github.com/dorlugasigal/annotator/discussions" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/d6827055885136f333a3ec0cd3f5b3fc25280d5a2a466a9a6101849f1ffca552/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f64697363757373696f6e732f646f726c756761736967616c2f616e6e6f7461746f723f6c6162656c3d64697363757373696f6e7326636f6c6f723d653865656639266c6162656c436f6c6f723d313131393238" alt="Discussions"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

  
    
    &lt;span class="m-1"&gt;annotator-reel.mp4&lt;/span&gt;
    
  

  

  


&lt;p&gt;Recorded in the real Mac app, with a scripted pointer and made-up sample apps.&lt;br&gt;
Music: “Product Launch Advertising Commercial Music” by lNPLUSMUSIC, from Pixabay.&lt;/p&gt;
&lt;div class="markdown-alert markdown-alert-note"&gt;
&lt;p class="markdown-alert-title"&gt;Note&lt;/p&gt;
&lt;p&gt;Annotator is in public beta. Expect rough edges, and please
&lt;a href="https://github.com/dorlugasigal/annotator/issues/new/choose" rel="noopener noreferrer"&gt;report them&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What you can do&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Pick a tool in the floating toolbar and draw over any app. Your marks stay on
screen while you keep working in the app underneath, and you can move, restyle
or undo them later.&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td width="50%"&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/dorlugasigal/annotator/.github/readme/circle.jpg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdorlugasigal%2Fannotator%2FHEAD%2F.github%2Freadme%2Fcircle.jpg" alt="A rough teal ellipse around a number on a sample dashboard"&gt;&lt;/a&gt;&lt;br&gt;
&lt;b&gt;Circle&lt;/b&gt;&lt;br&gt;
Choose Ellipse and circle a number. The Sketchy style keeps it hand-drawn.
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/dorlugasigal/annotator/.github/readme/arrow.jpg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdorlugasigal%2Fannotator%2FHEAD%2F.github%2Freadme%2Farrow.jpg" alt="A curved blue arrow from a percentage down to a dip in a chart"&gt;&lt;/a&gt;&lt;br&gt;
&lt;b&gt;Arrow&lt;/b&gt;&lt;br&gt;
Click to place points and press Return. Drag a point to bend the arrow.
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/dorlugasigal/annotator/.github/readme/smart-draw.jpg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdorlugasigal%2Fannotator%2FHEAD%2F.github%2Freadme%2Fsmart-draw.jpg" alt="A neat box and arrow around a Pay now button, labelled Make it bigger"&gt;&lt;/a&gt;&lt;br&gt;
&lt;b&gt;Smart Draw&lt;/b&gt;&lt;br&gt;
Sketch a box or an arrow, and Smart Draw makes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/dorlugasigal/annotator" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>opensource</category>
      <category>swift</category>
      <category>csharp</category>
      <category>productivity</category>
    </item>
    <item>
      <title>GitHub Just Proved That Remote Terminal Access Matters - Here's the Mobile IDE I Built for It</title>
      <dc:creator>Dor Lugasi-Gal</dc:creator>
      <pubDate>Tue, 14 Apr 2026 08:54:40 +0000</pubDate>
      <link>https://dev.to/dorlugasigal/github-just-proved-that-remote-terminal-access-matters-heres-the-mobile-ide-i-built-for-it-3ng9</link>
      <guid>https://dev.to/dorlugasigal/github-just-proved-that-remote-terminal-access-matters-heres-the-mobile-ide-i-built-for-it-3ng9</guid>
      <description>&lt;p&gt;GitHub just shipped &lt;a href="https://github.blog/changelog/2026-04-13-remote-control-cli-sessions-on-web-and-mobile-in-public-preview/" rel="noopener noreferrer"&gt;&lt;code&gt;copilot --remote&lt;/code&gt;&lt;/a&gt; - the ability to stream a Copilot CLI session to your phone or browser. Claude Code has had something similar with their &lt;code&gt;--remote&lt;/code&gt; flag. The idea is the same: you should be able to reach your coding session from wherever you are.&lt;/p&gt;

&lt;p&gt;I agree. In fact, I've been building exactly that in the last month.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/dorlugasigal/TermBeam" rel="noopener noreferrer"&gt;TermBeam&lt;/a&gt; is an open-source tool that turns your terminal into a mobile IDE. Run &lt;code&gt;npx termbeam&lt;/code&gt;, scan the QR code, and you get a full terminal on your phone — any shell, any command, any folder. It's a PWA, so after the first scan you add it to your home screen and it's just an app. One tap and you're in, works on any device since its just a web page.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;TermBeam's built-in Copilot SDK integration — markdown rendering, tool call visibility, and the terminal right there on my phone.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I can be pushing my daughter on the swings at the park, or sitting in a bomb shelter during a missile alert, and still pull out my phone, open TermBeam, and create a new session in whatever folder I need. thinking retrospectively, that kind of access isn't a nice-to-have for work-life balance, it's a necessity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx termbeam
// or
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; termbeam
termbeam
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That starts a server, prints a QR code, and when you scan it you get a real terminal session in your browser. Full PTY — whatever shell you use, whatever commands you run. It tunnels through Azure DevTunnels by default so it works from anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You start it in whatever folder you want, with whatever shell you want.&lt;/strong&gt; You're not locked into a specific tool's session model. And you can create new sessions from your phone — pick a folder, pick a shell, go.&lt;/p&gt;

&lt;p&gt;For something permanent, install it as a background service:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;PM2-managed, starts on boot. Your terminal is always one tap away.&lt;/p&gt;

&lt;h2&gt;
  
  
  It Runs AI Agents Too
&lt;/h2&gt;

&lt;p&gt;This is where the &lt;code&gt;copilot --remote&lt;/code&gt; connection gets interesting. TermBeam has a proper &lt;code&gt;@github/copilot-sdk&lt;/code&gt; integration — a dedicated UI pane with markdown rendering, tool call display, and a chat interface, all alongside the terminal. It's built specifically for interacting with Copilot on a small screen.&lt;/p&gt;

&lt;p&gt;On top of that, TermBeam auto-detects AI coding tools in your PATH — Copilot CLI, Claude Code, Aider, Codex, OpenCode — and lets you launch any of them from the new session dialog. One tap.&lt;/p&gt;

&lt;p&gt;So while &lt;code&gt;copilot --remote&lt;/code&gt; gives you remote access to an existing Copilot session, TermBeam gives you the ability to open a full terminal session where Copilot (or any other agent) is just one of the things you can run. Different approach to a similar idea — and they work well together.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes It Usable on a Phone
&lt;/h2&gt;

&lt;p&gt;A terminal on your phone is a gimmick unless the UX is built for touch, like the existing ssh solutions. Here's what makes the difference:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Touch bar.&lt;/strong&gt; Two rows of dedicated buttons: Esc, Copy, Paste, Home, End, ↑, ←, ↓, →, Enter, Ctrl, Shift, Tab, ^C, and a microphone for voice commands. These are the keys phone keyboards either don't have or hide behind long-presses. This makes running a Copilot session — or vim, or anything — actually practical on a phone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8nrr44bwbuhdb2klnrr6.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8nrr44bwbuhdb2klnrr6.jpeg" alt="Terminal with touch bar" width="430" height="932"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Multiple tabs at the top, git and folder context info, and the touch bar above your keyboard. This is a real session on my phone.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Command palette.&lt;/strong&gt; Tap the Tools button on the top right and you get a panel with six sections: &lt;strong&gt;Session&lt;/strong&gt; (new tab, upload/download files, view markdown, close/rename/split/stop sessions, resume agent sessions), &lt;strong&gt;Search&lt;/strong&gt; (find in terminal), &lt;strong&gt;View&lt;/strong&gt; (font size, themes, port preview, code viewer, git changes), &lt;strong&gt;Share&lt;/strong&gt; (copy shareable link), &lt;strong&gt;Notifications&lt;/strong&gt; (toggle push notifications), and &lt;strong&gt;System&lt;/strong&gt; (refresh, clear terminal, about). Every feature is two taps away.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;The full command palette. Upload files, view markdown, preview a port, switch themes, check git changes — all from one panel.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sessions hub.&lt;/strong&gt; Each session gets a card showing the working directory, shell, PID, git branch, repo name, and git status — which sessions have staged changes, which are clean, how long ago they were active. The "+ New Session" button lets you create a session in any folder, right from your phone.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Three sessions across different projects, each showing git info and status. Tap to connect, "+ New Session" to start one anywhere.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File browser.&lt;/strong&gt; Browse the session's working directory, search files, view code, download files, and see git changes — all in a side panel. The Changes tab shows what's modified and staged.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;The file browser with Files and Changes tabs, browsing a project directory.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the rest:&lt;/strong&gt; 40 themes (yes, i got carried away lol), push notifications when commands finish, port preview that proxies your local dev server. It's a PWA with iPhone safe-area support, so it feels native.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Under the Hood
&lt;/h2&gt;

&lt;p&gt;TermBeam spawns a PTY with your shell, serves a React frontend (xterm.js, Zustand, Radix UI) over Express, and bridges them with WebSocket. Azure DevTunnels handles the public URL, or you can choose to work on LAN. Sessions persist when you disconnect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx termbeam                           &lt;span class="c"&gt;# default: tunnel + auto password&lt;/span&gt;
termbeam &lt;span class="nt"&gt;--password&lt;/span&gt; secret &lt;span class="nt"&gt;--lan&lt;/span&gt;       &lt;span class="c"&gt;# LAN only, custom password&lt;/span&gt;
termbeam &lt;span class="nt"&gt;--persisted-tunnel&lt;/span&gt;            &lt;span class="c"&gt;# stable URL across restarts&lt;/span&gt;
termbeam resume                        &lt;span class="c"&gt;# reconnect from CLI&lt;/span&gt;
termbeam service &lt;span class="nb"&gt;install&lt;/span&gt;               &lt;span class="c"&gt;# always-on background service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On security — auto-generated passwords, rate-limited login (5 attempts/min/IP), httpOnly cookies, CSP and X-Frame-Options headers, WebSocket origin validation, and single-use QR auth tokens that expire in 5 minutes. Full threat model in &lt;a href="https://github.com/dorlugasigal/TermBeam/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;1350+ tests across Windows, macOS, and Linux on Node 20, 22, and 24. The less glamorous work was the mobile edge cases — Safari viewport bugs, PTY behavior differences across platforms, WebSocket reconnection after screen locks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;TermBeam is open source, MIT licensed, v1.20.1 on npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx termbeam
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scan the QR code, add to home screen, and you've got a terminal on your phone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/dorlugasigal/TermBeam" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/termbeam" rel="noopener noreferrer"&gt;npm&lt;/a&gt; · &lt;a href="https://dorlugasigal.github.io/TermBeam/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
