<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="http://willmcgugan.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="http://willmcgugan.github.io/" rel="alternate" type="text/html" /><updated>2026-02-11T19:09:01+00:00</updated><id>http://willmcgugan.github.io/feed.xml</id><title type="html">Will McGugan</title><subtitle>Will McGugan&apos;s essays</subtitle><author><name>Will McGugan</name></author><entry><title type="html">AI_POLICY.md</title><link href="http://willmcgugan.github.io/ai-pr-policy/" rel="alternate" type="text/html" title="AI_POLICY.md" /><published>2026-02-11T00:00:00+00:00</published><updated>2026-02-11T00:00:00+00:00</updated><id>http://willmcgugan.github.io/ai-pr-policy</id><content type="html" xml:base="http://willmcgugan.github.io/ai-pr-policy/"><![CDATA[<p>If you maintain Open Source software, you will likely have encountered AI slop PRs.</p>

<p>Not all AI authored code can be considered slop, which is why a blanket ban on AI would be counterproductive.
My definition of “slop” is work that is AI generated, with very little involvement by the human operator.
It may seem like a good deal if somebody is spending their tokens to help your project, but without a passing understanding of the project or issue in question, the author can’t always prompt their way to a good solution.</p>

<p>As far as I can tell, most AI slop PRs are generated by a relatively small number of individuals.
They tend to arrive in batches, and I can see the author has submitted dozens or even 100s of PRs to other projects.
The work is typically a poor solution, not required, or simply broken. And the author will never follow up on comments.</p>

<p>It is in effect a DDOS for FOSS maintainers; it takes much longer to review the PR than it did to create it.</p>

<p>Nonetheless, I still feel bad about closing a PR without comment.
But at the same time resentful at having to spend time formulating a response, that will likely be ignored.</p>

<p>So I plan to include a text file in my project, to clarify my stance on AI PRs.
I’m calling this <code class="language-plaintext highlighter-rouge">AI_POLICY.md</code>.
I did consider adding an AI policy to <code class="language-plaintext highlighter-rouge">CONTRIBUTING.md</code>, but that file tends to be used to inform <em>how</em> to contribute, which seems a different purpose entirely.</p>

<p>I’m hoping this could become a standard file, and AI agents would refer to this when generating a PR.
Until then, I can link to it when I close slop PRs.</p>

<p>If something like this exists already, or there is a more agent-friendly way of doing this, then let me know.</p>

<p>Here’s the text I’m going with.
I don’t think this is particularly challenging to meet, and only a slightly higher bar than what I’d expect from a mammalian brain.</p>

<h2 id="ai_policymd">AI_POLICY.md</h2>

<p>This project accepts AI generated Pull Requests, as long as the following guidelines are met.</p>

<ul>
  <li>The Pull Request must fill in the repository’s pull request template.</li>
  <li>The Pull Request must identify itself as AI generated, including the name of the agent used.</li>
  <li>The Pull Request must link to a issue or discussion where a solution has been approved by a maintainer (@willmcgugan).</li>
</ul>

<p>The maintainer reserves the right to close PRs without comment if the above are not met.</p>

<h2 id="update">Update</h2>

<p>Turns out <code class="language-plaintext highlighter-rouge">AI_POLICY.md</code> is being used in a bunch of projects, for this purpose.</p>

<p>Oddly, Claude told me there was no standard for this.
I suspect this is an organically emerging “standard”, and I hope it is adopted by more projects!</p>]]></content><author><name>Will McGugan</name></author><category term="tech" /><category term="AI" /><summary type="html"><![CDATA[If you maintain Open Source software, you will likely have encountered AI slop PRs.]]></summary></entry><entry><title type="html">Good AI, Bad AI - the experiment</title><link href="http://willmcgugan.github.io/good-ai-bad-ai/" rel="alternate" type="text/html" title="Good AI, Bad AI - the experiment" /><published>2026-01-10T00:00:00+00:00</published><updated>2026-01-10T00:00:00+00:00</updated><id>http://willmcgugan.github.io/good-ai-bad-ai</id><content type="html" xml:base="http://willmcgugan.github.io/good-ai-bad-ai/"><![CDATA[<p>If you are in tech, or possibly even if you aren’t, your social feeds are likely awash with AI.
Most developers seem to be either all-in or passionately opposed to AI (with a leaning towards the all-in camp).
Personally I think the needle is hovering somewhere between bad and good.</p>

<h2 id="good-ai">Good AI</h2>

<p>AI for writing code is a skill <em>multiplier</em>.</p>

<p>We haven’t reached the point where a normie can say “Photoshop, but easier to use”.
Will we ever?
But for now it seems those who are already skilled in what they are asking the AI to do, are getting the best results.</p>

<p>I’ve seen accomplished developers on X using AI to realize their projects in a fraction of the time.
These are developers who absolutely <em>could</em> write every line that the LLM produces.
They choose not to, because time is their most precious commodity.</p>

<p>Why is this <em>good</em> AI?
It means that skills acquired in the age before AI <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> are still valuable.
We have a little time before mindless automatons force senior developers into new careers as museum exhibits, tapping on their mechanical keyboards in front of gawping school kids, next to the other fossils,</p>

<h2 id="bad-ai">Bad AI</h2>

<p>The skill multiplier effect may not be enough to boost inexperienced (or mediocre) developers to a level they would like.
But AI use does seem to apply a greater boost to the Dunning-Kruger effect.</p>

<p>If you maintain an Open Source project you may be familiar with AI generated Pull Requests. Easily identifiable by long bullet lists in the description, these PRs are often from developers who copied an issue from a project into their prompt, prefixed with the words “please fix”.</p>

<p>These drive-by AI PRs generate work for the FOSS developer.
They can look superficially correct, but it takes time to figure out if the changes really do satisfy the requirements.
The maintainer can’t use the usual signals to cut through the noise when reviewing AI generated PRs.
Copious amounts of (passing) tests and thorough documentation are no longer a signal that the PR won’t miss the point, either subtly or spectacularly.</p>

<p>This is bad AI (more accurately a bad <em>outcome</em>), because it typically takes more time for the maintainer to review such PRs than the creator took to type in the prompt.
And those that contribute such PRs rarely respond to requests for changes.</p>

<p>In the past you could get around this with a blanket ban on AI generated code.
Now, I think developers would be foolish to do that.
Good code is good code, whether authored by a fleshy mammalian brain or a mechanical process.
And it is undeniable that AI code can be <em>good</em> code.</p>

<h2 id="the-experiment">The Experiment</h2>

<p>This makes me wonder if the job of maintainer could be replaced with AI.</p>

<p>I want to propose an experiment…</p>

<p>Let’s create a repository with some initial AI generated code: “Photoshop, but easier to use” is as a starting point as good as any.
An AI agent will review issues, respond via comments, and may tag the issue with “todo” or close it if it doesn’t reach a bar for relevance and quality.</p>

<p>PRs are accepted for “todo” issues and will be reviewed, discussed, and ultimately merged or closed by the AI.
These PRs may be human or AI generated—the AI doesn’t care (as if it could).</p>

<p>Note that PRs could modify any of the prompts used by the AI, and those edits will be reviewed by the AI in the same way as any other file.</p>

<p>Would the end result be quality software or a heinous abomination, succeeding only in creating a honeypot for prompt-injection attacks?</p>

<p>I have no intention of making this happen.
But if somebody does, tell me how it goes.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Feels like a long time, but there has only been a single Fast and Furious movie made since the advent of the AI age. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Will McGugan</name></author><category term="tech" /><category term="AI" /><summary type="html"><![CDATA[If you are in tech, or possibly even if you aren’t, your social feeds are likely awash with AI. Most developers seem to be either all-in or passionately opposed to AI (with a leaning towards the all-in camp). Personally I think the needle is hovering somewhere between bad and good.]]></summary></entry><entry><title type="html">Toad is a unified experience for AI in the terminal</title><link href="http://willmcgugan.github.io/toad-released/" rel="alternate" type="text/html" title="Toad is a unified experience for AI in the terminal" /><published>2025-12-18T00:00:00+00:00</published><updated>2025-12-18T00:00:00+00:00</updated><id>http://willmcgugan.github.io/toad-released</id><content type="html" xml:base="http://willmcgugan.github.io/toad-released/"><![CDATA[<p>My startup for terminals wrapped up mid-2025 when the funding ran dry.
So I don’t have money, but what I do have are a very particular set of skills.
Skills I have acquired over a very long career convincing terminals they are actually GUIs.</p>

<p>Skills which I have used to create a terminal app that offers a more pleasant experience for agentic coding.
Toad (a play on <em>Textual Code</em>) is a front-end for AI tools such as <a href="https://openhands.dev/">OpenHands</a>, <a href="https://www.claude.com/product/claude-code">Claude Code</a>, <a href="https://geminicli.com/">Gemini CLI</a>, and many more. 
All of which run seamlessly under a single terminal UI, thanks to the <a href="https://agentclientprotocol.com/protocol/initialization">ACP</a> protocol.</p>

<p>At the time of writing, Toad supports 12 agent CLIs, and I expect many more to come online soon.</p>

<p>Here’s a screenshot:</p>

<p><img src="../images/toad-released/toad-1.png" alt="Toad UI" /></p>

<p>So what does Toad offer over the CLI apps from big tech?</p>

<p>It has most of the UI interactions users have come to expect from agentic coding, but hopefully more refined.
For instance the “@” character to bring in files into the context. Here’s Toad’s implementation:</p>

<p><img src="../images/toad-released/fuzzy-file.gif" alt="Toad fuzzy files" /></p>

<p>A snappy <em>fuzzy</em> search which filters patterns from the project’s <code class="language-plaintext highlighter-rouge">.gitignore</code> (if there is one).</p>

<p>The prompt editor offers an experience which you might be surprised to find in a terminal.
You can navigate and select with the keyboard and mouse, select, cut, copy, paste, etc.
The prompt will highlight Markdown as you type (even syntax highlighting code fences <em>before</em> you close them).</p>

<p><img src="../images/toad-released/prompt.gif" alt="Toad prompt" /></p>

<p>Toad has really nice Markdown streaming, based on the techniques I described <a href="https://willmcgugan.github.io/streaming-markdown/">here</a>.
It remains fast with large documents, and renders everything from tables to syntax highlighted code fences.</p>

<p><img src="../images/toad-released/stream.gif" alt="Toad Markdown streaming" /></p>

<p>Many other tools either don’t bother to render the Markdown, or they do a fairly half-hearted job.</p>

<p>Another goal I had for Toad was to integrate a shell.
I wanted the conversation with AI to feel like a natural extension of a traditional terminal based workflow.</p>

<p>Most tools stop at displaying monochrome output from commands.
Some will break if you run something interactive, like a TUI.
Toad doesn’t have this limitation, and will let you run all your CLI apps with full color, interactivity, and mouse support.</p>

<p>At the time of writing the only terminal based agentic coding tool I know of that runs dynamic commands inline is Gemini.</p>

<p><img src="../images/toad-released/htop.gif" alt="Toad running htop" /></p>

<p>Toad adopts the convention of using a <code class="language-plaintext highlighter-rouge">!</code> character to introduce a shell command.
There is also a list of commands in settings which will automatically trigger shell mode.
In practice, this means that you rarely need to explicitly introduce shell commands—just type what’s on your mind.</p>

<p>Toad borrows tab completion from the shell.
You’ll appreciate this if you have worked in the terminal long enough to commit this interaction to muscle memory.
Hit tab to complete the command or path.
If there is more than one possibility you can hit tab again to cycle through them, and enter to accept.</p>

<p><img src="../images/toad-released/tabcomplete.gif" alt="Toad tab complete" /></p>

<p>In addition to the shell, Toad implements a few concepts from Jupyter notebooks.
You can cursor through previous conversation, moving a logical block at a time, and interact with it again.
At the moment that feature is used as a convenience to copy content to the clipboard or prompt, and a few other niceties like exporting a SVG.</p>

<p><img src="../images/toad-released/cursor-block.png" alt="Cursor block" /></p>

<p>Toad will lean more heavily in to this kind of interaction in the future.</p>

<h2 id="friends-of-toad">Friends of Toad</h2>

<p>I was very fortunate to collaborate with <a href="https://openhands.dev/">OpenHands</a>, who are doing some amazing work in this space. Check out their <a href="https://www.openhands.dev/blog/20251218-openhands-toad-collaboration">blog post</a> on Toad!</p>

<p>I also collaborated with <a href="https://huggingface.co">Hugging Face</a> on this release. Check out their blog post on their <a href="https://huggingface.co/toad-hf-inference-explorers">inference explorers</a>!</p>

<h2 id="try-toad">Try Toad</h2>

<p>When this post is live you will be able to install Toad yourself.</p>

<p>The work is ongoing: a few missing features and interface improvements to be done, but Toad is solid enough to use as your daily driver for AI.
I used it to create <a href="https://www.batrachian.ai">batrachian.ai</a>, where you will find install instructions.</p>

<p>For more details, see the <a href="https://github.com/batrachianai/toad">Toad</a> repository.</p>

<p>I need a break (sabbaticals are tiring), but I’ll be picking things up in 2026.
I’m hoping that by the time my year off ends, Toad could become my full-time gig.
If you want to help make that happen, consider <a href="https://github.com/sponsors/willmcgugan">sponsoring my work</a>.</p>]]></content><author><name>Will McGugan</name></author><category term="text" /><category term="toad" /><category term="ai" /><summary type="html"><![CDATA[My startup for terminals wrapped up mid-2025 when the funding ran dry. So I don’t have money, but what I do have are a very particular set of skills. Skills I have acquired over a very long career convincing terminals they are actually GUIs.]]></summary></entry><entry><title type="html">The Toad Report #3</title><link href="http://willmcgugan.github.io/toad-report-3/" rel="alternate" type="text/html" title="The Toad Report #3" /><published>2025-11-17T00:00:00+00:00</published><updated>2025-11-17T00:00:00+00:00</updated><id>http://willmcgugan.github.io/toad-report-3</id><content type="html" xml:base="http://willmcgugan.github.io/toad-report-3/"><![CDATA[<p>Welcome to the third issue of the Toad Report. If you are new here, Toad is a universal interface for API I am currently building.</p>

<p>To date, most of the my work on Toad has been focussed on building the “conversation” view where the user can converse and interact with their chosen AI agent.
This has come together rather well, with a polished enough user experience for <a href="https://huggingface.co/">Hugging Face</a> to start building integrations with their technology!
And all before Toad’s official release.</p>

<div style="display: flex; justify-content: center;">
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Some technologies are indistinguishable from magic... <a href="https://twitter.com/willmcgugan?ref_src=twsrc%5Etfw">@willmcgugan</a> <a href="https://t.co/6idsU3dIDa">pic.twitter.com/6idsU3dIDa</a></p>&mdash; Shaun Smith (@evalstate) <a href="https://twitter.com/evalstate/status/1990347931249954872?ref_src=twsrc%5Etfw">November 17, 2025</a></blockquote> <script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</div>

<p>The conversation view can be considered the body of Toad.
But a body needs a head.
In the case of Toad, it needs an interface where the user can find, install, and launch agents.
Which is what Ive been working on recently.</p>

<p>The following will be the first view the user will see if they launch <code class="language-plaintext highlighter-rouge">toad</code> with no arguments:</p>

<p><img src="../images/toadreport3/store1.png" alt="Toad Store" /></p>

<p>From here the user can navigate the (growing) list of compatible agents.
When they select one of interest, Toad will pop up a dialog with information (in beautiful markdown) regarding the agent.</p>

<p>In this dialog is a pull down list of possible actions associated with the agent—where there will typically be at least an “install” action.</p>

<p><img src="../images/toadreport3/store2.png" alt="Toad Store" /></p>

<p>Clicking the “Go” button downloads and installs the agent without ever leaving the app:</p>

<p><img src="../images/toadreport3/store3.png" alt="Toad Store" /></p>

<p>When an agent is installed, it is placed in the “quick launch” area.
Each entry in the quick launch has a digit associated, so they can jump straight to that agent.</p>

<p><img src="../images/toadreport3/store4.png" alt="Toad Store" /></p>

<p>When an agent is selected (quick launch or other), the user can press space to launch the conversation view and begin their agentic coding session.</p>

<p>I wanted the experience to be as friction free as possible.
There are many coding agents out there, but not all of them have the same visibility as those offered by big tech.
I’m hoping this will promote the less well funded agents.</p>

<p>Here’s a video a recorded of the installation process.
The interface is a little older than the screenshots, but you should get the idea…</p>

<iframe width="100%" style="aspect-ratio:16/11;" src="https://www.youtube.com/embed/xZvHS7wMFlk" title="toad store" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<h2 id="found-this-interesting">Found this interesting?</h2>

<p>Follow me on the socials where I will be posting regular updates.
You can also join the <a href="https://discord.gg/Enf6Z3qhVr">Textual Discord Server</a> if you want to chat with me or the Textual community.</p>

<p>Join the <code class="language-plaintext highlighter-rouge">#toad</code> channel if you would like an invite to the Toad repository. I’ll be sending more out in a week or two.</p>

<p>Thanks for reading!</p>]]></content><author><name>Will McGugan</name></author><category term="tech" /><category term="toad" /><category term="ai" /><summary type="html"><![CDATA[Welcome to the third issue of the Toad Report. If you are new here, Toad is a universal interface for API I am currently building.]]></summary></entry><entry><title type="html">A Developer’s experience with weight loss injections</title><link href="http://willmcgugan.github.io/weight-loss/" rel="alternate" type="text/html" title="A Developer’s experience with weight loss injections" /><published>2025-11-06T00:00:00+00:00</published><updated>2025-11-06T00:00:00+00:00</updated><id>http://willmcgugan.github.io/weight-loss</id><content type="html" xml:base="http://willmcgugan.github.io/weight-loss/"><![CDATA[<p>This is a personal account of my experiences taking weight loss medication.
Quite a departure from my usual content.</p>

<p>I have no doubt that you are smart enough <em>not</em> to take medical advice from some random nerd on the internet, but let me say it anyway: none of the following is medical advice.
I want to share my experiences and perspectives in addition to a vibe-coded tool, but I’m not trying to convince you of anything.
Please don’t take my word for anything health related—talk to a medical professional first!</p>

<p>Got it? Good.</p>

<p>One of my main goals from my year off was to lose weight, which had been creeping upwards over the years.
When I started my sabbatical in June I had reached 100kg which pushed me just over the threshold from overweight to obese.</p>

<p>Of course this shocked me.
Doubly so, because I didn’t feel that big.
If I were to walk out of my house on any given day I would pass plenty of other folk with much larger waistlines than mine.
Living in a country 🏴󠁧󠁢󠁳󠁣󠁴󠁿 were deep frying is a religion and “juice” doesn’t contain fruit, clearly gave me a false perspective.</p>

<p><a href="https://en.wikipedia.org/wiki/Deep-fried_Mars_bar"><img src="../images/DeepFriedMarsBar.jpg" alt="Scottish food" /></a>
<em>Scottish food</em></p>

<p>Although I didn’t feel so large, I was very much aware of a number of weight related health issues dragging my quality of life downwards.
For years I had managed these issue, but I could see they weren’t going to improve without losing weight.</p>

<p>There was a time (~10 years ago) when I was in good shape and my weight was stable.
I attributed this to running.
A 5km three times a week and the occasional 10km plus some resistance training meant I could eat more-or-less what I wanted.
The turning point was when I developed a <em>herniated disc</em>.
An hour’s run would be followed by 48 hours of constant back pain.</p>

<p>Incidentally, a herniated disc must be the dumbest flaw in the human body.
The human spine is a miraculous thing, but the “discs” (essentially washers that separate vertebrae) are prone to (literal) bursting.
Around 50% of the population have a herniated disc by the age of 50.
Most cause no symptoms, but in some people (like myself) the bulging disc presses on a nerve, causing pain.
If there were a git repository for the human body, I would surely file a passive-aggressive issue.
I might remark on that issue how it was clearly a poor choice to install a component with 50 year life span into something expected to last at least 80 years.</p>

<p>Exercises recommended by a physio didn’t help (they actually made it worse).
Heat packs, cold packs, ibuprofen, and massage offered only short term relief.</p>

<p>When I stopped running for a few days the pain became an occasional sharp piercing pain rather than a constant ache.
Not great, but bearable.</p>

<p>And so ended my running career.</p>

<p>I replaced the running with nothing at all.
Other than walking (more as my default method of transport) I didn’t really exercise.
I can’t attribute this entirely to the back pain.
There are several forms of exercise that don’t inflame the back pain, such as swimming.
I just didn’t find anything that I enjoyed doing regularly in the same way as running.</p>

<p>My diet is fairly healthy—for a Scot at least.
I suspect I am in the top 0.1% percentile in the country for eating vegetables (and I’m not counting potato).
My downfall is more sweet foods, which I typically indulge in my morning pastry and coffee.
The rest of the day I try to eat healthy.
I watch portion size and routinely deny myself things I would enjoy eating (trust me: if I ate all the sweet things I wanted to, my diet would be 90% cinnamon buns).</p>

<p>Alas, this wasn’t enough to keep the weight down.
The scales don’t lie, and they were telling me I was getting heaver over time, which meant I was consuming more calories than I was expending.
But how many more calories was I eating?</p>

<p>Turns out that is easy to calculate.
There are approximately 7,700 calories in 1kg of human fat (eww—try not to imagine 1kg of fat).
I had gained 8.5kg in 1400 days, which works out as a ~47 calorie excess per day.
Around half an apple’s worth.</p>

<p>Half an apple?
Two or three additional mouthfuls a day is all it takes to put you on a trajectory to being overweight / obese and all the health issues that entails.
Yeah, well that was an eye opener.</p>

<p>If you are gaining or losing weight yourself, you might want to try this tool I vide-coded with <a href="https://willmcgugan.github.io/categories/#toad">Toad</a> and Claude:</p>

<div class="calorie-calculator">
    <h3>Excess Calorie Calculator</h3>
    
    <div class="form-group">
        <label for="startWeight">Starting Weight (kg)</label>
        <input type="number" id="startWeight" step="0.1" placeholder="e.g., 70.0" />
    </div>
    
    <div class="form-group">
        <label for="endWeight">Ending Weight (kg)</label>
        <input type="number" id="endWeight" step="0.1" placeholder="e.g., 73.5" />
    </div>
    
    <div class="form-group">
        <label for="startDate">Start Date</label>
        <input type="date" id="startDate" />
    </div>
    
    <div class="form-group">
        <label for="endDate">End Date</label>
        <input type="date" id="endDate" />
    </div>
    
    <button class="calculate-btn" onclick="calculateCalories()">Calculate</button>
    
    <div class="error" id="error"></div>
    
    <div class="results" id="results">
        <h3>Results</h3>
        <div class="result-item">
            <div class="result-label">Weight Change:</div>
            <div class="result-value" id="weightChange"></div>
        </div>
        <div class="result-item">
            <div class="result-label">Time Period:</div>
            <div class="result-value" id="timePeriod"></div>
        </div>
        <div class="result-item">
            <div class="result-label">Total Excess Calories:</div>
            <div class="result-value" id="totalCalories"></div>
        </div>
        <div class="result-item">
            <div class="result-label">Excess Calories Per Day:</div>
            <div class="result-value" id="caloriesPerDay"></div>
        </div>
    </div>
</div>

<style>
    .calorie-calculator {
        max-width: 500px;
        margin: 20px auto;
        padding: 20px;
        border: 1px solid #ddd;
        border-radius: 8px;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
        background-color: #f9f9f9;
    }
    
    .calorie-calculator h2 {
        margin-top: 0;
        color: #333;
        font-size: 1.5em;
    }
    
    .form-group {
        margin-bottom: 15px;
    }
    
    .form-group label {
        display: block;
        margin-bottom: 5px;
        font-weight: 500;
        color: #555;
    }
    
    .form-group input {
        width: 100%;
        padding: 8px;
        border: 1px solid #ccc;
        border-radius: 4px;
        box-sizing: border-box;
        font-size: 14px;
    }
    
    .form-group input:focus {
        outline: none;
        border-color: #4CAF50;
    }
    
    .calculate-btn {
        width: 100%;
        padding: 10px;
        background-color: #4CAF50;
        color: white;
        border: none;
        border-radius: 4px;
        font-size: 16px;
        font-weight: 500;
        cursor: pointer;
        margin-top: 10px;
    }
    
    .calculate-btn:hover {
        background-color: #45a049;
    }
    
    .results {
        margin-top: 20px;
        padding: 15px;
        background-color: white;
        border-radius: 4px;
        border-left: 4px solid #4CAF50;
        display: none;
    }
    
    .results.show {
        display: block;
    }
    
    .results h3 {
        margin-top: 0;
        color: #333;
        font-size: 1.2em;
    }
    
    .result-item {
        margin: 10px 0;
        padding: 8px 0;
        border-bottom: 1px solid #eee;
    }
    
    .result-item:last-child {
        border-bottom: none;
    }
    
    .result-label {
        font-weight: 500;
        color: #666;
    }
    
    .result-value {
        font-size: 1.1em;
        color: #333;
        font-weight: 600;
    }
    
    .result-value.positive {
        color: #d32f2f;
    }
    
    .result-value.negative {
        color: #388e3c;
    }
    
    .error {
        color: #d32f2f;
        font-size: 14px;
        margin-top: 5px;
        display: none;
    }
    
    .error.show {
        display: block;
    }
</style>

<script>
    const CALORIES_PER_KG = 7700;

    function calculateDaysBetween(startDate, endDate) {
        const start = new Date(startDate);
        const end = new Date(endDate);
        const diffTime = end - start;
        const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
        return diffDays;
    }

    function calculateTotalExcessCalories(weightStart, weightEnd) {
        const weightChange = weightEnd - weightStart;
        return weightChange * CALORIES_PER_KG;
    }

    function showError(message) {
        const errorDiv = document.getElementById('error');
        errorDiv.textContent = message;
        errorDiv.classList.add('show');
        document.getElementById('results').classList.remove('show');
    }

    function hideError() {
        document.getElementById('error').classList.remove('show');
    }

    function calculateCalories() {
        hideError();

        // Get input values
        const startWeight = parseFloat(document.getElementById('startWeight').value);
        const endWeight = parseFloat(document.getElementById('endWeight').value);
        const startDate = document.getElementById('startDate').value;
        const endDate = document.getElementById('endDate').value;

        // Validate inputs
        if (!startWeight || !endWeight || !startDate || !endDate) {
            showError('Please fill in all fields');
            return;
        }

        if (startWeight <= 0 || endWeight <= 0) {
            showError('Weight values must be positive');
            return;
        }

        const days = calculateDaysBetween(startDate, endDate);

        if (days <= 0) {
            showError('End date must be after start date');
            return;
        }

        // Calculate results
        const weightChange = endWeight - startWeight;
        const totalExcessCalories = calculateTotalExcessCalories(startWeight, endWeight);
        const caloriesPerDay = totalExcessCalories / days;

        // Display results
        document.getElementById('weightChange').textContent = 
            `${weightChange > 0 ? '+' : ''}${weightChange.toFixed(1)} kg`;
        
        document.getElementById('timePeriod').textContent = 
            `${days} day${days !== 1 ? 's' : ''}`;
        
        const totalCaloriesElement = document.getElementById('totalCalories');
        totalCaloriesElement.textContent = 
            `${totalExcessCalories > 0 ? '+' : ''}${totalExcessCalories.toFixed(0)} kcal`;
        totalCaloriesElement.className = 'result-value ' + (totalExcessCalories > 0 ? 'positive' : 'negative');
        
        const caloriesPerDayElement = document.getElementById('caloriesPerDay');
        caloriesPerDayElement.textContent = 
            `${caloriesPerDay > 0 ? '+' : ''}${caloriesPerDay.toFixed(0)} kcal/day`;
        caloriesPerDayElement.className = 'result-value ' + (caloriesPerDay > 0 ? 'positive' : 'negative');

        // Show results
        document.getElementById('results').classList.add('show');
    }

    // Allow Enter key to trigger calculation
    document.querySelectorAll('input').forEach(input => {
        input.addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                calculateCalories();
            }
        });
    });

    // Set default dates (today and 2 months ago)
    const today = new Date();
    const twoMonthsAgo = new Date();
    twoMonthsAgo.setMonth(today.getMonth() - 2);
    
    document.getElementById('endDate').valueAsDate = today;
    document.getElementById('startDate').valueAsDate = twoMonthsAgo;
</script>

<p>So the weight gain is no mystery—I was simply eating to excess.
The solution was also no mystery.
I would have have to stop consuming that additional 47 calories a day to stabilize my weight.
And then reduce it further to reverse the trend.</p>

<p>If I were to subtract <em>another</em> 47 calories in addition to the 47 excess calories, then I would lose weight at the same rate I had previously been gaining.
In other words, eating 94 calories less a day I would return to my healthy weight (~80kg) in about 9 years.</p>

<p>Screw that.</p>

<p>I have a year off to focus on my health, not 9 years.
Losing 20kg in a year would require a 422kcal deficit, on top of the 47 excess I had been eating.</p>

<p>That’s quite doable with healthier food choices and exercise.
I could give up my morning pastry and take up swimming.
But its hard to avoid hearing about weight loss medications, and after looking into it I was tempted.
Ultimately I settled on Mounjaro as it is reported to lead to the greatest weight loss.</p>

<p>The medication comes in a format described as “pen”.
In reality of course it is a syringe, albeit a cleverly designed one designed to deliver 4 measured doses.</p>

<p>I don’t have a problem with needles but I was a little apprehensive about injecting myself.
I needn’t have been.
The needle is tiny (less than 1cm and almost as thin as a human hair), and only needs to go just under the skin to a fat layer.
You bring it down in a stabbing motion and press a button at the top of the “pen” until it delivers the dose.</p>

<p><img src="../images/pulp-fiction-stab.gif" alt="Pulp fiction stab" /></p>

<p>In the first week it was immediately apparent that the medication was doing something.
It did suppress my appetite, but in those first weeks I consciously didn’t eat much at all as any meal resulted in gastric symptoms.
I’ll spare you the details, but that first month the weight loss was dramatic.
The second month was a little better and I could really appreciate the reduction in my appetite.</p>

<p>My wife and I have a running joke.
When I’m hungry I tell her “if I don’t eat in the next 15 minutes I am going to <em>die</em>”.
She replies “give it a try”.
Well I never did give it a try until recently and it turns out she was right.
The sensation of hunger was far less urgent, and I could put it off for way more than 15 minutes.
And when I did eat, I felt like I was done after a very small portion.</p>

<p>Paradoxically I also found that my energy levels were positively brimming during the first two months.
From morning until evening I had loads of energy and of course I wasn’t suffering from a post-lunch slump or evening sluggishness.
I was still suffering from stomach issues, but the increase in energy and clear weight-loss felt like fair compensation.</p>

<p>The stomach issues improved somewhat but were relatively constant throughout the whole experience.
Alas the high energy levels disappeared after two months or so.
The weight loss was rapid, averaging 1kg a week, which is borderline concerning.
I always ate to my appetite and intentionally chose nutritious high-energy foods, hoping to slow down the weight loss just a bit.
But I just couldn’t face eating after a point.</p>

<p>At about 3 months or so after starting Mounjaro I started experiencing another symptom: frequent head-rushes.
I would stand up from a sitting or laying position and instantly feel woozy.
Often leaning against the wall in case I blacked out (I never did).
I would be back to normal after 30 seconds, but this occurred often enough to be a worry.</p>

<p>This turned out to be only indirectly related to the Mounjaro.
I had been on blood pressure medication for a couple of years, and with my blood pressure coming down naturally from the weight loss the medication was too effective.
My GP reduced the medication by half and monitored my blood pressure for another month.
The head-rushes abated and I eventually come off the blood pressure medication altogether.
What a result!</p>

<p>My back pain has also improved.
It hasn’t gone away, but the frequency and intensity of bad-back days has reduced remarkably.
If that wasn’t enough, I haven’t had a single bout of <a href="https://www.nhsinform.scot/illnesses-and-conditions/stomach-liver-and-gastrointestinal-tract/gastro-oesophageal-reflux-disease-gord/">GORD</a> since losing the weight.</p>

<p>It’s now 5 months on since I started Mounjaro, and I have taken my last dose.
I lost just over 20kg which puts me in my ideal weight bracket.
It has been no cake-walk, but I consider the months-long stomach ache to be worth it for the health benefits it has delivered.
I’m looking forward to enjoying food again, but I’m highly motivated not to eat the 47 calories a day that made me obese.</p>

<p>Reflecting on the wider impacts of Mounjaro and similar drugs.
These must surely be the largest change to public health since anti-biotics.
Many of the conditions that kill people in their 50s onwards are made worse if not outright caused by being overweight or obese.
If everyone can get their weight under control it will dramatically reduce rates of diabetes, heart disease, stroke, and cancer.
We will all be living longer more productive lives as a result.</p>

<p>Let me finish by re-iterating that I’m not giving health advice.
Talk to a doctor if you are considering taking any medication, not a Python developer.</p>]]></content><author><name>Will McGugan</name></author><category term="personal" /><category term="weight-loss" /><summary type="html"><![CDATA[This is a personal account of my experiences taking weight loss medication. Quite a departure from my usual content.]]></summary></entry><entry><title type="html">The Toad Report #2</title><link href="http://willmcgugan.github.io/toad-report-2/" rel="alternate" type="text/html" title="The Toad Report #2" /><published>2025-11-01T00:00:00+00:00</published><updated>2025-11-01T00:00:00+00:00</updated><id>http://willmcgugan.github.io/toad-report-2</id><content type="html" xml:base="http://willmcgugan.github.io/toad-report-2/"><![CDATA[<p>Welcome to the second issue of The Toad Report. If you are new here, Toad is a universal interface for AI I am currently building.</p>

<p>Things are moving (hopping) fast in the Toad world.
I recently implemented the <a href="https://agentclientprotocol.com/protocol/initialization">Agent Client Protocol</a> in Toad, which means that Toad can now act as a front-end for Gemini, Claude, Codex, and a variety of other agentic coding solutions.</p>

<p>This is very exciting, as you can now use all these services from a single terminal interface.
And the UX is already leaps and hops ahead of big tech’s CLIs.</p>

<p>Here is a video I recorded of Toad running Claude and Gemini head-to-head.</p>

<iframe width="100%" style="aspect-ratio:16/11;" src="https://www.youtube.com/embed/OGGVdPZTc8E" title="claude v gemini" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<h2 id="welcome-openhands">Welcome OpenHands</h2>

<p>I’m very happy to welcome <a href="https://github.com/OpenHands/OpenHands">OpenHands</a> as my first corporate sponsor!
These guys are pioneers in agentic coding, and I look forward to getting OpenHands running on Toad.</p>

<h2 id="toads-shell">Toad’s shell</h2>

<p>Claude tells me that there is no species of toad that has a shell.
But Toad with a capital T does.</p>

<p>Having an integrated shell within agentic coding tools just makes sense.
But if Toad’s shell can’t match the basic features developers have developed muscle memory for, then it may as well not have a shell at all.
So in the last week I have added a number of traditional shell features.</p>

<p>The first is <em>history</em>: The ability to navigate through your recent shell commands and potentially re-use one.
Pressing <code class="language-plaintext highlighter-rouge">up</code> or <code class="language-plaintext highlighter-rouge">down</code> in Toad’s prompt will move through the history.
Here it is in action:</p>

<p><img src="../images/toadreport2/history.gif" alt="Toad History" /></p>

<p>Toad will offer to auto-complete commands from the history (in addition to some pre-configured favorites).
Hitting <code class="language-plaintext highlighter-rouge">return</code> will accept the suggestion, while <code class="language-plaintext highlighter-rouge">escape</code> will dismiss it.</p>

<p>Here is Toad auto-completing shell commands:</p>

<p><img src="../images/toadreport2/auto-complete-shell.gif" alt="Toad auto complete shell" /></p>

<p>Finally, still in the realm of auto-completion, Toad will auto-complete filenames and paths.
If you hit <code class="language-plaintext highlighter-rouge">tab</code>, then Toad will look for any matching files or directories.
If it finds a single file, it will insert that filename.
If it finds more than one, it will insert any common characters and suggest the remaining characters in the filename.
You can hit <code class="language-plaintext highlighter-rouge">tab</code> again to cycle through those suggestions, and <code class="language-plaintext highlighter-rouge">return</code> or <code class="language-plaintext highlighter-rouge">escape</code> to accept or cancel.</p>

<p>Here is path auto-completion in action:</p>

<p><img src="../images/toadreport2/autocomplete.gif" alt="Toad auto complete paths" /></p>

<h2 id="found-this-interesting">Found this interesting?</h2>

<p>Follow me on the socials where I will be posting regular updates.
You can also join the <a href="https://discord.gg/Enf6Z3qhVr">Textual Discord Server</a> if you want to chat with me or the Textual community.</p>

<p>Join the <code class="language-plaintext highlighter-rouge">#toad</code> channel if you would like an invite to the Toad repository. I’ll be sending more out in a week or two.</p>

<p>Thanks for reading!</p>]]></content><author><name>Will McGugan</name></author><category term="tech" /><category term="toad" /><category term="ai" /><summary type="html"><![CDATA[Welcome to the second issue of The Toad Report. If you are new here, Toad is a universal interface for AI I am currently building.]]></summary></entry><entry><title type="html">Three fascinating Toad facts (the last one blew my mind)</title><link href="http://willmcgugan.github.io/facts-about-toads/" rel="alternate" type="text/html" title="Three fascinating Toad facts (the last one blew my mind)" /><published>2025-09-28T00:00:00+00:00</published><updated>2025-09-28T00:00:00+00:00</updated><id>http://willmcgugan.github.io/facts-about-toads</id><content type="html" xml:base="http://willmcgugan.github.io/facts-about-toads/"><![CDATA[<h3 id="toads-are-frogs">Toads are frogs</h3>

<p>Toads are a <em>common name</em> given to certain species of frog, which are characterized by dry leathery skin, short legs, and a bumps on their back.
But “toad” is not a biological classification; two toads of different species may not be more closely related that a frog and a toad.
A possible exception would be the Bufonidae family in which all members are known as toads (although some may also be called frogs).</p>

<h3 id="toads-are-long-lived">Toads are long lived</h3>

<p>Toads can be surprisingly long lived, reaching 10-12 years in the wild. While others have been recorded living 30 years in captivity.</p>

<h3 id="toad-has-agent-client-protocol-support">Toad has Agent Client Protocol support</h3>

<p>The universal agentic coding application currently known as Toad has added support for <a href="https://agentclientprotocol.com/overview/introduction">Agent Client Protocol</a>, developed by <a href="https://zed.dev/">Zed industries</a>.</p>

<p>Toad now provides an alternative terminal-based front-end to Gemini CLI, and Claude CLI, with no flicker and a greatly enhanced User Interface.</p>

<iframe width="100%" style="aspect-ratio:3840/2160" src="https://www.youtube.com/embed/TzBLW2eFmag" title="Introduction to agentic coding with Toad" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>For further updates and chat about amphibians, join the <code class="language-plaintext highlighter-rouge">#toad</code> channel on the <a href="https://discord.gg/Enf6Z3qhVr">Textualize Discord Server</a>.</p>]]></content><author><name>Will McGugan</name></author><category term="tech" /><category term="toad" /><category term="AI" /><summary type="html"><![CDATA[Toads are frogs]]></summary></entry><entry><title type="html">The Toad Report #1</title><link href="http://willmcgugan.github.io/toad-report-1/" rel="alternate" type="text/html" title="The Toad Report #1" /><published>2025-08-28T00:00:00+00:00</published><updated>2025-08-28T00:00:00+00:00</updated><id>http://willmcgugan.github.io/toad-report-1</id><content type="html" xml:base="http://willmcgugan.github.io/toad-report-1/"><![CDATA[<p>Welcome to the inaugural issue of the Toad Report, an irregular series where I document updates to Toad—my terminal interface for agentic coding and all things AI.</p>

<p>To quickly recap: Toad is intended to be a <em>universal</em> front-end to AI services, which is entirely agnostic to the models and providers, while providing an altogether more humane user-experience.</p>

<iframe width="100%" style="aspect-ratio:16/11;" src="https://www.youtube.com/embed/fSE44AuiC8k" title="Toad Report 1 - video demonstration" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>Recently I’ve been working on the prompt input, which will be the focus of this post.</p>

<h2 id="the-prompt-input">The Prompt Input</h2>

<p>Like all good things in computing, Toad presents you with a flashing cursor and space to write something.</p>

<p>Toad’s text input follows a number of conventions established by agentic coding tools.
There is plenty of room for innovation here, but if it ain’t broke don’t fix it — so the interactions will feel quite familiar to most devs (although I do intend to address a number of usability and quality of life issues left on the table).</p>

<p><img src="../images/toadreport1/prompt.gif" alt="The flashing cursor is the software developer's chisel and the empty prompt their block of marble" /></p>

<p>The input is a competent text area with much of what you might expect from a browser or desktop app.
You can select text with both the mouse and the cursor, cut, copy, paste, undo and redo.</p>

<p><img src="../images/toadreport1/babies.gif" alt="Toad Text Area" /></p>

<p>The input highlights Markdown quite well.
You can see this occurring in the previous animation, but it really comes in to its own when writing code fences where it can highlight a variety of different languages.
In the future I can see code fences grow a bunch of editing features like smart indentation and LSP support.</p>

<p><img src="../images/toadreport1/fib.gif" alt="Syntax highlighting" /></p>

<p>The default mode for the input is for editing the “prompt” for the large language model.
If you want to enter a shell command you can type a “!” or “$” to enter shell mode.
The prompt symbol and the highlight color changes, so you know can be sure which mode you are in.</p>

<p><img src="../images/toadreport1/shell.gif" alt="Shell commands" /></p>

<p>Additionally, there are a number of blessed commands (which will ultimately be editable in the config) that trigger shell mode.
For the most part Toad will know wether you want to run a command or send a prompt to the AI.
And if you ever do want to send something which looks like a shell command to the LLM, you can hit escape to cancel shell mode.</p>

<p>Typing a forward slash brings up the slash command auto-complete.
You can navigate the available slash commands by moving the cursor through the list, or continue typing and have it narrow down the search, then right to insert the suggested command.</p>

<p>I haven’t implemented any slash commands as yet, but I expect this list to grow.
I also plan on implementing the custom command system supported by other agentic coding tools.</p>

<p><img src="../images/toadreport1/slash.gif" alt="Slash commands" /></p>

<p>Hitting the <code class="language-plaintext highlighter-rouge">@</code> key brings up a list of all the files in the project.
These can be refined by entering a <em>fuzzy</em> search, which matches paths with the letters you enter, but not necessarily contiguously.
This is way more intuitive that in sounds: you can start typing the name of the file and it will typically put it at the top of the list after a few keystrokes, or a enter a few characters from elsewhere in the path to match a subdirectory.</p>

<p><img src="../images/toadreport1/files.gif" alt="Files" /></p>

<p>There is also a tree view of the project docked to the left side of the screen.
While not primarily designed to add paths to the prompt, it would be a nice feature to select a file in the tree and have it populate the prompt.</p>

<h2 id="found-this-interesting">Found this interesting?</h2>

<p>Follow me on the socials where I will be posting regular updates.
You can also join the <a href="https://discord.gg/Enf6Z3qhVr">Textual Discord Server</a> if you want to chat with me or the Textual community.</p>]]></content><author><name>Will McGugan</name></author><category term="tech" /><category term="toad" /><category term="ai" /><summary type="html"><![CDATA[Welcome to the inaugural issue of the Toad Report, an irregular series where I document updates to Toad—my terminal interface for agentic coding and all things AI.]]></summary></entry><entry><title type="html">Why I ban users from my repositories</title><link href="http://willmcgugan.github.io/banning-github-users/" rel="alternate" type="text/html" title="Why I ban users from my repositories" /><published>2025-07-25T00:00:00+00:00</published><updated>2025-07-25T00:00:00+00:00</updated><id>http://willmcgugan.github.io/banning-github-users</id><content type="html" xml:base="http://willmcgugan.github.io/banning-github-users/"><![CDATA[<p>I’ve been maintaining various Open Source projects for more than a decade now.
In that time I have had countless interactions with users reporting issues and submitting pull requests.
The <em>vast</em> majority of these interactions are positive, polite, and constructive.
In fact, it is these interactions which make me continue to do the work.</p>

<p>A few haven’t been so pleasant, and I have banned a subset of the users involved.
If we exclude spam, I think the number of users I have banned over the years may still be a single figure.</p>

<p>There are three broad categories of banned users, listed below.</p>

<h2 id="spammers">Spammers</h2>

<p>I’m sure there is a place for links to sites that sell male enhancement pills, but my repositories are not it.
Spam earns you an instant ban.
This doesn’t happen all that often as GitHub is quite proactive about dealing with spam.
Often I’ll get a notification, but by the time I open it the spammer’s account will have been deleted.</p>

<p>As well as the usual spam nonsense, I’ve had some other weird stuff posted on my repositories.
I recall one user that posted lengthy conspiracy theories, with something about terminals intertwined.
I’m pretty sure this user wasn’t a spammer in the traditional sense, and was suffering from psychological issues.
I had no choice but to ban them, but I genuinely hope they found treatment for their condition.</p>

<h2 id="venting">Venting</h2>

<p>A venting post almost always starts with “I spent X hours / days on this”.
Such users want me to know how much I have inconvenienced them and they are rarely genuine in asking for help.</p>

<p>I try to be generous, but that phrase tends to put me on the defensive.
I’m not going to treat the issue as a priority, and when I do respond it will be a tad more snarky that usual.</p>

<p>Venting posts are almost always a skill issue in behalf of the user.
The user has either misread the docs or not read them at all, and they have consequently made some invalid assumptions about how the API should work.
They couldn’t get their code to work, because it was never intended to work in they way they were using it.
Rather than stepping back to reconsider their approach, or read the docs again, they want to shift the blame to me.</p>

<p>Venting is not an instant ban, because I know how frustrating programming can be.
But virtually 100% of these issues are resolved with a link to the docs, and none of reply with a “my bad” even if I suppress my snark.</p>

<p>So not an instant ban, but if venting crosses the line to personal abuse, then I’m going to be reaching for that ban button.</p>

<p>Almost everyone recognizes that an posting an issue is essentially asking a favor.
If you want somebody to help you move house, you wouldn’t start by criticizing their driving.
Same deal with issues.</p>

<h2 id="time-wasting">Time wasting</h2>

<p>This last category is trickier to define, as its not a single transgression like the others.
It’s more of a pattern of behavior that sucks time that could be better spent helping other users or writing code.</p>

<p>So what constitutes time wasting?</p>

<p>It will often start with well meaning issues that are overly long.
Pages and pages of text that don’t clearly describe the problem the user is tackling.
In the end, the issue typically boils down to a perfectly legitimate “it crashes when I do this”.
But it can take a lot of fruitless back and forth to get there.</p>

<p>One time I can overlook.
But if it keeps happening, I can feel like I am essentially working for this user at the expense of other users.</p>

<p>Related, is the user who doesn’t listen or choses not to respond to my requests.
Simple things like asking for the version of their OS or software they are using.
Details that I need to properly investigate their issue.
Sometimes it takes the form of ignoring a recommendation.
I’ll let them know there is a canonical solution to that issue, and give them a short code snippet that resolves the issue with less work, but they won’t use it or even acknowledge it.</p>

<p>However the most common “not listening” issue is when I ask the user <em>why</em> they are attempting the thing they need help with.
This can be vital in avoiding the <a href="https://en.wikipedia.org/wiki/XY_problem">XY Problem</a>.
If there is a better way of solving their problem then I can point them in the right direction.
But only if they tell me.
A few users have just refused to and repeatedly assert the odd thing they are trying to do is the only acceptable fix.</p>

<p>Other examples of time wasting include asking for LLM hallucinated code to work, posting lists of questions that can be answered with a skim over the docs, and posting the same questions in multiple support locations even after they have been answered (as though they will keep posting until they get a response they like)?</p>

<p>This is a tricky category, because the user can be well meaning.
So its a very high threshold to be banned for this.
I only consider it if the user is a clear net negative for the project.</p>

<h2 id="unbanning">Unbanning</h2>

<p>I so rarely ban users, and when I do it’s for the good of the project.
But I don’t hold grudges.
Other than spammers, if anyone I have banned wants to contribute I am happy to un-ban if they reach out.
No apology required, but I will require they avoid the ban categories above.</p>

<h2 id="open-source-is-awesome">Open Source is awesome</h2>

<p>I hope this post doesn’t sound too much like whinging.</p>

<p>I’d like to end by stressing that the community is what makes Open Source awesome.
Everyone benefits by contributing in their own way.</p>]]></content><author><name>Will McGugan</name></author><category term="tech" /><summary type="html"><![CDATA[I’ve been maintaining various Open Source projects for more than a decade now. In that time I have had countless interactions with users reporting issues and submitting pull requests. The vast majority of these interactions are positive, polite, and constructive. In fact, it is these interactions which make me continue to do the work.]]></summary></entry><entry><title type="html">Efficient streaming of Markdown in the terminal</title><link href="http://willmcgugan.github.io/streaming-markdown/" rel="alternate" type="text/html" title="Efficient streaming of Markdown in the terminal" /><published>2025-07-24T00:00:00+00:00</published><updated>2025-07-24T00:00:00+00:00</updated><id>http://willmcgugan.github.io/streaming-markdown</id><content type="html" xml:base="http://willmcgugan.github.io/streaming-markdown/"><![CDATA[<p>While working on <a href="../announcing-toad/">Toad</a>, it occurred to me there was a missing feature I would need.
Namely <em>streaming markdown</em>.</p>

<p>When talking to an LLM via an API, the Markdown doesn’t arrive all at once.
Rather you get fragments of markdown (known as tokens) which should be appended to an existing document.
Until recently the only way to render this in Textual was to remove the Markdown widget and add it again with the updated markdown.
This worked, but it would get slower to append content as the document grew.
It wasn’t a scalable solution.</p>

<p>Fortunately there are a number of optimizations which made markdown streaming scalable to massive documents.
I would expect these <em>tricks</em> to be equally applicable in the browser.</p>

<p>Here’s Textual’s new Markdown streaming in action:</p>

<iframe width="100%" style="aspect-ratio: 1512 / 982;" src="https://www.youtube.com/embed/PzkOAkvtF40" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>In a Textual Markdown widget, every part of the output it also a widget.
In other words, every paragraph, code fence, and table is a independent widget in its own right, with its own event loop.
Since the bottleneck was adding and removing these widgets, any solution would have to avoid or dramatically reduce the number of times that needed to occur.</p>

<h3 id="optimization-1">Optimization 1.</h3>

<p>The Python library I use for Markdown parsing, <a href="https://markdown-it-py.readthedocs.io/en/latest/">markdown-it-py</a>, doesn’t support any kind of streaming.
This turned out to be a non-issue as it is possible to build streaming on top of it (and probably any Markdown library).
Markdown documents can be neatly divided in to top-level blocks, like a header, paragraph, code fence, table etc.
When you add to the document, only the very last block can change.
You can consider the blocks prior to the last to be finalized.</p>

<p>This observation lead me to working on an optimization to avoid removing and re-creating these finalized blocks.
But there was a sticking point: the last block can change its type when you add new content.
Consider a table where the first tokens add the headers to the table.
The parser considers that text to be a simple paragraph block up until the entire row has arrived, and then all-of-a-sudden the paragraph becomes a table.
Once I took that into account, it worked.
It was a massive win and streaming became more practical.</p>

<h3 id="optimization-2">Optimization 2.</h3>

<p>The next step was to avoid replacing even the last widget on new content.
This is unavoidable if the last block changes type, but if it didn’t, I could add new content without replacing the widget (a far simpler operation in Textual).
The paragraph block, for instance, was trivial to update.
As was the code fence.
The table was more complicated, but still doable.
Replacing even a single widget per token could be expensive when new tokens arrive 100 times a second or higher.
So this update was a decent win.</p>

<h3 id="optimization-3">Optimization 3.</h3>

<p>I could possibly have left it there, but there was one more outstanding optimization.
Markdown-it-py is an excellent library and really quite fast, but a very large document could take a few milliseconds to parse.
Multiply that by 100 tokens a second and it becomes significant.</p>

<p>I could reduce that parsing cost dramatically by only considering the last block in the document.
All it took was to store the line number where that last block began, and feed the parser the data from there to the end of the document.
This update meant that no matter how large the document is, parsing was always sub 1ms.</p>

<h3 id="optimization-4">Optimization 4.</h3>

<p>This final optimization occurs not within the Markdown widget itself, but at a level above.</p>

<p>No matter how optimized appending to the markdown widget is, tokens could arrive faster than they can be displayed.
A naive solution would queue up these updates, so you see all the intermediate steps.
The problem with that is that your UI can be dutifully scrolling through content for many seconds after it has arrived.
I say if you have the output, let the user see it without delay.
They probably paid for the tokens, after all.</p>

<p>I fixed this with a buffer between the producer (the LLM) and the consumer (the Markdown widget).
When new tokens arrive before the previous update has finished, they are concatenated and stored until the widget is ready for them.
The end result is that the display is only ever a few milliseconds behind the data itself.</p>

<h3 id="get-the-code">Get the code</h3>

<p>This work will shortly land in the <a href="https://github.com/textualize/textual">Textual repository</a>.
I would expect this to be a common occurrence working on Toad.
If it belongs in the core library it goes in the core library, so expect some Toad features being available in Textual prior to the release of Toad itself.</p>]]></content><author><name>Will McGugan</name></author><category term="tech" /><category term="markdown" /><category term="toad" /><summary type="html"><![CDATA[While working on Toad, it occurred to me there was a missing feature I would need. Namely streaming markdown. When talking to an LLM via an API, the Markdown doesn’t arrive all at once. Rather you get fragments of markdown (known as tokens) which should be appended to an existing document. Until recently the only way to render this in Textual was to remove the Markdown widget and add it again with the updated markdown. This worked, but it would get slower to append content as the document grew. It wasn’t a scalable solution.]]></summary></entry></feed>