<?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 benchmarked 14 AI coding agents on the same task — same model, wildly different results</title>
      <dc:creator>cuculhart</dc:creator>
      <pubDate>Mon, 19 Oct 2026 13:19:50 +0000</pubDate>
      <link>https://dev.to/cuculhart/i-benchmarked-14-ai-coding-agents-on-the-same-task-same-model-wildly-different-results-16o8</link>
      <guid>https://dev.to/cuculhart/i-benchmarked-14-ai-coding-agents-on-the-same-task-same-model-wildly-different-results-16o8</guid>
      <description>&lt;p&gt;In my last post, benchmarking a small local LLM exposed a bug in my own harness — the "weak model" was partly my code. That raised the obvious next question: &lt;strong&gt;how much does the tool around the model matter, measured across many tools?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I ran &lt;strong&gt;14 agent-type AI coding tools&lt;/strong&gt; through the same task set, on the same ordinary PC (Windows 11, Ryzen 5 4500U — no GPU, nothing special), with two model backends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BYOK&lt;/strong&gt;: gemini-3.5-flash-lite via each user's own API key&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local LLM&lt;/strong&gt;: qwen3.5:4b on Ollama&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: agent-type tools only — things that autonomously read files, edit files, and run commands. Chat assistants and subscription products (Copilot, Claude Code, Cursor, Devin) are out of scope.&lt;/p&gt;

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

&lt;p&gt;Real tasks: create a file, search/read and answer, perform a precise edit, diagnose+fix a failing test. Success rates count only the &lt;strong&gt;last 2 attempts per task&lt;/strong&gt;, and results identical to baseline ("did nothing but claimed done") are marked failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  BYOK results (gemini-3.5-flash-lite)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Passes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cline (VS Code ext)&lt;/td&gt;
&lt;td&gt;7/7 (100%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kilo Code, OpenHands, OpenCode, Aider&lt;/td&gt;
&lt;td&gt;6/6 (100%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teaspoon IDE (standalone Electron)&lt;/td&gt;
&lt;td&gt;6/7 (86%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zoo Code, Goose&lt;/td&gt;
&lt;td&gt;5/6 (83%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continue&lt;/td&gt;
&lt;td&gt;2/5 (40%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SWE-agent, Void, Roo Code&lt;/td&gt;
&lt;td&gt;0–1 attempts (0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same model, same tasks — and the spread is 100% → 0%. The model is constant; the &lt;strong&gt;harness&lt;/strong&gt; (prompt assembly, tool-call format, retry logic, context handling) is what varies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local LLM results (qwen3.5:4b, Ollama)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Passes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cline&lt;/td&gt;
&lt;td&gt;6/9 (67%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teaspoon IDE&lt;/td&gt;
&lt;td&gt;6/9 (67%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Void&lt;/td&gt;
&lt;td&gt;2/3 (67%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aider&lt;/td&gt;
&lt;td&gt;2/4 (50%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zoo Code, Kilo Code, Continue, OpenCode, Goose&lt;/td&gt;
&lt;td&gt;~25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roo Code, OpenHands, Devika, Plandex&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The weak local model is a harsher judge: tools whose harnesses were tuned for strong cloud APIs fall apart (brittle tool-call formats, no retry on malformed output), while tools with tolerant parsing and explicit command protocols still complete tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers actually show
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"This model is good/bad" claims usually include the harness.&lt;/strong&gt; Same backbone model, different success rate — that's tool-side design, not intelligence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small local models are the best harness test.&lt;/strong&gt; A 4B on CPU has no slack; every weakness in the agent loop surfaces as a visible failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead ecosystems show up in measurements, not marketing.&lt;/strong&gt; Roo Code's repo is archived; attempts got no response on either backend — the bench records it as "not evaluable" instead of pretending otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-vs-AI reviews add a second axis.&lt;/strong&gt; I also had ChatGPT, Gemini, Claude, and Devin (SWE-2) score each tool's beginner-friendliness 0–5 by reading the measured data: Teaspoon IDE 3.8, Cline 3.5, everything else ≤2.8.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Scores reflect &lt;em&gt;the last two runs&lt;/em&gt; — small n by design (agent runs are slow on this hardware). Full per-attempt history is published per tool.&lt;/li&gt;
&lt;li&gt;SWE-agent and Plandex hit environment constraints (Windows/autocrlf, no native build) — recorded as not evaluable, not failed.&lt;/li&gt;
&lt;li&gt;I'm the author of one tool in the table (Teaspoon IDE). The measurement protocol is identical for all tools and the raw logs are public — judge for yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Full tables, feature matrix, and per-tool detail pages: &lt;a href="https://labo.mond.jp/articles/editor-benchmark.en.html" rel="noopener noreferrer"&gt;https://labo.mond.jp/articles/editor-benchmark.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;All site content: &lt;a href="https://labo.mond.jp/index.en.html" rel="noopener noreferrer"&gt;https://labo.mond.jp/index.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;My previous post (the harness bug that started this): &lt;a href="https://dev.to/cuculhart/i-thought-the-local-llm-was-weak-it-was-a-bug-in-my-own-harness-5dg0"&gt;https://dev.to/cuculhart/i-thought-the-local-llm-was-weak-it-was-a-bug-in-my-own-harness-5dg0&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're choosing an agent tool, don't ask "which model is it using" — ask "what does the harness do when the model misbehaves." That's where the success rate lives.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I thought the local LLM was weak — it was a bug in my own harness</title>
      <dc:creator>cuculhart</dc:creator>
      <pubDate>Sun, 18 Oct 2026 04:12:15 +0000</pubDate>
      <link>https://dev.to/cuculhart/i-thought-the-local-llm-was-weak-it-was-a-bug-in-my-own-harness-5dg0</link>
      <guid>https://dev.to/cuculhart/i-thought-the-local-llm-was-weak-it-was-a-bug-in-my-own-harness-5dg0</guid>
      <description>&lt;p&gt;I've been benchmarking local LLMs as &lt;strong&gt;coding agents&lt;/strong&gt; inside my own IDE (Teaspoon IDE) on a CPU-only machine — Ryzen 5 4500U, 32 GB RAM, no GPU, Ollama.&lt;/p&gt;

&lt;p&gt;One symptom kept showing up across models: the agent would "re-read the same file over and over," or claim "I read it, but the contents aren't visible." Classic weak-model behavior, right?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It was a bug in my own harness.&lt;/strong&gt; This is the record of how a 4B model exposed it — and how a control experiment proved it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure that started it
&lt;/h2&gt;

&lt;p&gt;The task (T3): &lt;em&gt;"Running pytest shows one failure. Find the cause, fix it, and confirm all pass."&lt;/em&gt; I gave it to &lt;code&gt;qwen3.5:4b&lt;/code&gt; — a small, fast 4B model — on my old harness.&lt;/p&gt;

&lt;p&gt;What happened:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;READ_FILE: test_calc.py&lt;/code&gt; — reads it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;READ_FILE: calc.py&lt;/code&gt; — reads it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;READ_FILE: test_calc.py&lt;/code&gt; — &lt;strong&gt;reads it again&lt;/strong&gt;. My dedup logic answers: "unchanged — use the content you already read"&lt;/li&gt;
&lt;li&gt;The model replies: &lt;strong&gt;"I read them, but the contents aren't visible here"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Tries &lt;code&gt;GREP&lt;/code&gt;, gets 0 hits → re-reads again → burns all 6 turns and fails&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I was telling it "unchanged," and it was insisting it couldn't see the contents. My first assumption: the 4B just isn't smart enough to follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The investigation: tool results survived exactly one turn
&lt;/h2&gt;

&lt;p&gt;Tracing the code revealed the flaw. The agent loop accumulated history like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User messages → kept ✓&lt;/li&gt;
&lt;li&gt;Assistant replies → kept ✓&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool results (READ_FILE contents, command output) → present in context for exactly ONE turn after execution, then silently dropped&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step 1 reads the file → step 2 can see the content → everything works&lt;/li&gt;
&lt;li&gt;From step 3 on, that content has &lt;strong&gt;already vanished from context&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;My duplicate-read suppression — "unchanged, use what you have" — was pointing at data &lt;strong&gt;the model did not have&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model's "I can't see the contents" was an &lt;strong&gt;accurate status report&lt;/strong&gt;. It knew it had read the file (its own past READ_FILE commands were in history) but the payload was gone. It was debugging my context assembly correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus discovery: past evaluations needed rewriting
&lt;/h2&gt;

&lt;p&gt;This changed how earlier runs looked. My 9B model (&lt;code&gt;qwen3.5:9b&lt;/code&gt;) had been rated as "redundantly re-reading files" — 2 of 7 steps wasted on the old harness.&lt;/p&gt;

&lt;p&gt;That wasn't weak memory. It was &lt;strong&gt;legitimate self-repair&lt;/strong&gt;: fetching content that had disappeared. The smarter models were working around my bug. The weak one just reported it honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix (and a second bug it surfaced)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Persist tool results into running history&lt;/strong&gt; — each step's tool output now stays in context for the whole run, so "unchanged" finally tells the truth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strip outer quotes from GREP patterns&lt;/strong&gt; — models kept emitting &lt;code&gt;GREP "member.*discount"&lt;/code&gt; and the literal &lt;code&gt;"&lt;/code&gt; characters ended up in the regex, matching nothing. Seven turns of re-quoting, zero hits. The parser now strips surrounding quotes, and a zero-match GREP reply carries a hint to simplify the pattern or just READ the file&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The control experiment
&lt;/h2&gt;

&lt;p&gt;Same model, same task, same prompts — only the harness fix differs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4b · old harness&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;All 6 turns burned · re-read loop · failed&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4b · fixed&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Completed in 5 turns&lt;/strong&gt; (pytest → read → fix → re-verify, correct)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9b · old harness&lt;/td&gt;
&lt;td&gt;7 steps with 2 re-reads (self-repairing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9b · fixed&lt;/td&gt;
&lt;td&gt;3 steps, &lt;strong&gt;zero re-reads&lt;/strong&gt;, shortest path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e4b · fixed&lt;/td&gt;
&lt;td&gt;4 turns, zero re-reads, clean&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same 4B model went from "hopeless" to "clean shortest-path completion" — just by fixing the code around it.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Model weakness" and "harness weakness" are inseparable until you check.&lt;/strong&gt; Every "this model is useless" article may actually be rating the harness's constraints. If you build agent tools, suspect what's in the context before you suspect the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedup/context-saving must be designed against what the model can actually see.&lt;/strong&gt; "It read it before" is only true while that content is physically in the prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak models are the best harness-testers.&lt;/strong&gt; The strong ones paper over defects with self-repair; the 4B, with no slack, faithfully visualized my flaw. If you're testing your own agent loop, run it on a marginal model.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Data
&lt;/h2&gt;

&lt;p&gt;All raw logs, task definitions, and the evaluation record are public:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full measured data + analysis: &lt;a href="https://labo.mond.jp/articles/localllm-harness-bug.en.html" rel="noopener noreferrer"&gt;https://labo.mond.jp/articles/localllm-harness-bug.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Fixtures, prompts, run artifacts: &lt;a href="https://github.com/cuculhart/ai-editor-lab/tree/main/experiments/localllm" rel="noopener noreferrer"&gt;https://github.com/cuculhart/ai-editor-lab/tree/main/experiments/localllm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The harness (Teaspoon IDE, FSL-1.1-MIT, fix shipped in v1.5.1): &lt;a href="https://github.com/cuculhart/teaspoon-ide" rel="noopener noreferrer"&gt;https://github.com/cuculhart/teaspoon-ide&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The environment is deliberately modest — an old 6-core laptop CPU, no GPU. If the harness had been worse, every model would have looked broken. Check your context before you check your model.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ollama</category>
      <category>ai</category>
      <category>debugging</category>
    </item>
    <item>
      <title>One canvas engine hosts CAD floor plans and wandering game units — position is data</title>
      <dc:creator>cuculhart</dc:creator>
      <pubDate>Fri, 16 Oct 2026 11:25:44 +0000</pubDate>
      <link>https://dev.to/cuculhart/one-canvas-engine-hosts-cad-floor-plans-and-wandering-game-units-position-is-data-3iom</link>
      <guid>https://dev.to/cuculhart/one-canvas-engine-hosts-cad-floor-plans-and-wandering-game-units-position-is-data-3iom</guid>
      <description>&lt;p&gt;A CAD floor plan of a building, and two game units wandering inside that same building. Two wildly different applications — running on the same engine.&lt;/p&gt;

&lt;p&gt;I just open-sourced &lt;strong&gt;Kimetron Isle&lt;/strong&gt;, a framework-agnostic 2D placement engine where position is data, not decoration. v1.0.0, MIT licensed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/cuculhart/kimetron" rel="noopener noreferrer"&gt;https://github.com/cuculhart/kimetron&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Playground (no install, runs in the browser): &lt;a href="https://cuculhart.github.io/kimetron/" rel="noopener noreferrer"&gt;https://cuculhart.github.io/kimetron/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The same engine, two faces
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;plan&lt;/strong&gt; — a building sketch on a 10 mm grid. Walls are thickness×length rectangles that may not overlap each other; butt joints ("|-" T junctions) are legal because edge-touching doesn't count as an overlap. A door is the declared exception that may sit on a wall. Rulers, dimension lines, wall centerlines, and architectural areas (site / building / core / inner) are all derived — move a wall and the floor area changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;game&lt;/strong&gt; — two 300 mm units wander inside the building via &lt;code&gt;engine.tryMove&lt;/code&gt;. Walls and rocks (locked, concave polygons) are impassable; the gate is the only way out. Units can't occupy the same space, and when two come within range an effect link appears — talk, attack, item exchange. The pond is a driver-side SVG pattern.&lt;/p&gt;

&lt;p&gt;Same engine in both cases. The only difference is the &lt;strong&gt;policy each driver declares&lt;/strong&gt;: "a door may overlap a wall", "rocks can't move", "units can't stack" — a few lines of driver code each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why build it
&lt;/h2&gt;

&lt;p&gt;I built two production apps (Plesfeda, Sethera Piece) on React Flow, and learned that what a canvas library does for you stops at rendering, dragging, selection, and the viewport. Layout and domain semantics were always on my side — so the coupling was thin, and a replacement was feasible.&lt;/p&gt;

&lt;p&gt;But a React Flow clone would be pointless. Every canvas/diagram library out there assumes screen coordinates, pixels, and a downward Y axis. In CAD, the coordinate itself &lt;em&gt;is&lt;/em&gt; the design data — yet no engine treats it that way. That's the gap Kimetron Isle fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  The world-coordinate model
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real units&lt;/strong&gt; — mm / m / inch / px as first-class units. Define a world where 1 unit = 1 mm; zoom is a drawing &lt;em&gt;scale&lt;/em&gt;, not a magnification&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selectable origin&lt;/strong&gt; — bottom-left + Y-up (CAD/math) or top-left + Y-down (screen)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in snapping&lt;/strong&gt; — grid, vertices, edge midpoints, centers; drivers inject custom snaps via &lt;code&gt;snapExtra&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collision &amp;amp; occupancy&lt;/strong&gt; — &lt;code&gt;solid&lt;/code&gt; objects can't overlap while edge-touching stays legal; &lt;code&gt;canOverlap&lt;/code&gt; declares pair exceptions; &lt;code&gt;collisionMode&lt;/code&gt; picks none/flag/reject&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotation &amp;amp; polygons&lt;/strong&gt; — rotation honored all the way down to OBB/SAT collision; &lt;code&gt;kind:'polygon'&lt;/code&gt; hit-tests and collides correctly even when concave&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Programmatic movement&lt;/strong&gt; — &lt;code&gt;objectsIn&lt;/code&gt;, &lt;code&gt;canPlace&lt;/code&gt;, &lt;code&gt;tryMove&lt;/code&gt; so AI-driven motion goes through the same collision policy as human gestures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Undo &amp;amp; serialization&lt;/strong&gt; — immutable model ops, document-level undo/redo, JSON serialize&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;Three layers: &lt;code&gt;@kimetron/core&lt;/code&gt; (vanilla TypeScript — model, geometry, selection, drag, hit-testing, no DOM), &lt;code&gt;@kimetron/dom&lt;/code&gt; (SVG/CSS renderer + pointer input), &lt;code&gt;@kimetron/react&lt;/code&gt; (a thin adapter).&lt;/p&gt;

&lt;p&gt;Semantics live on the user's side: Kimetron assigns meaning only up to geometry and interaction. That's exactly why a floor-plan editor and a wandering-unit demo can share one engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest status
&lt;/h2&gt;

&lt;p&gt;v1.0.0 is a stability commitment — additive-only changes since 0.6.0. There are still gaps: associated dimensions, named layers, and block references are 1.x territory; Svelte/Vue adapters are "structurally possible" rather than built; collaboration and cloud storage are deliberately out of scope.&lt;/p&gt;

&lt;p&gt;If you're building something where placement itself carries meaning — floor plans, equipment layouts, seating charts, panel assignments, game maps — I'd love to hear what you'd put on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/cuculhart/kimetron" rel="noopener noreferrer"&gt;https://github.com/cuculhart/kimetron&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Playground: &lt;a href="https://cuculhart.github.io/kimetron/" rel="noopener noreferrer"&gt;https://cuculhart.github.io/kimetron/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Product page: &lt;a href="https://cuculhart.com/oss/kimetron/index.en.html" rel="noopener noreferrer"&gt;https://cuculhart.com/oss/kimetron/index.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>typescript</category>
      <category>react</category>
      <category>opensource</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Most "WBS tools" are secretly Gantt editors — I built a board for real Work Breakdown Structures</title>
      <dc:creator>cuculhart</dc:creator>
      <pubDate>Wed, 14 Oct 2026 12:37:36 +0000</pubDate>
      <link>https://dev.to/cuculhart/most-wbs-tools-are-secretly-gantt-editors-i-built-a-board-for-real-work-breakdown-structures-3id</link>
      <guid>https://dev.to/cuculhart/most-wbs-tools-are-secretly-gantt-editors-i-built-a-board-for-real-work-breakdown-structures-3id</guid>
      <description>&lt;p&gt;I built &lt;strong&gt;Plesfeda&lt;/strong&gt; — a board for building real Work Breakdown&lt;br&gt;
Structures. Breakdown first, schedule later. MIT-licensed, fully&lt;br&gt;
local, runs in your browser with no install.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/cuculhart/plesfeda" rel="noopener noreferrer"&gt;https://github.com/cuculhart/plesfeda&lt;/a&gt;&lt;br&gt;
Live demo: &lt;a href="https://cuculhart.github.io/plesfeda/" rel="noopener noreferrer"&gt;https://cuculhart.github.io/plesfeda/&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%2F1incwipmsbubzd3dhw2s.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%2F1incwipmsbubzd3dhw2s.png" alt="A Plesfeda board — a WBS tree with dependency edges and the critical path" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Most things called "WBS tools" are actually Gantt chart editors. You&lt;br&gt;
arrange rows and draw dates — useful, but it never builds the thing a&lt;br&gt;
WBS actually is: &lt;strong&gt;work, broken down, into a structure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Serious PM tools didn't help either. MS Project, OpenProj, LibrePlan —&lt;br&gt;
they let you move rows, but never let you pick up a whole block of work&lt;br&gt;
and hang it somewhere else in the structure. Their calendars don't even&lt;br&gt;
ship with Japanese holidays, so estimates always ended up back in Excel.&lt;/p&gt;

&lt;p&gt;Plesfeda is not a project management tool. It's a tool for calmly&lt;br&gt;
judging whether a volume of work fits inside a deadline. The&lt;br&gt;
protagonists are &lt;strong&gt;breakdown&lt;/strong&gt; and &lt;strong&gt;hierarchy&lt;/strong&gt; — dependencies,&lt;br&gt;
scheduling, and the Gantt chart are all derived output, never the&lt;br&gt;
input surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breakdown input
&lt;/h2&gt;

&lt;p&gt;The core feature is an input flow that mirrors the act of decomposing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Place a &lt;code&gt;[task]&lt;/code&gt; on the board and choose &lt;strong&gt;breakdown&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Two dotted slots appear — the first is an &lt;strong&gt;active input&lt;/strong&gt; with a
cursor, the second is a &lt;strong&gt;ghost&lt;/strong&gt; previewing the next entry.&lt;/li&gt;
&lt;li&gt;Every Enter promotes the ghost to active and sprouts a new ghost,
so children stack up at the tempo of your thinking.&lt;/li&gt;
&lt;li&gt;Walk away mid-entry and the active slot falls back to a ghost parked
at the parent's tail — click it to resume.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ghosts never persist into data until confirmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The structure is free
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reparent whole subtrees&lt;/strong&gt; — drop a node onto another node and the
entire branch moves. Drop on empty space to make it a root. Cycles
are always rejected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a forest&lt;/strong&gt; — multiple roots are first-class. Add a parent
later and adopt existing roots as children: bottom-up structuring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Derived WBS numbers&lt;/strong&gt; — every node shows &lt;code&gt;1.2.3&lt;/code&gt;-style numbering
computed from position. Reparenting renumbers automatically; nothing
is stored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deleting a parent is a choice&lt;/strong&gt; — promote its children to roots,
or delete the whole subtree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effort rollup&lt;/strong&gt; — leaves take a hand-entered effort; parents show
the recursive subtree total (Σ), so a root shows the cost of the
whole structure. Units: days or hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Structure becomes schedule
&lt;/h2&gt;

&lt;p&gt;Tasks can declare predecessors (finish-to-start). One simple rule —&lt;br&gt;
leaf earliest start = max(parent start, predecessor finishes) — drives&lt;br&gt;
a PERT/CPM forward pass that returns the structure's earliest finish.&lt;br&gt;
The toolbar always shows the project's earliest completion, and the&lt;br&gt;
&lt;strong&gt;critical path&lt;/strong&gt; is highlighted on nodes and dependency edges.&lt;/p&gt;

&lt;p&gt;Dependency cycles and ancestor/descendant dependencies are flagged as&lt;br&gt;
errors, and Gantt output stays locked until they're resolved — so a&lt;br&gt;
broken structure can't produce a plausible-looking schedule.&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%2F2jcxeggylk256oz2z9n0.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%2F2jcxeggylk256oz2z9n0.png" alt="The Gantt output — holiday-aware calendar and critical path" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Gantt is a read-only report rendered from the tree: baseline date,&lt;br&gt;
weekend/holiday exclusion (click a day header to toggle a holiday),&lt;br&gt;
milestones (zero-effort leaves render as ◆), and print-friendly&lt;br&gt;
two-dimensional pagination with repeated headers and page numbers —&lt;br&gt;
real report output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Gantt is read-only
&lt;/h2&gt;

&lt;p&gt;Typical Gantt tools let you grab a bar and drag the date. That's&lt;br&gt;
manual tail-end fitting — the structure loses the reason for the date.&lt;br&gt;
In Plesfeda you fix the &lt;strong&gt;structure&lt;/strong&gt; and look at the schedule, not&lt;br&gt;
the other way around. Read-only Gantt is a deliberate design choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fully local, by design
&lt;/h2&gt;

&lt;p&gt;Data lives in the browser's localStorage and JSON files only&lt;br&gt;
(&lt;code&gt;{app:'plesfeda', version:1}&lt;/code&gt; export/import). No account, no cloud,&lt;br&gt;
nothing is sent anywhere.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;React 19 + TypeScript + Vite&lt;/li&gt;
&lt;li&gt;Canvas on &lt;a href="https://github.com/xyflow/xyflow" rel="noopener noreferrer"&gt;@xyflow/react&lt;/a&gt;
(React Flow, MIT)&lt;/li&gt;
&lt;li&gt;English-first i18n — Japanese is a translation table keyed by the
English source; untranslated strings fall back to English&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Plesfeda&lt;/strong&gt; = Welsh &lt;em&gt;pleth&lt;/em&gt; ("weave / braid") + Old Irish &lt;em&gt;feda&lt;/em&gt;&lt;br&gt;
("woods" — also the name of the letters of Ogham, the lost Celtic&lt;br&gt;
script whose characters are named after trees). A structure woven&lt;br&gt;
from trees.&lt;/p&gt;

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

&lt;p&gt;Single-player and local-only — deliberately. Editing is mouse-first,&lt;br&gt;
so phones can look but won't enjoy it. The Gantt report caps at 400&lt;br&gt;
days of horizon. v0.1.0.&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/plesfeda/issues" rel="noopener noreferrer"&gt;https://github.com/cuculhart/plesfeda/issues&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>typescript</category>
      <category>productivity</category>
    </item>
    <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>
