<?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: cuculhart</title>
    <description>The latest articles on DEV Community by cuculhart (@cuculhart).</description>
    <link>https://dev.to/cuculhart</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%2F4139016%2F47c085f6-09b0-483a-8d84-691ffcbf4dcc.png</url>
      <title>DEV Community: cuculhart</title>
      <link>https://dev.to/cuculhart</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cuculhart"/>
    <language>en</language>
    <item>
      <title>I built a cell-less spreadsheet where you count by enclosing — free, runs in your browser</title>
      <dc:creator>cuculhart</dc:creator>
      <pubDate>Tue, 13 Oct 2026 11:11:28 +0000</pubDate>
      <link>https://dev.to/cuculhart/i-built-a-cell-less-spreadsheet-where-you-count-by-enclosing-free-runs-in-your-browser-1g79</link>
      <guid>https://dev.to/cuculhart/i-built-a-cell-less-spreadsheet-where-you-count-by-enclosing-free-runs-in-your-browser-1g79</guid>
      <description>&lt;p&gt;I built &lt;strong&gt;Sethera Piece&lt;/strong&gt; — a calculation board with no cells. You drop&lt;br&gt;
text and number "pieces" anywhere on a canvas, draw a zone around them,&lt;br&gt;
and it aggregates them live. MIT-licensed, fully local, and it runs in&lt;br&gt;
your browser with no install.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/cuculhart/sethera-piece" rel="noopener noreferrer"&gt;https://github.com/cuculhart/sethera-piece&lt;/a&gt;&lt;br&gt;
Live demo: &lt;a href="https://cuculhart.github.io/sethera-piece/" rel="noopener noreferrer"&gt;https://cuculhart.github.io/sethera-piece/&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%2Fzvpxk44rgdgjqq1f1kvy.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%2Fzvpxk44rgdgjqq1f1kvy.png" alt="An estimate built on a paper frame in Sethera Piece" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;A lot of what people do in Excel isn't actually spreadsheet work. We&lt;br&gt;
use cells as graph paper: laying out forms, sticking notes, summing&lt;br&gt;
things by hand. The grid is a constraint the job never asked for.&lt;/p&gt;

&lt;p&gt;Sethera Piece removes the grid entirely. Objects live anywhere. If you&lt;br&gt;
want a total, you &lt;strong&gt;draw a zone around the pieces&lt;/strong&gt; — spatial layout is&lt;br&gt;
the formula.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it feels like
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pieces&lt;/strong&gt; — text / number / sticky-note+number. Multi-line,
alignment, colors, sizes, borderless display.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Σ zones&lt;/strong&gt; — aggregate enclosed pieces with Sum / Count / Avg /
Max / Min. Drag a piece in or out and every zone's total updates
instantly. A piece that only partially overlaps a zone gets a red
dashed warning so it can't silently escape aggregation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calc pieces&lt;/strong&gt; — + − × ÷ with rounding modes. Wire a zone's output
handle into a calc's inputs and you get chains like
&lt;code&gt;subtotal × tax rate → total&lt;/code&gt; with zero syntax.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The screenshot above is a US-style estimate built on an A4 paper&lt;br&gt;
frame: the AMOUNT column sits inside a Σ zone (subtotal 4,800), a tax&lt;br&gt;
piece and the zone feed a calc piece that shows the total (5,196).&lt;br&gt;
Printing/PDF export captures the paper frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Color = category (SumIf / CountIf)
&lt;/h2&gt;

&lt;p&gt;The 1.0 feature I'm happiest with: pick a zone, choose a piece color&lt;br&gt;
in the inspector, and only pieces of that color are counted. Eight&lt;br&gt;
usable colors = eight categories. To mark items, multi-select them&lt;br&gt;
and batch-change the color — that's the whole workflow. No label&lt;br&gt;
matching, no strings to typo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything else in the box
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lines, rects, ellipses, arcs, curves, diagonals&lt;/strong&gt; — table frames,
flowchart connectors; grid snapping makes adjacent edges land exactly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom pieces&lt;/strong&gt; — group any combination into a reusable unit;
template library saves named selections across documents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSV/TSV round-trip with Excel&lt;/strong&gt; — paste a range or import a file
(auto-detects UTF-8/UTF-16/Shift_JIS/EUC-JP); number columns become
number pieces that aggregate immediately&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paper &amp;amp; print&lt;/strong&gt; — A4/B5/A3 portrait/landscape, print/PDF capture&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fonts&lt;/strong&gt; — bundled Noto Sans JP / Shippori Mincho / BIZ UDP Gothic /
monospace, or a custom font name with a missing-font warning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Japanese / English UI&lt;/strong&gt; — toggle in one click, auto-detects from
the browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fully local&lt;/strong&gt; — localStorage + JSON files only. Nothing is sent
anywhere. No telemetry&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why the name
&lt;/h2&gt;

&lt;p&gt;"Sethera" comes from &lt;strong&gt;Yan Tan Tethera&lt;/strong&gt;, a counting rhyme shepherds&lt;br&gt;
in northern England used to tally sheep — probably one of humanity's&lt;br&gt;
oldest aggregation functions. Enclosing pieces to count them is the&lt;br&gt;
modern version.&lt;/p&gt;

&lt;p&gt;(The other image in my head: a general moving pieces on a sand table.&lt;br&gt;
Move a piece and the situation changes — a board that recalculates&lt;br&gt;
when you move it.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;p&gt;React 19 + TypeScript + Vite, canvas on&lt;br&gt;
&lt;a href="https://github.com/xyflow/xyflow" rel="noopener noreferrer"&gt;@xyflow/react&lt;/a&gt; (React Flow, MIT).&lt;br&gt;
I picked React Flow over tldraw because tldraw's license charges for&lt;br&gt;
production use; React Flow is MIT. The aggregation core is just&lt;br&gt;
rect-containment + a filter predicate — the spatial model does the&lt;br&gt;
rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;Single-player. Real-time collaboration is still only an idea on the&lt;br&gt;
drawing board — the local-first version comes first. Editing is&lt;br&gt;
mouse-first; a phone can view but won't enjoy it.&lt;/p&gt;

&lt;h2&gt;
  
  
  License
&lt;/h2&gt;

&lt;p&gt;MIT. Feedback and issues welcome:&lt;br&gt;
&lt;a href="https://github.com/cuculhart/sethera-piece/issues" rel="noopener noreferrer"&gt;https://github.com/cuculhart/sethera-piece/issues&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>typescript</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Tokens arrived on time — the renderer didn't: measuring a 90x display lag in local-LLM streaming (and how Ollama fixed it)</title>
      <dc:creator>cuculhart</dc:creator>
      <pubDate>Sun, 11 Oct 2026 01:48:08 +0000</pubDate>
      <link>https://dev.to/cuculhart/tokens-arrived-on-time-the-renderer-didnt-measuring-a-90x-display-lag-in-local-llm-streaming-3ace</link>
      <guid>https://dev.to/cuculhart/tokens-arrived-on-time-the-renderer-didnt-measuring-a-90x-display-lag-in-local-llm-streaming-3ace</guid>
      <description>&lt;p&gt;I built a local-LLM coding IDE, and I noticed something odd: giving&lt;br&gt;
the model &lt;strong&gt;all&lt;/strong&gt; the CPU cores made the app feel &lt;em&gt;slower&lt;/em&gt;. Tokens&lt;br&gt;
were arriving at the same speed, but the UI lagged behind. So I&lt;br&gt;
measured it — and then, mid-verification, a new Ollama release&lt;br&gt;
changed the whole story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — On Ollama 0.21.0, running inference on all 6 cores vs. 4&lt;br&gt;
produced the same token rate but &lt;strong&gt;90x worse display lag (P95 181ms →&lt;br&gt;
2ms)&lt;/strong&gt; and &lt;strong&gt;6x worse UI click response (929ms → 158ms)&lt;/strong&gt;. On Ollama&lt;br&gt;
0.40.2 the gap nearly vanished, because the runner internals changed.&lt;br&gt;
I shipped the thread-cap setting anyway — here's why.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually wanted to measure
&lt;/h2&gt;

&lt;p&gt;Not generation speed. API tok/s is easy to measure, but my question&lt;br&gt;
was &lt;strong&gt;"how fast does what the API returned get onto the screen?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hypothesis: with all cores allocated, the LLM saturates the CPU, the&lt;br&gt;
renderer is starved, and incoming tokens can't be painted in time.&lt;br&gt;
With fewer cores, generation speed is nearly identical but the display&lt;br&gt;
stays smooth — so it &lt;em&gt;feels&lt;/em&gt; faster.&lt;/p&gt;

&lt;p&gt;To quantify "feel," I split the clocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wire arrival time&lt;/strong&gt; — a thin proxy between the IDE and Ollama
timestamps every NDJSON chunk. Accurate even when the renderer is
starving.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DOM render time&lt;/strong&gt; — a MutationObserver records streaming text
length over time, giving per-chunk arrival→paint lag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event-loop delay&lt;/strong&gt; — drift of a 100ms setTimeout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effective FPS&lt;/strong&gt; — requestAnimationFrame fire count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI click probe&lt;/strong&gt; — actually clicking a real UI button during
generation and timing the response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixed conditions&lt;/strong&gt; — temperature 0, fixed seed, num_predict 600,
think: false.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Environment: Ryzen 5 4500U (6 logical cores), CPU inference, Windows.&lt;br&gt;
Compared "all cores" vs "4", median over a 60s window. The harness is&lt;br&gt;
&lt;code&gt;e2e/bench-threads.mjs&lt;/code&gt; (Playwright + measurement proxy) in the&lt;br&gt;
&lt;a href="https://github.com/cuculhart/teaspoon-ide" rel="noopener noreferrer"&gt;repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: Ollama 0.21.0 — same speed, 90x more invisible
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;All 6 cores&lt;/th&gt;
&lt;th&gt;4 cores&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wire-arrival speed (≈ generation)&lt;/td&gt;
&lt;td&gt;4.6 chars/s&lt;/td&gt;
&lt;td&gt;4.7 chars/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Display lag P95 (arrival→DOM)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;181 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Display lag Max&lt;/td&gt;
&lt;td&gt;553 ms&lt;/td&gt;
&lt;td&gt;6 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event-loop delay P95&lt;/td&gt;
&lt;td&gt;70 ms&lt;/td&gt;
&lt;td&gt;13.6 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective FPS&lt;/td&gt;
&lt;td&gt;55.9&lt;/td&gt;
&lt;td&gt;60.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UI click response (real click)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;929 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;158 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three takeaways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Generation speed was identical&lt;/strong&gt; — decode is memory-bandwidth
bound, so cutting 6→4 threads didn't slow token production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Display lag diverged ~90x at P95&lt;/strong&gt; — on all cores, tokens waited
up to half a second to appear. Streaming output "backs up."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One UI click took 929ms vs 158ms (~6x)&lt;/strong&gt; — a proxy for "can you
still do light work while the LLM runs."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On the old engine the runner occupied ~5.7 cores continuously during&lt;br&gt;
generation. "4 cores feels faster" wasn't an illusion — it existed as&lt;br&gt;
real delay in the display pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: Ollama 0.40.2 — I upgraded, and the premise changed
&lt;/h2&gt;

&lt;p&gt;Before publishing, I wondered "isn't 0.21.0 old?" and re-ran on&lt;br&gt;
0.40.2. &lt;strong&gt;The gap nearly disappeared:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;num_thread=6&lt;/th&gt;
&lt;th&gt;num_thread=4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wire-arrival speed&lt;/td&gt;
&lt;td&gt;7.6 chars/s&lt;/td&gt;
&lt;td&gt;7.5 chars/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Display lag P95&lt;/td&gt;
&lt;td&gt;13 ms&lt;/td&gt;
&lt;td&gt;3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event-loop delay P95&lt;/td&gt;
&lt;td&gt;14.2 ms&lt;/td&gt;
&lt;td&gt;13.9 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective FPS&lt;/td&gt;
&lt;td&gt;59.9&lt;/td&gt;
&lt;td&gt;59.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI click response&lt;/td&gt;
&lt;td&gt;123 ms&lt;/td&gt;
&lt;td&gt;114 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same story on a 9.6GB model (lagP95: 5ms vs 3ms). Inspecting the&lt;br&gt;
runner process showed why:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Ollama 0.21.0&lt;/th&gt;
&lt;th&gt;Ollama 0.40.2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runner&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ollama runner&lt;/code&gt; (old engine)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;llama-server&lt;/code&gt; (llama.cpp)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threads, unspecified&lt;/td&gt;
&lt;td&gt;all cores (=6)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;n_threads=3&lt;/strong&gt; (measured)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actual CPU at num_thread=6&lt;/td&gt;
&lt;td&gt;~5.7 cores saturated&lt;/td&gt;
&lt;td&gt;~4.3–4.6 cores, no saturation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;default changed&lt;/strong&gt;: unspecified now means 3 threads — it never
grabs all cores in the first place.&lt;/li&gt;
&lt;li&gt;Even &lt;strong&gt;explicit num_thread=6 doesn't saturate&lt;/strong&gt; — decode is memory-
bound and the new engine's threads simply wait instead of spinning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headroom exists now&lt;/strong&gt;: 1.5–2 spare cores at all times, so the
renderer/OS/other apps can preempt. UI starvation doesn't happen.&lt;/li&gt;
&lt;li&gt;Prefill behaves the same way — UI response stayed 90–180ms during
prompt processing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "all cores make it stutter" experience was a real property of the&lt;br&gt;
old engine's design, and the new engine fixed it internally.&lt;/p&gt;

&lt;h2&gt;
  
  
  So is a thread cap useless? Not quite
&lt;/h2&gt;

&lt;p&gt;Two reasons to keep the setting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Guarantee, not tuning.&lt;/strong&gt; Engine internals change per version —
the unspecified default moved 6→3 in one release. &lt;code&gt;num_thread&lt;/code&gt; is
an explicit ceiling that stays put regardless of version, model, or
whatever else is running on the machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API reach.&lt;/strong&gt; The option only exists on Ollama's &lt;em&gt;native&lt;/em&gt;
&lt;code&gt;/api/chat&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Bonus finding: the "thinking" switch
&lt;/h2&gt;

&lt;p&gt;While measuring, I noticed the model (qwen3.5:4b, a thinking model)&lt;br&gt;
produced a long reasoning trace — input analysis, greeting candidates,&lt;br&gt;
style polish — before answering a plain "hello." On a modest PC that&lt;br&gt;
trace alone costs tens of seconds to minutes. Ollama's native API has&lt;br&gt;
a &lt;code&gt;think&lt;/code&gt; flag; &lt;code&gt;think:false&lt;/code&gt; suppresses it (verified: zero&lt;br&gt;
pre-content chunks in our runs). For practical local-LLM speed it's as&lt;br&gt;
effective a switch as the thread cap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this becomes a tooling difference
&lt;/h2&gt;

&lt;p&gt;Here's the part that turned this into a product feature: &lt;strong&gt;these&lt;br&gt;
options only exist on Ollama's native &lt;code&gt;/api/chat&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you want&lt;/th&gt;
&lt;th&gt;Ollama &lt;code&gt;/api/chat&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;OpenAI-compatible &lt;code&gt;/v1&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cap CPU cores&lt;/td&gt;
&lt;td&gt;&lt;code&gt;options.num_thread&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No way to pass it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thinking on/off&lt;/td&gt;
&lt;td&gt;&lt;code&gt;think&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No way to pass it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output length / seed&lt;/td&gt;
&lt;td&gt;&lt;code&gt;options.*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Partially&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most VS Code-extension agents don't expose "CPU core count for local&lt;br&gt;
LLM" as a setting — with an OpenAI-compatible client there's simply no&lt;br&gt;
channel to send it through. To run a local LLM without killing the&lt;br&gt;
host PC, you need a design that reaches the native API's options.&lt;/p&gt;

&lt;p&gt;I shipped this in &lt;strong&gt;Teaspoon IDE v1.2.0&lt;/strong&gt; — a free, standalone&lt;br&gt;
Electron AI coding IDE I maintain — as "CPU Threads" and "Thinking"&lt;br&gt;
settings, plus the IDE's Ollama path moved to &lt;code&gt;/api/chat&lt;/code&gt;. It's BYOK&lt;br&gt;
(bring your own Gemini key) or fully offline via Ollama, and&lt;br&gt;
source-available under FSL-1.1-MIT.&lt;br&gt;
Product page: &lt;a href="https://cuculhart.com/teaspoon.en.html" rel="noopener noreferrer"&gt;https://cuculhart.com/teaspoon.en.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;tok/s explains only half the experience&lt;/strong&gt; — same generation speed,
but starved rendering changed perceived speed by a measured 90x.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Use everything" isn't always optimal&lt;/strong&gt; — and "how much to use"
depends on engine internals that change between versions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-verify on the latest environment&lt;/strong&gt; — re-testing on the new
release showed the phenomenon was engine-dependent, which made the
article stronger, not weaker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI responsiveness is measurable&lt;/strong&gt; — split wire time and DOM time,
and "feels slow" becomes a number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local-LLM features are bounded by API reach&lt;/strong&gt; — a design limited
to the compatibility API's common denominator can't implement
&lt;code&gt;num_thread&lt;/code&gt; or &lt;code&gt;think&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Methodology: harness &lt;code&gt;e2e/bench-threads.mjs&lt;/code&gt;, raw data in&lt;br&gt;
&lt;code&gt;e2e/bench-threads-results.json&lt;/code&gt; — reproduce with&lt;br&gt;
&lt;code&gt;BENCH_THREADS=6,4 BENCH_RUNS=3 BENCH_MODEL=qwen3.5:4b node e2e/bench-threads.mjs&lt;/code&gt;.&lt;br&gt;
n=2–3 on a single Ryzen 5 4500U, so treat the exact numbers as&lt;br&gt;
one-machine data; the measurement method is the reusable part.&lt;/p&gt;

</description>
      <category>ollama</category>
      <category>ai</category>
      <category>electron</category>
      <category>performance</category>
    </item>
    <item>
      <title>I built a free, BYOK AI coding agent IDE that runs fully offline on local LLMs</title>
      <dc:creator>cuculhart</dc:creator>
      <pubDate>Wed, 23 Sep 2026 08:33:21 +0000</pubDate>
      <link>https://dev.to/cuculhart/i-built-a-free-byok-ai-coding-agent-ide-that-runs-fully-offline-on-local-llms-4bdb</link>
      <guid>https://dev.to/cuculhart/i-built-a-free-byok-ai-coding-agent-ide-that-runs-fully-offline-on-local-llms-4bdb</guid>
      <description>&lt;p&gt;I built &lt;strong&gt;Teaspoon IDE&lt;/strong&gt;, a standalone Electron app that bundles a file&lt;br&gt;
explorer, Monaco editor, AI chat, Git, and a real TTY terminal into one&lt;br&gt;
window — with an agent loop that lists, reads, searches, and edits&lt;br&gt;
files across multiple steps, all behind approval dialogs.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/cuculhart/teaspoon-ide" rel="noopener noreferrer"&gt;https://github.com/cuculhart/teaspoon-ide&lt;/a&gt;&lt;br&gt;
Setup guide: &lt;a href="https://cuculhart.com/teaspoon-howto.en.html" rel="noopener noreferrer"&gt;https://cuculhart.com/teaspoon-howto.en.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not a VS Code fork, not an extension — a single exe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;Doing contract work, I kept hitting the same walls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confidential code can't go to vendor clouds.&lt;/strong&gt; In contract work you
handle code and engineering knowledge that simply may not pass
through someone else's servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orgs that can't approve $20/mo subscriptions.&lt;/strong&gt; There are still
plenty of organizations where a monthly AI IDE subscription is a
hard sell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VS Code extension agents get tangled.&lt;/strong&gt; Extensions are convenient,
but they also collide with each other — Copilot interference being
the usual suspect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unapproved rewrites are a review burden.&lt;/strong&gt; When the AI silently
rewrites code, the human reviewer pays the cognitive cost. So every
write, diff edit, and command goes through an approval dialog, and
checkpoint/rollback undoes only the AI's changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker-in-Docker tools are heavy.&lt;/strong&gt; Nice, but a hard setup for
beginners. I wanted a single exe that just runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beginners need visible panes.&lt;/strong&gt; Explorer, editor, console, and AI
chat as separate, obvious panes is simply easier to learn.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing matched all of this — so I built it, with AI, in under 3 days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free + BYOK&lt;/strong&gt; — bring your own Gemini API key, pay Google directly
for what you use. No subscription, no markup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fully offline option&lt;/strong&gt; — point it at Ollama (Gemma, Qwen, ...) and
every request goes to localhost only. Your code never leaves the
machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No telemetry&lt;/strong&gt; — the only network calls go to the LLM endpoint you
configure. Source-available, so you can verify that yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-in-the-loop&lt;/strong&gt; — writes are confined to the project root;
file creation, diff edits, and shell commands all require approval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LiteLLM proxy support&lt;/strong&gt; — real API keys never sit on the client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token-efficient context&lt;/strong&gt; — the AI gets the file tree and fetches
contents on demand via &lt;code&gt;// GREP:&lt;/code&gt; / &lt;code&gt;// READ_FILE:&lt;/code&gt; commands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheap by default&lt;/strong&gt; — Gemini 3.8 Flash is at introductory pricing
(half the standard rate) through the end of 2026, and even cheaper
models like 3.5 Flash-Lite are selectable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;p&gt;Electron / React / TypeScript / Monaco Editor / Gemini API &amp;amp; Ollama /&lt;br&gt;
Vite&lt;/p&gt;

&lt;h2&gt;
  
  
  License
&lt;/h2&gt;

&lt;p&gt;FSL-1.1-MIT (source-available). Free to use, modify, fork, and&lt;br&gt;
redistribute for any non-competing purpose — including commercial work&lt;br&gt;
inside your company. The one restriction: you can't sell it (or a&lt;br&gt;
renamed clone) as a competing product. Each release automatically&lt;br&gt;
converts to MIT two years after publication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;It's chat/agent-centric (no inline completions), there's no codebase&lt;br&gt;
index (tree + on-demand reads instead), editor tabs don't exist yet,&lt;br&gt;
and it's Windows-first with the UI in English/Japanese.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roadmap
&lt;/h2&gt;

&lt;p&gt;Considering PDF/image recognition, making the agent loop more reliable&lt;br&gt;
on small local models, and chat improvements (multiple chat tabs).&lt;/p&gt;

&lt;p&gt;Feedback and bug reports welcome via&lt;br&gt;
&lt;a href="https://github.com/cuculhart/forger-ide/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>electron</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
