<?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: Eduard Maghakyan</title>
    <description>The latest articles on DEV Community by Eduard Maghakyan (@eduardmaghakyan).</description>
    <link>https://dev.to/eduardmaghakyan</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F113916%2Fe7f663e0-7b71-480a-9cd2-4b528c788f84.jpeg</url>
      <title>DEV Community: Eduard Maghakyan</title>
      <link>https://dev.to/eduardmaghakyan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eduardmaghakyan"/>
    <language>en</language>
    <item>
      <title>Mnemonic - local-first voice notes with Gemma 4 E4B</title>
      <dc:creator>Eduard Maghakyan</dc:creator>
      <pubDate>Sat, 16 May 2026 23:43:24 +0000</pubDate>
      <link>https://dev.to/eduardmaghakyan/mnemonic-local-first-voice-notes-with-gemma-4-e4b-3ihe</link>
      <guid>https://dev.to/eduardmaghakyan/mnemonic-local-first-voice-notes-with-gemma-4-e4b-3ihe</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Build with Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Mnemonic is a macOS menu-bar app and CLI for voice notes that go straight into your daily journal.&lt;/p&gt;

&lt;p&gt;Press a hotkey, speak, release. One bullet appears in today's &lt;code&gt;YYYY-MM-DD.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; 14:35 This is a new node. Let me try to see if it'll work. &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;../audio/2026-05-10/143500.wav&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; 15:12 I want to email Sarah tomorrow about the migration plan. &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;../audio/2026-05-10/151200.wav&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; 16:08 The bug is in how we handle the empty array case in &lt;span class="sb"&gt;`merge_chunks`&lt;/span&gt;. &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;../audio/2026-05-10/160800.wav&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole product. No titles, no summaries, no auto-generated TODO lists, no extracted entities. No cloud, no telemetry. Transcribed thoughts dropped into a Markdown file you already control.&lt;/p&gt;

&lt;p&gt;Early versions over-structured short voice memos - every 30-second thought came back with a title, a "summary" that restated what you said, and an "Actions" list that invented TODOs. v0.2 cut all of that. The model's job is now narrow: transcribe and lightly clean.&lt;/p&gt;

&lt;p&gt;v0.3 added three things on top, each either opt-in or invisible by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image attachments.&lt;/strong&gt; Take a screenshot, then speak - or use &lt;code&gt;Ctrl+Option+Cmd+Space&lt;/code&gt; to drag a region and start recording in one motion. Gemma 4 reads the WAV and the PNG together and produces one bullet referencing both. PNG saves next to the audio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recording queue.&lt;/strong&gt; Recording is decoupled from structuring. Release the hotkey, the tray goes idle, fire the next one immediately. A background worker drains an on-disk inbox serially; quitting mid-job is safe, the inbox survives. Tray is now binary - gray idle, red recording.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intent routing (opt-in, off by default).&lt;/strong&gt; A second narrow Gemma 4 call decides whether your note is asking the OS to do something - "remind me to call Sarah at 3 PM" - and if it is, fires a macOS Shortcut you've whitelisted. Undoable for 5 seconds. No AppleScript, no shell interpolation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The file format (&lt;code&gt;YYYY-MM-DD.md&lt;/code&gt; at the vault root) matches Obsidian's Daily Notes plugin, so pointing &lt;code&gt;notes_dir&lt;/code&gt; at an Obsidian vault makes bullets land in today's daily note. Graph view, backlinks, search - all free.&lt;/p&gt;

&lt;p&gt;Everything runs locally. No network call leaves the loopback interface. The DMG is signed and notarized; no telemetry crates are linked into the binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/OuFhST0VFdQ"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/EduardMaghakyan/mnemonic" rel="noopener noreferrer"&gt;github.com/EduardMaghakyan/mnemonic&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latest release (signed + notarized DMG):&lt;/strong&gt; &lt;a href="https://github.com/EduardMaghakyan/mnemonic/releases/tag/v0.3.1" rel="noopener noreferrer"&gt;v0.3.1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install via Homebrew:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  brew tap EduardMaghakyan/tap
  brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; mnemonic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rust workspace: Tauri 2 for the menu-bar app, &lt;code&gt;clap&lt;/code&gt; for the CLI, a shared &lt;code&gt;mnemonic-core&lt;/code&gt; crate for audio + markdown + the llama-server client. Single Apple Silicon DMG, code-signed with a Developer ID, notarized, and stapled. MIT licensed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Gemma 4
&lt;/h2&gt;

&lt;p&gt;Mnemonic uses three Gemma 4 capabilities through a single local model: &lt;strong&gt;native audio, native vision, and lightweight reasoning&lt;/strong&gt;. They all run against the same llama-server on &lt;code&gt;127.0.0.1&lt;/code&gt; - no second model, no external API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why E4B
&lt;/h3&gt;

&lt;p&gt;Gemma 4 ships in four sizes. Only two are audio-capable, and only one fits a 16 GB laptop. Numbers from the &lt;a href="https://ai.google.dev/gemma/docs/core/model_card_4" rel="noopener noreferrer"&gt;official Gemma 4 model card&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Audio?&lt;/th&gt;
&lt;th&gt;MMLU Pro&lt;/th&gt;
&lt;th&gt;BBEH&lt;/th&gt;
&lt;th&gt;CoVoST&lt;/th&gt;
&lt;th&gt;FLEURS (↓)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;E2B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;60.0%&lt;/td&gt;
&lt;td&gt;21.9%&lt;/td&gt;
&lt;td&gt;33.47&lt;/td&gt;
&lt;td&gt;0.09&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;E4B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;69.4%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;33.1%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;35.54&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.08&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26B A4B&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;higher&lt;/td&gt;
&lt;td&gt;higher&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31B Dense&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;highest&lt;/td&gt;
&lt;td&gt;highest&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;E2B and E4B are the only sizes with the audio encoder (~300M params). Both also ship with a ~150M vision encoder. The 26B and 31B are vision-only - no ears. "Use the biggest model that fits" is a non-starter for this product.&lt;/p&gt;

&lt;p&gt;Between E2B and E4B, the deltas matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MMLU Pro 60.0 → 69.4&lt;/strong&gt; (+9.4). The difference between a model that fumbles unfamiliar technical vocabulary in voice notes and one that doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BBEH 21.9 → 33.1&lt;/strong&gt; (+11.2). Reasoning quality matters for self-correction ("actually, scratch that…") and for intent routing - one misclassification fires the wrong Shortcut.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CoVoST 33.47 → 35.54&lt;/strong&gt; and &lt;strong&gt;FLEURS 0.09 → 0.08&lt;/strong&gt;. Modest audio-recognition wins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At Q4_K_M the E4B GGUF is &lt;strong&gt;4.98 GB&lt;/strong&gt; (&lt;a href="https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF" rel="noopener noreferrer"&gt;Hugging Face&lt;/a&gt;), plus audio and vision mmprojs (~1 GB combined). Co-resident with an IDE and browser on 16 GB.&lt;/p&gt;

&lt;h3&gt;
  
  
  One model, one pass - for both audio and vision
&lt;/h3&gt;

&lt;p&gt;The conventional architecture for this product is two stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ASR (Whisper, Parakeet, etc.) → raw transcript&lt;/li&gt;
&lt;li&gt;Text LLM → clean and structure&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Mnemonic does both in one Gemma 4 forward pass. The audio goes into the model with a system prompt that says, in effect: "transcribe this and write it the way the speaker would write it themselves." Why it works better than two stages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One model in memory, one HTTP round-trip per recording. A 2-stage version means two model downloads, two warm processes, two failure modes.&lt;/li&gt;
&lt;li&gt;The cleaning prompt operates on the audio, not on a flat transcript. The model can hear pauses, hesitation, restarts - the difference between "I think" as filler and "I think" as opinion. A downstream LLM working from a transcript has already lost that.&lt;/li&gt;
&lt;li&gt;Lower end-to-end latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same approach works for vision. A two-stage screenshot-with-voice product would be OCR (Tesseract, Apple Vision) → LLM merge. Mnemonic sends the WAV and the PNG to Gemma 4 in one multipart request, and the model produces a bullet that references both. The image isn't OCR'd in isolation - it's grounded by what the user said while taking it. Captions come out as "the panic the speaker mentioned in line 42" rather than generic "code editor with red error text."&lt;/p&gt;

&lt;h3&gt;
  
  
  Intent routing - a second narrow call, same model
&lt;/h3&gt;

&lt;p&gt;Letting a voice note fire a macOS Shortcut took some thought. I didn't want to bolt on a tools/function-calling framework, an MCP server, or anything that added attack surface for a side-effect feature running on a user's machine.&lt;/p&gt;

&lt;p&gt;What works is a second Gemma 4 call to the same llama-server, on the already-cleaned transcript, with one job - output a single JSON object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"create-reminder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call Sarah at 3 PM"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…or &lt;code&gt;{ "tool": "none" }&lt;/code&gt; if the transcript isn't a request. No tools registry, no plugin protocol - same model, same server.&lt;/p&gt;

&lt;p&gt;Most of the work is around what &lt;em&gt;doesn't&lt;/em&gt; fire:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Whitelist required.&lt;/strong&gt; Mnemonic only runs Shortcuts named in &lt;code&gt;allowed_shortcuts&lt;/code&gt;. A hallucinated name is refused before it reaches the OS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No AppleScript, no shell interpolation.&lt;/strong&gt; Input is piped to the Shortcut via stdin (&lt;code&gt;shortcuts run NAME --stdin&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Undoable for 5 seconds.&lt;/strong&gt; The tray menu shows &lt;code&gt;Undo: &amp;lt;name&amp;gt;&lt;/code&gt; for the configured window. Click it to run a paired &lt;code&gt;undo-&amp;lt;name&amp;gt;&lt;/code&gt; Shortcut.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thought-dumps don't fire.&lt;/strong&gt; "I was thinking about reminding Sarah, but maybe she already knows" → &lt;code&gt;{tool: "none"}&lt;/code&gt;. Validated at 30/30 on hand-labelled transcripts, including 15 hedged/observational cases that must not fire (&lt;code&gt;docs/spike/intent/PHASE-0-INTENT-FINDINGS.md&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notes are the source of truth.&lt;/strong&gt; A fire writes a &lt;code&gt;↳ Ran shortcut "&amp;lt;name&amp;gt;": &amp;lt;input&amp;gt;&lt;/code&gt; continuation line under the bullet. Greppable, auditable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cost is ~1.7s of warm latency per recording when enabled, off the user's critical path because the structuring queue is already async.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;

&lt;p&gt;Users run llama-server themselves, on loopback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;llama-server &lt;span class="nt"&gt;-hf&lt;/span&gt; unsloth/gemma-4-E4B-it-GGUF:Q4_K_M &lt;span class="nt"&gt;--port&lt;/span&gt; 5809 &lt;span class="nt"&gt;--mmproj-auto&lt;/span&gt; &lt;span class="nt"&gt;-ngl&lt;/span&gt; 99 &lt;span class="nt"&gt;-c&lt;/span&gt; 8192
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app posts one multipart request per recording (WAV + optional PNG + system prompt) to &lt;code&gt;127.0.0.1:5809&lt;/code&gt;, parses the JSON response, and appends a bullet. With intent routing on, a second JSON-mode call follows.&lt;/p&gt;

&lt;p&gt;A few things that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JSON mode + thinking mode.&lt;/strong&gt; &lt;code&gt;response_format: { type: "json_object" }&lt;/code&gt; for structure, &lt;code&gt;chat_template_kwargs: { thinking: true }&lt;/code&gt; for chain-of-thought. The thinking tokens cost a few hundred ms but improve handling of technical terms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The schema shrunk over three versions, then stayed put.&lt;/strong&gt; v1 had seven fields (title, tags, summary, cleaned, actions, questions, entities). v1.5 had two. v2 has one: &lt;code&gt;{ cleaned: String }&lt;/code&gt;. Each prune was a UX win - the simpler the schema, the less the model felt the urge to narrate, summarize, or invent. The system prompt explicitly bans third-party narration ("the speaker", "the user", "the recording") and includes two calibration examples. v0.3's vision and intent work added new &lt;em&gt;inputs&lt;/em&gt; and a new &lt;em&gt;second call&lt;/em&gt;, but didn't grow that schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure is visible, not silent.&lt;/strong&gt; llama-server unreachable, malformed JSON twice, silent audio - each produces a stub bullet with the timestamp and a redacted error. No recording is ever lost without a trace; the audio is preserved on disk, and the inbox queue means a job in flight survives a quit or crash.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What works well
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audio transcription and cleaning.&lt;/strong&gt; Better than the Whisper + LLM pipeline I started with. Audio-grounded cleaning preserves intent across self-correction and hesitation in a way a downstream LLM on a flat transcript can't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vision captions are grounded.&lt;/strong&gt; They answer "what was the speaker talking about" rather than describing everything in the image. Short, useful captions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intent routing is conservative.&lt;/strong&gt; It refuses to fire more often than it fires. That's the right error direction for a feature that can run OS-level actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The queue makes the app feel instant.&lt;/strong&gt; You stop waiting on the model. Tray returns to idle the moment you release the hotkey.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything is local.&lt;/strong&gt; Loopback only. No keys to manage, no quota to worry about, no data leaves the machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Processing is batched after the fact, not streamed. Fine for journaling; wouldn't work for live captioning.&lt;/li&gt;
&lt;li&gt;16 GB unified memory is the floor. With a heavy IDE + browser open, memory pressure shows.&lt;/li&gt;
&lt;li&gt;I haven't tested non-English voice notes systematically. Gemma 4 is multilingual, but I work in English.&lt;/li&gt;
&lt;li&gt;No speaker diarization, no noise suppression. By design, most voice memos are solo.&lt;/li&gt;
&lt;li&gt;Intent routing requires building macOS Shortcuts by hand. Powerful if you set it up, but most users won't.&lt;/li&gt;
&lt;li&gt;Image OCR is good for screenshots, not for dense document scans. Short captions and inline text work well; multi-column papers don't.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  UX
&lt;/h2&gt;

&lt;p&gt;The whole interaction is one keystroke. Default is &lt;code&gt;Ctrl+Option+Space&lt;/code&gt; hold-to-record (push-to-talk). Both the combo and the mode (hold or toggle) are configurable in &lt;code&gt;~/.config/mnemonic/config.toml&lt;/code&gt;, and the config hot-reloads - change a hotkey, save, the app re-registers without a restart. Tray transitions are perceptible within 100 ms.&lt;/p&gt;

&lt;p&gt;The CLI ships a &lt;code&gt;mnemonic doctor&lt;/code&gt; command with a green/red checklist for every common failure mode (mic permission, llama-server reachable, model loaded, mmproj loaded, paths writable). Brew install creates the &lt;code&gt;mnemonic&lt;/code&gt; symlink on PATH automatically - no admin password prompt anywhere in the install path.&lt;/p&gt;




&lt;p&gt;Built solo (well... not quite, Claude was involved). Source under MIT. Thanks to the Gemma team for shipping audio, vision, and chain-of-thought in a model that fits a laptop.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
    </item>
    <item>
      <title>IPE v0.1.17 - Keyboard Shortcuts, Crash Recovery &amp; macOS Fix</title>
      <dc:creator>Eduard Maghakyan</dc:creator>
      <pubDate>Thu, 16 Apr 2026 10:30:55 +0000</pubDate>
      <link>https://dev.to/eduardmaghakyan/ipe-v0117-keyboard-shortcuts-crash-recovery-macos-fix-2k9</link>
      <guid>https://dev.to/eduardmaghakyan/ipe-v0117-keyboard-shortcuts-crash-recovery-macos-fix-2k9</guid>
      <description>&lt;p&gt;A quick update on &lt;a href="https://github.com/EduardMaghakyan/ipe" rel="noopener noreferrer"&gt;IPE&lt;/a&gt; - the local PR-review UI for Claude Code plans. If you missed the original announcement, &lt;a href="https://dev.to/eduardmaghakyan/building-a-local-pr-review-interface-for-claude-code-plans-57o2"&gt;check it out here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here's what's new since v0.1.15:&lt;/p&gt;

&lt;h2&gt;
  
  
  Keyboard Shortcuts
&lt;/h2&gt;

&lt;p&gt;You can now navigate the review flow without touching the mouse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shift+Tab&lt;/strong&gt; - Accept the plan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;x&lt;/strong&gt; - Request changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;c&lt;/strong&gt; - Jump to the comment box&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;?&lt;/strong&gt; - Toggle the shortcuts overlay&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shortcut hints are displayed directly on the buttons, so you don't have to memorize them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server Crash Recovery
&lt;/h2&gt;

&lt;p&gt;If the IPE server dies unexpectedly (killed process, laptop sleep, etc.), the client now recovers gracefully instead of hanging forever. New sessions also correctly open the browser when connecting to an already-running server.&lt;/p&gt;

&lt;h2&gt;
  
  
  macOS Binary Fix
&lt;/h2&gt;

&lt;p&gt;If you upgraded to v0.1.15 or v0.1.16 and IPE suddenly stopped launching (getting &lt;code&gt;killed&lt;/code&gt; in the terminal), this was caused by macOS Gatekeeper blocking the unsigned binary. v0.1.17 fixes this - the release binary is now properly ad-hoc signed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're affected&lt;/strong&gt;, just re-run the installer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/eduardmaghakyan/ipe/main/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;More improvements coming soon. If you have feedback or ideas, &lt;a href="https://github.com/EduardMaghakyan/ipe/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt; or drop a comment below!&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Generate a PDF from JSON with one API call</title>
      <dc:creator>Eduard Maghakyan</dc:creator>
      <pubDate>Sat, 11 Apr 2026 23:43:51 +0000</pubDate>
      <link>https://dev.to/eduardmaghakyan/generate-a-pdf-from-json-with-one-api-call-5g08</link>
      <guid>https://dev.to/eduardmaghakyan/generate-a-pdf-from-json-with-one-api-call-5g08</guid>
      <description>&lt;p&gt;If you've ever needed to email a quarterly report from a cron job, attach an invoice to a Zapier workflow, or ship a weekly digest from an n8n trigger, you've hit the same wall I've been hitting for years: &lt;strong&gt;generating a PDF programmatically is weirdly hard&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The options today are all flavors of the same compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What exists in 2026
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PDFMonkey&lt;/strong&gt; - Starter €5/mo for 300 documents, Pro €15/mo for 3,000. You design your template in their visual editor, then POST variables to fill it in. Solid editor, reasonable pricing. The friction is that every new document shape means a trip back to the visual tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;APITemplate.io&lt;/strong&gt; - PDF Basic $19/mo (annual) for 3,000 PDFs. Same core idea as PDFMonkey: design a template in a web editor, then POST JSON data to render it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DocRaptor&lt;/strong&gt; - from $15/mo, powered by Prince. HTML-in, PDF-out - so no template editor, but you own the problem of producing print-ready HTML and CSS (page breaks, headers, footers, paged media rules). Powerful if you already have a styled HTML pipeline; more work if you don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rolling your own&lt;/strong&gt; - Puppeteer or Chromium in a Lambda, &lt;code&gt;@react-pdf/renderer&lt;/code&gt;, or &lt;code&gt;wkhtmltopdf&lt;/code&gt; in a Docker image. Works until you need it to not-work: font loading, CJK support, memory limits, cold starts, timeouts, and the joy of debugging headless Chrome in CI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pattern&lt;/strong&gt;: most hosted PDF APIs either hand you a template editor (design once, fill slots later) or hand you an HTML-to-PDF pipe (bring your own rendered HTML). Both are fine when the layout is known ahead of time. Both are friction when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The shape of your data is determined at runtime (e.g., an LLM decides what to render)&lt;/li&gt;
&lt;li&gt;You want multiple outputs from the same pipeline - a PDF, a web view, and a raw data table&lt;/li&gt;
&lt;li&gt;You need a dashboard with metrics + charts + tables composed together, not a static invoice&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one-API-call approach
&lt;/h2&gt;

&lt;p&gt;A few months ago I started building &lt;a href="https://genui.sh" rel="noopener noreferrer"&gt;genui.sh&lt;/a&gt; for the workflows I couldn't make the existing tools fit. The pitch is simple: POST a JSON payload describing what you want, get back a signed URL.&lt;/p&gt;

&lt;p&gt;Here's the smallest version - a markdown document rendered to a hosted PDF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://www.genui.sh/api/artifacts/share &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$GENUI_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "template": "pdf",
    "title": "Q4 Report",
    "content": {
      "text": "# Q4 2025 Performance\n\nRevenue grew **23%** year-over-year.\n\n## Highlights\n- Launched mobile app\n- Expanded to 3 new markets",
      "pageSize": "A4"
    },
    "expiresIn": "7d"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"550e8400-e29b-41d4-a716-446655440000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://genui.sh/a/550e8400-e29b-41d4-a716-446655440000?token=eyJhbGc..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"expiresAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-19T14:30:45.123Z"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;See it live:&lt;/strong&gt; &lt;a href="https://www.genui.sh/a/311f0511-376f-4221-8c4b-6193a3f0a822?token=eyJhbGciOiJIUzI1NiJ9.eyJhcnRpZmFjdElkIjoiMzExZjA1MTEtMzc2Zi00MjIxLThjNGItNjE5M2EzZjBhODIyIiwidXNlcklkIjoiZTQ2MDdjYjUtNjBlMi00YjRjLWI2MWYtMWE1YjE5YWNkZWM2IiwiaWF0IjoxNzc1OTQ3NTQ3LCJzdWIiOiIzMTFmMDUxMS0zNzZmLTQyMjEtOGM0Yi02MTkzYTNmMGE4MjIifQ.CimDhX4kiZ7Sh5I-TX-977Fm43pytkVofIjYEQ9YhyY" rel="noopener noreferrer"&gt;Q4 report PDF →&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.amazonaws.com%2Fuploads%2Farticles%2Fqmw0hi0owom22we7pg4v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqmw0hi0owom22we7pg4v.png" alt="Q4 report PDF preview" width="800" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the whole workflow. No template to design first, no visual editor to maintain, no data-shape gymnastics. The &lt;code&gt;content&lt;/code&gt; field is just markdown - the same kind you'd write in a README. The &lt;code&gt;expiresIn&lt;/code&gt; field is optional: on Pro it defaults to 30 days, so pass &lt;code&gt;"never"&lt;/code&gt; explicitly if you want a permanent link; on Free and Starter it's clamped to the plan max (7 and 30 days respectively). And &lt;code&gt;Authorization&lt;/code&gt; accepts either &lt;code&gt;Bearer &amp;lt;key&amp;gt;&lt;/code&gt; or the raw key, whichever your HTTP client prefers.&lt;/p&gt;

&lt;p&gt;If you want a table instead of prose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "template": "table",
  "title": "Recent orders",
  "content": {
    "columns": [
      {"header": "Order ID", "accessorKey": "id"},
      {"header": "Customer", "accessorKey": "customer"},
      {"header": "Amount", "accessorKey": "amount"}
    ],
    "rows": [
      {"id": "ORD-001", "customer": "Acme Corp", "amount": "$1,250"},
      {"id": "ORD-002", "customer": "Globex", "amount": "$890"},
      {"id": "ORD-003", "customer": "Initech", "amount": "$3,420"},
      {"id": "ORD-004", "customer": "Umbrella", "amount": "$560"},
      {"id": "ORD-005", "customer": "Stark Industries", "amount": "$7,800"}
    ],
    "config": {
      "enableSearch": true,
      "enablePagination": true,
      "enableExport": true
    }
  }
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;See it live:&lt;/strong&gt; &lt;a href="https://www.genui.sh/a/13162dc6-b76b-4a9b-b96f-df827adddedf?token=eyJhbGciOiJIUzI1NiJ9.eyJhcnRpZmFjdElkIjoiMTMxNjJkYzYtYjc2Yi00YTliLWI5NmYtZGY4MjdhZGRkZWRmIiwidXNlcklkIjoiZTQ2MDdjYjUtNjBlMi00YjRjLWI2MWYtMWE1YjE5YWNkZWM2IiwiaWF0IjoxNzc1OTQ3ODgxLCJzdWIiOiIxMzE2MmRjNi1iNzZiLTRhOWItYjk2Zi1kZjgyN2FkZGRlZGYifQ.Bg0vpqU5jr2bj7gLs3Sr7oHQ5TlSN8-ABjDI1Cs7l6s" rel="noopener noreferrer"&gt;Orders table →&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.amazonaws.com%2Fuploads%2Farticles%2Fczbysvy7znpp2h2br4iw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fczbysvy7znpp2h2br4iw.png" alt="Orders table preview" width="800" height="229"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Or a bar chart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "template": "chart",
  "title": "Monthly revenue",
  "content": {
    "type": "bar",
    "data": [
      {"name": "Jan", "value": 84000},
      {"name": "Feb", "value": 92000},
      {"name": "Mar", "value": 108000},
      {"name": "Apr", "value": 121000},
      {"name": "May", "value": 134000},
      {"name": "Jun", "value": 142000}
    ],
    "config": {
      "showGrid": true,
      "showLegend": true,
      "colors": ["#3b82f6"]
    }
  }
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;See it live:&lt;/strong&gt; &lt;a href="https://www.genui.sh/a/dd315369-983e-409a-9ad0-4055484e87c1?token=eyJhbGciOiJIUzI1NiJ9.eyJhcnRpZmFjdElkIjoiZGQzMTUzNjktOTgzZS00MDlhLTlhZDAtNDA1NTQ4NGU4N2MxIiwidXNlcklkIjoiZTQ2MDdjYjUtNjBlMi00YjRjLWI2MWYtMWE1YjE5YWNkZWM2IiwiaWF0IjoxNzc1OTQ3ODgzLCJzdWIiOiJkZDMxNTM2OS05ODNlLTQwOWEtOWFkMC00MDU1NDg0ZTg3YzEifQ.HJ1eOjNs0Moer0uPOSv80G4-2ce8bIlu-oGiLe1SWTY" rel="noopener noreferrer"&gt;Monthly revenue chart →&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.amazonaws.com%2Fuploads%2Farticles%2Fjbc0zliye64w1zumejif.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjbc0zliye64w1zumejif.png" alt="Monthly revenue chart preview" width="800" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same URL, same auth header, same response shape. Five output types (markdown, table, chart, pdf, and a composable dashboard format called &lt;code&gt;@std/dynamic&lt;/code&gt;) all sharing one endpoint. The chart template supports four sub-types - &lt;code&gt;line&lt;/code&gt;, &lt;code&gt;bar&lt;/code&gt;, &lt;code&gt;area&lt;/code&gt;, &lt;code&gt;pie&lt;/code&gt; - so swapping from a bar to a line chart is a one-word change. That's the one-call, multi-format part: the same POST body shape gives you a PDF, a hosted web view, or a live dashboard depending on &lt;code&gt;template&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When things go wrong, errors come back in a consistent shape too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;your&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;monthly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;artifact&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;quota&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Monthly artifact limit reached (50). Upgrade your plan for more."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FORBIDDEN"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;limited&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;req/min&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;per&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;key)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Too many requests"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RATE_LIMITED"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Composing a dashboard
&lt;/h2&gt;

&lt;p&gt;The interesting template is &lt;code&gt;@std/dynamic&lt;/code&gt;. Instead of a single content blob, you POST a tree of components - Metric, Chart, Table, Card, Stack - and the renderer composes them into a hosted page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "template": "@std/dynamic",
  "title": "Weekly snapshot",
  "content": {
    "component": "Stack",
    "props": { "gap": "md" },
    "children": [
      {
        "component": "Metric",
        "props": { "label": "Revenue", "value": "$284,000", "delta": "+12%" }
      },
      {
        "component": "Chart",
        "props": {
          "type": "line",
          "data": [
            {"day": "Mon", "visits": 1200},
            {"day": "Tue", "visits": 1450},
            {"day": "Wed", "visits": 1380}
          ],
          "config": {
            "categoryKey": "day",
            "series": [{"key": "visits", "label": "Visits"}]
          }
        }
      }
    ]
  }
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;See it live:&lt;/strong&gt; &lt;a href="https://www.genui.sh/a/b644e015-af79-4095-a925-2964daf1bec0?token=eyJhbGciOiJIUzI1NiJ9.eyJhcnRpZmFjdElkIjoiYjY0NGUwMTUtYWY3OS00MDk1LWE5MjUtMjk2NGRhZjFiZWMwIiwidXNlcklkIjoiZTQ2MDdjYjUtNjBlMi00YjRjLWI2MWYtMWE1YjE5YWNkZWM2IiwiaWF0IjoxNzc1OTQ3NTY4LCJzdWIiOiJiNjQ0ZTAxNS1hZjc5LTQwOTUtYTkyNS0yOTY0ZGFmMWJlYzAifQ.WV99zndYXRlZp4ha-jB2lutsGOO4rr36rwmDZQ9WtUw" rel="noopener noreferrer"&gt;Weekly snapshot dashboard →&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.amazonaws.com%2Fuploads%2Farticles%2F7xwjmypblcxt5lf7taqn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7xwjmypblcxt5lf7taqn.png" alt="Weekly snapshot dashboard preview" width="800" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the actual payoff: the same tree can be served as a web view today and re-POSTed as &lt;code&gt;"template": "pdf"&lt;/code&gt; tomorrow to get a printable version, without re-designing anything. If an LLM is deciding the shape of the output at runtime, this is the format you want it writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you don't have to think about
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hosting the output.&lt;/strong&gt; The returned URL is a genui.sh page that renders the artifact. No S3 bucket, no CDN config, no expired-link headache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing.&lt;/strong&gt; The URL comes pre-signed with a token you can set to expire in 1h, 30d, or never.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fonts, print styles, headers, footers.&lt;/strong&gt; The renderer handles A4/Letter layout, page breaks, and PDF metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View counts, if you care.&lt;/strong&gt; Each hosted link tracks opens; you can see them in the dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple formats from the same data.&lt;/strong&gt; If you want a PDF and a web dashboard of the same report, you just POST twice with different &lt;code&gt;template&lt;/code&gt; values. No duplicated template design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Works cleanly as an HTTP Request node in n8n, a Webhooks step in Zapier, or an HTTP module in Make — the single POST plus signed-URL response maps to what those tools already expect, so you don't need a custom function to glue anything together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Artifacts / month&lt;/th&gt;
&lt;th&gt;Max payload&lt;/th&gt;
&lt;th&gt;Link expiry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;1MB&lt;/td&gt;
&lt;td&gt;7 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starter&lt;/td&gt;
&lt;td&gt;$7/mo&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;5MB&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;$19/mo&lt;/td&gt;
&lt;td&gt;3,000&lt;/td&gt;
&lt;td&gt;10MB&lt;/td&gt;
&lt;td&gt;No expiry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To be upfront: on raw PDF-per-dollar, you can match or beat these numbers on the big template-editor APIs - PDFMonkey's Pro is €15/mo for 3,000 documents, APITemplate.io's PDF Basic is $19/mo (annual) for the same. genui.sh isn't trying to be the cheapest PDF factory; it's trying to be the one call you make when you want a PDF &lt;em&gt;and&lt;/em&gt; a web view &lt;em&gt;and&lt;/em&gt; a dashboard from the same JSON, without designing a template first.&lt;/p&gt;

&lt;p&gt;The Free tier needs no credit card. Starter and Pro are managed through Stripe's hosted customer portal, so you can change plan, update payment, or cancel at any time without talking to anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is &lt;em&gt;not&lt;/em&gt; the right tool
&lt;/h2&gt;

&lt;p&gt;Being honest: if your use case is a heavily-branded invoice PDF with an exact legal layout your accountant approves annually, you probably want PDFMonkey's template editor. genui.sh is optimized for the case where the data is dynamic and the layout doesn't need to match a pixel-perfect brand document.&lt;/p&gt;

&lt;p&gt;If you need PDF/A compliance, digital signatures, or long-term archival metadata, none of the hosted APIs (including this one) will fit - you want a library like iText or Prince with full control.&lt;/p&gt;

&lt;p&gt;Everything else - reports, dashboards, invoices that don't need legal compliance, data exports, AI-generated summaries, automation workflow outputs - one POST is probably enough.&lt;/p&gt;

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

&lt;p&gt;Grab a free key at &lt;a href="https://genui.sh" rel="noopener noreferrer"&gt;genui.sh&lt;/a&gt; - 50 artifacts/month, no credit card. Ping me at &lt;a href="mailto:support@genui.sh"&gt;support@genui.sh&lt;/a&gt; with feedback, or open an issue if the docs are unclear.&lt;/p&gt;

&lt;p&gt;If you want to see what the composable dashboard format looks like, there's a &lt;a href="https://genui.sh/#pricing" rel="noopener noreferrer"&gt;live example&lt;/a&gt; on the landing page rendering a real dashboard from a single POST body.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>showdev</category>
      <category>automation</category>
    </item>
    <item>
      <title>Building a Local GitHub-style Review Interface for Claude Code Plans</title>
      <dc:creator>Eduard Maghakyan</dc:creator>
      <pubDate>Wed, 04 Mar 2026 00:27:49 +0000</pubDate>
      <link>https://dev.to/eduardmaghakyan/building-a-local-pr-review-interface-for-claude-code-plans-57o2</link>
      <guid>https://dev.to/eduardmaghakyan/building-a-local-pr-review-interface-for-claude-code-plans-57o2</guid>
      <description>&lt;p&gt;Plan mode in Claude Code feels like reviewing a pull request with no comments, no diff, and no history.&lt;/p&gt;

&lt;p&gt;Claude thinks for a moment (well much longer than a "moment"), then dumps a wall of text into your terminal - It then asks for approval with many variations of "Yes, ...."&lt;/p&gt;

&lt;p&gt;Then I open the Markdown file, start taking notes while reading through it. After doing this for a while I felt there should be a better way.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/EduardMaghakyan/ipe" rel="noopener noreferrer"&gt;IPE&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.amazonaws.com%2Fuploads%2Farticles%2F8u13z72zpsq99v8xhyoz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8u13z72zpsq99v8xhyoz.png" alt="interactive-claude-code-plan-review" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What is IPE?
&lt;/h2&gt;

&lt;p&gt;IPE intercepts Claude Code's &lt;code&gt;ExitPlanMode&lt;/code&gt; hook and opens a browser tab showing the plan in a GitHub-style code review interface. You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add inline comments&lt;/strong&gt; on any block or text selection — just like leaving a review comment on a PR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click any file reference&lt;/strong&gt; (e.g. &lt;code&gt;`src/index.ts`&lt;/code&gt;) to pop open a syntax-highlighted side drawer showing the actual file contents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare plan versions&lt;/strong&gt; side-by-side when Claude revises after your feedback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch between sessions&lt;/strong&gt; if you're running multiple Claude Code instances at once&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approve or request changes&lt;/strong&gt; — clicking "Request Changes" bundles your inline comments and sends them back to Claude&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Problems I wanted to fix
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You can't annotate.&lt;/strong&gt; If step 3 looks wrong and step 8 is fine but needs a tweak, you're writing one blob of feedback hoping Claude parses it all correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You lose context across revisions.&lt;/strong&gt; Claude revises the plan based on your feedback - but there's no diff. Did it actually address your concern? You're re-reading the whole thing from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;IPE registers itself as a Claude Code hook on &lt;code&gt;PermissionRequest&lt;/code&gt; with the &lt;code&gt;ExitPlanMode&lt;/code&gt; matcher. When Claude finishes planning and tries to proceed, the hook fires.&lt;/p&gt;

&lt;p&gt;The binary spins up a local HTTP server, opens your browser, and &lt;strong&gt;blocks&lt;/strong&gt; - Claude is sitting there waiting for your response. You review at your own pace (the timeout is 4 days, so no rush). When you approve or request changes, the server sends the response back to the hook and the browser tab closes automatically.&lt;/p&gt;

&lt;p&gt;The whole thing is a self-contained binary built with Bun.&lt;/p&gt;




&lt;h2&gt;
  
  
  Install in One Line
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;macOS / Linux:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/eduardmaghakyan/ipe/main/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows (PowerShell):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;irm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://raw.githubusercontent.com/eduardmaghakyan/ipe/main/install.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The script downloads the binary and registers the hook in your Claude Code settings automatically. Run it again to update.&lt;/p&gt;

&lt;p&gt;Verify it's wired up by running &lt;code&gt;/hooks&lt;/code&gt; inside Claude Code — you should see the &lt;code&gt;ExitPlanMode&lt;/code&gt; hook listed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Workflow in Practice
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Work with Claude Code as normal&lt;/li&gt;
&lt;li&gt;Claude generates a plan and calls &lt;code&gt;ExitPlanMode&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Your browser opens with the plan displayed&lt;/li&gt;
&lt;li&gt;Read through it, click file references to inspect code, leave inline comments where needed&lt;/li&gt;
&lt;li&gt;Hit &lt;strong&gt;Accept&lt;/strong&gt; → Claude proceeds&lt;/li&gt;
&lt;li&gt;Hit &lt;strong&gt;Request Changes&lt;/strong&gt; → your comments go back to Claude, it revises, you review the diff&lt;/li&gt;
&lt;/ol&gt;




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

&lt;p&gt;The project is open source: &lt;a href="https://github.com/EduardMaghakyan/ipe" rel="noopener noreferrer"&gt;github.com/EduardMaghakyan/ipe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you use Claude Code in plan mode regularly, give it a spin and let me know what you think. Issues and PRs welcome - there's a lot of room to grow this (comment threads, keyboard shortcuts, plan history persistence...).&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
