vadimdemedes

Stars and mentions

Last 30 days, Oct 4 – Nov 2

GitHub stars182
mentions5
0481216Oct 4Oct 18Nov 2
UTC days · repositories vadimdemedes owns13 repositories

Package downloads

Cumulative recorded downloads · since 2025-10-19

npm308.8M
077.2M154.4M231.6M308.8MOct 4Oct 18Nov 2
Oct 24npm: 5/9 packages observed

Across 9 known packages in repositories vadimdemedes owns. Cumulative totals include recorded downloads before the selected range. Missing observations leave gaps; totals may be incomplete.

Top repositories

vadimdemedes’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside vadimdemedes’s account where vadimdemedes opened or reviewed pull requests in the past 365 days.

Who noticed vadimdemedes

Everyone who starred or forked vadimdemedes’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

6posts
Nov 31 post
  1. Why One Emoji Breaks Your Coding Agent's TUI Borders: wcwidth vs Grapheme Clusters - Book: AI That Acts - The series: AI in TypeScript — 5 books, from your first LLM call to agents in production — all five here - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- Paste 🫪 into Claude Code 2.1.207 and the text after it slides one cell to the right. That is the report in anthropics/claude-code#80475, opened July 23, 2026: stray characters bleeding into neighbouring rows, inverse-video blocks between words, and a mess that stays until Ctrl-L or a window resize forces a full repaint. The emoji is U+1FAEA, DISTORTED FACE, which arrived with Unicode 17.0 in 2025. The terminal drew it two cells wide. Claude Code's width table was older than the emoji and counted fewer. In the maintainer's closing comment, the fix "came from updating the Unicode width tables in the runtime Claude Code ships with," and from 2.1.221 on, both 🫪 and 🫩 (U+1FAE9, FACE WITH BAGS UNDER EYES, reported later in the same thread) measure as two cells. Claude Code and the terminal disagreed on how many columns U+1FAEA takes, and every cell after it inherited the gap. Two programs, one grid A terminal is a grid of cells. A TUI like Claude Code, Codex or Gemini CLI never sees that grid. It writes bytes, and to draw a box it has to predict where the cursor will be after each string. The right border of a box sits at "left edge plus the width I computed." The terminal then advances the cursor by the width it computed. When those two numbers agree, borders line up. When they differ by one, everything the program draws after that character lands one column off, and every partial redraw targets the wrong cells. That is why these bugs show up as smearing and duplicated lines across the whole screen. kitty's text sizing protocol docs put it bluntly: if the client program and the terminal disagree on cell counts, "the entire user interface can be broken". !A terminal grid with two rulers, one from the program and one from the terminal, measuring the same emoji tile and ending one cell apart The program and the terminal each measure the same character. One cell of disagreement moves every border after it. Three counts, none of them is width Start with what JavaScript can count. Save this as count.mjs and run it with node count.mjs: const seg = new Intl.Segmenter("en", { granularity: "grapheme", }); const samples = { ascii: "A", cjk: "中", heart: "\u{2764}", heartVS16: "\u{2764}\u{FE0F}", thumbTone: "\u{1F44D}\u{1F3FD}", family: "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}", distorted: "\u{1FAEA}", }; console.log("ICU Unicode:", process.versions.unicode); for (const [name, s] of Object.entries(samples)) { const units = s.length; const points = [...s].length; const clusters = [...seg.segment(s)].length; console.log(name.padEnd(10), s, units, points, clusters); } .length counts UTF-16 code units. Spreading the string counts code points. Intl.Segmenter counts grapheme clusters, which UAX #29 defines as user-perceived characters. Its rule GB11 says: "Do not break within emoji modifier sequences or emoji zwj sequences." The family is three emoji glued together by two ZERO WIDTH JOINERs (U+200D), so it is five code points and eight UTF-16 units but one grapheme, while the skin-tone thumbs-up and the heart followed by U+FE0F are two code points and one grapheme each. A Chinese character is one code point and one grapheme, and it still takes two cells. The first line prints the Unicode version your Node's ICU knows. The Node docs example shows unicode: '17.0'. Unicode 18.0 is dated September 16, 2026. Every program that measures width carries a snapshot of some version, and they are rarely the same snapshot. Now draw a box the naive way, one cell per code point: const rows = [ "plain text", "中文", "\u{2764}\u{FE0F} ok", "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467} team", ]; const width = (s) => [...s].length; const inner = 16; const edge = "+" + "-".repeat(inner) + "+"; console.log(edge); for (const r of rows) { console.log("|" + r + " ".repeat(inner - width(r)) + "|"); } console.log(edge); The plain-text row lines up. The 中文 row overshoots by two cells in every terminal, because each character is one code point and two cells. The heart and family rows are where terminals disagree: run the script in two emulators and compare where those right borders land. Where width actually comes from The width a terminal assigns usually starts from the East_Asian_Width property in UAX #11. Its values are A (ambiguous), F (fullwidth), H (halfwidth), N (neutral), Na (narrow) and W (wide). W and F take two cells, and W also covers "characters that have the UTS51 property Emoji_Presentation." For ambiguous characters the annex says: "If the context cannot be established reliably, they should be treated as narrow characters by default." That covers single code points. Emoji sequences are where the tables run out. UTS #51 defines U+FE0F, VARIATION SELECTOR-16, as the character "used to request an emoji presentation." The plain heart U+2764 has no Emoji_Presentation, so on its own it is a text-style, one-cell character. Add VS16 and you ask for the two-cell emoji. A per-code-point width function sees one narrow character plus a variation selector that takes no cells of its own, and so the total depends on whether the function knows the sequence. The Python wcwidth library documents exactly this split. wcwidth('\U00002640'), the plain ♀ (U+2640), is 1. wcswidth('\U00002640\U0000FE0F'), the same sign with VS16, is 2. Ask for VTE's behaviour with wcstwidth(..., term_program='vte') on that same VS16 string and the docs show 1. Its four-person family example measures 2 by default and 8 with term_program='alacritty', a correction the docs put down to Alacritty not supporting emoji ZWJ. Here is the Python side of the same experiment: # pip install --upgrade wcwidth import unicodedata from wcwidth import wcwidth, wcswidth print("UCD", unicodedata.unidata_version) for ch in ["A", "中", "\U00002764", "\U0001FAEA"]: eaw = unicodedata.east_asian_width(ch) print(f"U+{ord(ch):04X}", eaw, wcwidth(ch)) print(wcswidth("\U00002640\U0000FE0F")) # wcwidth docs: 2 unicodedata follows the Python release, not the wcwidth package. The Python 3.15 docs say its data comes from UCD 17.0.0. An older interpreter or an older wcwidth will give a different answer for U+1FAEA, which is the same kind of mismatch as the Claude Code bug. It goes the other way too. In gemini-cli#25369, Ink, the React renderer Gemini CLI draws with, used Intl.Segmenter to merge a Thai consonant and SARA AM (U+0E33) into one cluster of width 1, while tmux and xterm.js drew it across two columns. The symptom was one streamed line repeated 8 to 10 times and an input line that kept jumping down. It was closed as completed on October 23, 2026, via PR #29883. This time the program was the one counting by clusters, and the terminal in front of it counted differently. !Three code points, a man, a woman and a girl, fusing through two joiner links into one emoji tile on a terminal grid A ZWJ sequence is several code points and one grapheme. Whether it takes two cells or eight depends on who is counting. Mode 2027: asking the terminal to count graphemes Some terminals now let a program opt into one shared rule. Contour's terminal-unicode-core spec defines DEC private mode 2027. CSI ? 2027 h turns it on, CSI ? 2027 l turns it off, and CSI ? 2027 $ p asks whether the terminal supports it. With the mode on, a grapheme cluster lives in one place on the grid, and a VS16 in the cluster promotes it to emoji presentation, which makes it two cells wide. You can probe your terminal from Node: process.stdin.setRawMode(true); const timer = setTimeout(() => done("no reply"), 500); function done(msg) { clearTimeout(timer); process.stdin.setRawMode(false); console.log(msg); process.exit(0); } process.stdin.once("data", (b) => done(JSON.stringify(String(b)))); process.stdout.write("\x1b[?2027$p"); The answer is a DECRPM report shaped like CSI ? 2027 ; Ps $ y. Ps 0 means the terminal doesn't recognise the mode, 1 means it is set, 2 means reset, 3 and 4 mean permanently set or reset. The timeout matters: a terminal that ignores the query sends nothing at all. Support is uneven. In the ucs-detect results, which measure terminals by printing test strings and asking for the cursor position afterwards, Ghostty, WezTerm, foot and Windows Terminal report mode 2027. kitty, iTerm2, Alacritty and xterm.js do not. The page gives no test date and says results are provided as-is, so check the version next to each row. Ghostty exposes the trade-off as a setting. grapheme-width-method defaults to unicode, which the docs call correct but able to desync the cursor with programs that measure the legacy wcswidth way. legacy matches those programs and can get skin-tone emoji wrong. A program that turns on mode 2027 forces unicode regardless. kitty went a different direction with OSC 66, where the program states the width with w= and the terminal stops guessing. The two-pane test Put two agent TUIs side by side and the disagreement becomes something you can locate. I build Hermes IDE, so weigh this section with that in mind. Hermes runs every agent's own interface in a real terminal and lets you split panes right or down, so Claude Code and Codex can sit next to each other on the same screen. !Two agent sessions side by side in Hermes IDE: Codex on orders-types and Claude Code on orders-api, each in its own hermes/ worktree, with the sessions sidebar on the left Hermes IDE 2.3.4: Codex on orders-types and Claude Code on orders-api, side by side, each in its own hermes/ worktree, with the sessions sidebar showing status, context use, cost and branch. Paste the same string with an emoji into both. Both panes live in the same app, so the terminal side of the comparison stays fixed. If one pane's border breaks and the other's holds, the difference is in the agents. Codex's TUI is built on Ratatui and pulls in the unicode-width and unicode-segmentation crates (its Cargo.toml), and Claude Code ships its own width tables in its runtime, so the two can carry different Unicode snapshots. Then flip it. Run the same agent in a second terminal emulator. If the border is fine there, the agent agrees with that emulator and the first one is the odd one out. The rest is cheap: 1. Update the agent first. claude update is the whole fix for #80475. 2. Repaint with Ctrl-L or a resize to clear a smeared screen. 3. Keep fresh emoji out of the strings an agent draws in its chrome: session names, branch names, status lines. 4. If you write a TUI, measure grapheme clusters, query mode 2027 and turn it on when the reply shows the mode is recognised (Ps 1 or 2). Where it isn't, test against the terminals your users actually run. Hermes is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It isn't affiliated with Nous Research, Anthropic, OpenAI or Google. Download it and the source is on GitHub. Finding the character Next time an agent's box comes out with a jagged right edge, skip the font settings and find the character just before the break. Compare the width your agent gives it with the width your terminal draws, then check which Unicode version each was built with. If the versions match, compare how each one segments that cluster; the Gemini CLI Thai bug was that kind. --- If this was useful If you're building your own agent in TypeScript, the terminal is only the surface. AI That Acts covers the part underneath it: tool calling, functions, and getting a first working agent to do real work in Node, so the thing drawing those borders has something worth showing. ![AI That Acts — AI in TypeScript](amazon.com) It is book 3 of AI in TypeScript, a five-book series that runs from your first LLM call to agents in production. ![AI in TypeScript — the five-book series](xgabriel.com)

  2. Your Half-Typed Prompt Lives Inside the Agent, Not the Terminal. Injected Commands Hit It - Book: Prompt Engineering Pocket Guide - The series: Pocket Guides for Developers — 8 short books on agents, RAG, prompts, observability, events, databases and system design — the whole series - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- You are halfway through a sentence in Claude Code: fix the flaky login test but don't touch. In another window, a helper script decides the context is getting full and runs tmux send-keys -t agent '/compact' Enter. Nothing in the terminal stops the agent's input box from ending up as fix the flaky login test but don't touch/compact, and the Enter that came with it will submit whatever the box holds. You never finished the sentence, and the command never runs as a command. None of this is a bug in Claude Code or in tmux. It follows from where a half-typed prompt lives once a full-screen agent owns the terminal. Any tool that sends /compact or /model into a running agent has to deal with it. Where your half-typed prompt actually lives A normal shell prompt runs in canonical mode. The termios man page says that in this mode "input is made available line by line" and "line editing is enabled". The kernel's line discipline holds the line you are typing, handles Backspace, and only hands the program the finished line when you press Enter. Full-screen agents switch that off. Claude Code is built on Ink, which lists it under "Who's Using Ink?" and exposes setRawMode for reading keys. Node's setRawMode makes input "available character-by-character" with "all special processing of input characters by the terminal" disabled. Codex's TUI depends on ratatui and crossterm, and crossterm's raw mode means input is "not line buffered (input sent byte-by-byte to input buffer)". In termios terms, raw mode clears ICANON and ECHO (see cfmakeraw in the same man page). With ICANON off, "input is available immediately" and "line editing is disabled". The kernel stops holding a line. Every keystroke goes straight to the agent, and the agent keeps the draft in its own memory, draws it on screen, and decides what Backspace or Enter means. You can see the switch. In a plain shell: stty -a | grep icanon The output contains icanon with no minus sign. Now run tty in a terminal, note the device, start claude there, and from a second terminal point stty at that device (-F on Linux, -f on macOS and BSD): # Linux stty -a -F /dev/pts/3 | grep icanon # macOS stty -a -f /dev/ttys003 | grep icanon While the agent's interface is up, expect -icanon and -echo. The line buffer you might have hoped to inspect does not exist. Your draft is a string inside a Node or Rust process, and short of attaching a debugger, nothing outside the process can read it. The closest anything outside can get is the rendered screen. !A pipe on the kernel side passing bytes straight through, and a sealed box on the agent side holding the half-typed draft In raw mode the kernel passes every byte through, and the draft lives only inside the agent process. Watch the collision with two small Python scripts You don't need an agent to see it. The first file is a toy agent that does what a raw-mode TUI does: it switches its terminal to raw mode and keeps its own draft. # toy_agent.py: keeps its own draft, like a TUI import os, sys, tty tty.setraw(sys.stdin.fileno()) draft = b"" while True: ch = os.read(0, 1) if ch == b"\r": os.write(1, b"SUBMITTED: " + draft + b"\r\n") draft = b"" elif ch == b"\x7f": draft = draft[:-1] elif ch == b"\x04": break else: draft += ch The second file plays both you and the tool. It starts the toy on a pseudo-terminal with pty.fork(), whose parent gets "a file descriptor connected to the child's controlling terminal". Writing to that descriptor is exactly what a terminal emulator does when you press a key, and what tmux send-keys does for a script. # inject.py: you type half a prompt, a tool sends /help import os, pty, sys, time pid, fd = pty.fork() if pid == 0: os.execvp(sys.executable, [sys.executable, "toy_agent.py"]) time.sleep(0.5) # let the toy enter raw mode os.write(fd, b"fix the fl") # you, mid-word time.sleep(0.2) os.write(fd, b"/help\r") # the tool's command time.sleep(0.2) os.write(fd, b"\x04") # tell the toy to quit time.sleep(0.2) print(os.read(fd, 1024)) os.waitpid(pid, 0) Run python3 inject.py. The toy should submit one line, b'SUBMITTED: fix the fl/help\r\n': your fragment with /help glued to the end. It never saw a command. Now change b"fix the fl" to b"" and run it again: the toy submits /help alone. That is the only case where injection behaves, and from outside the process you cannot tell which case you are in. (If the output starts with an echo of your text, the toy wasn't in raw mode yet; raise the first sleep.) A real agent is more careful than the toy, with autocomplete, multi-line input and paste handling, but the bytes arrive the same way. I haven't run this against Claude Code or Codex myself, so read the toy as the mechanism. Each product's input handling is its own. Two ways an injected command goes wrong The first is the one above: the bytes are appended to a draft. Clearing the box first does not save you. In Claude Code, per the interactive mode docs, Ctrl+C with nothing running "clears the prompt input and a second press exits Claude Code". A script that sends Ctrl+C or Ctrl+U to get a clean box throws away the human's text to do it. Recent Claude Code versions can bring a cleared draft back with Up, but your script should not be relying on that. The second is timing. The agent may still be working when the bytes land, and agents treat input during a turn on purpose: - Claude Code queues a message you send while it works. Queued messages can be passed to Claude "as soon as those tool calls finish, within the same turn", and queued commands are held "until the turn ends" (docs). - Some commands skip the queue. The same page says /model, /effort and /fast run "as soon as you send them", and a new model is used "from the next request it makes in that turn". An injected /model can switch models halfway through a piece of work. - Codex lets you steer the active turn while it works, so a line that arrives mid-turn can become an instruction to the work already in progress. Each of those behaviours makes sense for a human at the keyboard who knows the agent is busy. A script that doesn't know will hit them by accident. !Two hands reaching for the same keyboard, one human hand mid-sentence and one mechanical hand pressing Enter Two writers, one input box: the agent cannot tell which bytes came from whom. The only safe rules There are two, and both are about what the tool knows before it writes. Send only when the agent says it is waiting. Don't infer that from a quiet screen. Ask the agent. Claude Code has hooks for this: Stop fires "when Claude finishes responding", and the Notification event can be matched on idle_prompt and permission_prompt. A tool that listens to those knows when a turn is over, which a screen scraper can only guess. Never write on top of a draft. Since nothing outside the agent can read the draft, the safe version of this rule is structural: the command must not travel through the box the human is typing into. If a human might be mid-sentence in that box, the tool waits, or it hands the decision back to the human. Wiping the box to make room is not an option: it throws the human's text away. For an unattended session where nobody types, tmux send-keys after a Stop hook is fine. The trouble starts when a person and a program share one input box. How Hermes IDE 2.3.4's command bar handles it I build Hermes IDE, so weigh this section with that in mind. Hermes runs each agent's own interface in a real terminal, so Claude Code's input box is Claude Code's, raw mode and all. The README says Hermes "never types into a terminal on its own". Version 2.3.4, released on October 15, 2026, rebuilt the bar above each session: Model, Plan and Review in the same place for every agent, a context meter that offers Compact when the conversation gets close to full, and a Commands list with built-in commands plus your own commands and skills. Commands run when you click them, never on their own. !The action bar above a Claude Code session in Hermes IDE with the Commands list open The action bar above a Claude Code session (Prompts, Context 4%, Model, Plan, Review, / Commands) with the Commands list open: pinned Context, Model, Plan and Review, then built-in commands such as Compact, Usage and Diff. The fix in the same release follows the two rules above, in the release notes' own words: "Clicking a command never touches what you were typing, and it only runs when the agent is waiting." Waiting comes from Hermes's per-session status (working, needs approval, asked you, done, error or idle), and the README notes that Hermes says when a status is a guess. Hermes is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It is not affiliated with Nous Research, Anthropic, OpenAI or any agent vendor. If you write your own helper instead, the toy experiment above is a cheap regression test: put a fragment in the draft, fire your command, and check the fragment survives. --- If this was useful A slash command is a prompt that takes a shortcut, and where it lands decides what the model reads. The Prompt Engineering Pocket Guide covers the other half of that: what to put in the prompt once it arrives intact, from system prompts to context you feed the agent on purpose. It is one of 8 short books in the Pocket Guides for Developers series, on agents, RAG, prompts, observability, events, databases and system design. If you want to try the command bar, Hermes IDE is a free download and the code is on GitHub. ![Prompt Engineering Pocket Guide: Techniques for Getting the Most from LLMs](amazon.com)

  3. 😳 用 Node 写命令行,界面只能一行行刷。Ink 4 万星,用 React 组件写终端界面,会 React 就能做,状态更新和布局现成。 vadimdemedes/ink

    1 like · 33 views
  4. Paste 2,000 Characters at a Line-Mode Prompt and macOS Keeps 1,024. Blame the TTY - Book: System Design Pocket Guide: Fundamentals - The series: Pocket Guides for Developers — 8 short books on agents, RAG, prompts, observability, events, databases and system design — the whole series - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- In July 2026 someone filed issue #81485 against Claude Code: on macOS, a deep link that opens a session by typing a launch command into a terminal failed without a word whenever that command was longer than 1,024 bytes. The report describes "truncation at exactly the 1024-byte boundary, command never executes, no session." The terminal bell rang. The command sat at the shell prompt, cut short, with its trailing newline gone. iTerm2 didn't do that, and neither did Terminal.app, where the report says it also reproduces. The kernel did. Every keystroke and every paste you send to a Unix terminal goes through a layer most developers never think about, and that layer has a size limit on the line you're typing. It differs by OS. It also explains a second failure, a big paste that hangs the terminal. The kernel edits your line before your program sees it A terminal window talks to your program through a pseudo-terminal, a pair of kernel devices. The terminal app writes your keystrokes into one end, your program reads from the other, and in between sits the line discipline. By default it runs in canonical mode, which the Linux termios(3) man page describes plainly: "Input is made available line by line," and line editing (erase, kill) is enabled. That's why Backspace works in cat even though cat has no editing code. The kernel holds the line in its own buffer, applies your Backspace and Ctrl-U there, and only hands the line to read() when you press Enter. Until then, the program sees nothing. You can see the mode with stty: stty -a | grep -o -- '-\{0,1\}icanon' icanon means canonical mode is on. -icanon means it's off. stty sane, the reset the man page lists, switches it back on. The kernel's line buffer is fixed in size. Hold that thought. 1,024 bytes on macOS, 4,095 on Linux On macOS, the limit lives in XNU's bsd/sys/syslimits.h: #define MAX_CANON 1024 /* max bytes in term canon input line */ #define MAX_INPUT 1024 /* max bytes in terminal input */ The code that enforces it is ttyinput() in XNU's bsd/kern/tty.c. Once the queued input reaches MAX_INPUT, each new character is thrown away, and with IMAXBEL set the kernel rings the bell. Signal keys (Ctrl-C), flow control and the erase and kill keys are handled before the overflow check. The newline and the EOF key are handled after it. So on a full line, Ctrl-C and Backspace still work, but Enter and Ctrl-D get dropped like any other character. The Claude Code command never ran because its Enter never arrived. An Apple Developer Forums thread from 2024 shows the same thing from the user side: run base64, paste, and "I can paste or type up to 1024 characters (bytes). After that terminal starts beeping and does not allow me to type or paste anymore input." Apple's DTS engineer answered that the limit "comes from how the shell and the underlying tty manage their I/O, not terminal.app." Linux allows a longer line and handles the overflow differently. termios(3) puts the maximum line at "4096 chars (including the terminating newline character)," and says "any input data after 4095 characters up to (but not including) any terminating newline is discarded." The buffer behind it is N_TTY_BUF_SIZE, defined as 4096 in drivers/tty/n_tty.c. Linux keeps the newline, so your truncated line still gets submitted. macOS drops it, so the line sits there. Neither OS tells your program. It receives a shorter line and has no way to know. Run the test yourself Put a 5,000-character line with no newline on your clipboard. On macOS: head -c 5000 /dev/zero | tr '\0' a | pbcopy On Linux, pipe the same head | tr into whatever clipboard tool your desktop uses. Now run wc -c, paste, press Enter, then Ctrl-D: wc -c From the sources above, here's what to expect. On Linux, termios(3) says you keep 4,095 characters plus the newline, so wc should print 4096 instead of 5001. On macOS the bell rings after 1,024 bytes, and Enter and Ctrl-D do nothing, because they're dropped too. Press Ctrl-C to get out. Now the same paste with canonical mode switched off for one command: stty -icanon min 1; head -c 5000 | wc -c; stty icanon -icanon turns off line assembly, and min 1 (per the stty man page, "with -icanon, set N characters minimum for a completed read") makes each read() return as soon as a byte is there. head drains the bytes as they arrive, so no line ever has to fit in the kernel's buffer. If the TTY was the culprit, this one reports 5000. The Apple engineer in that forum thread describes pasting a single very long line into pico without trouble. A full-screen editor like pico reads in non-canonical mode, so the line buffer never fills. When nobody is reading, the writer waits Truncation needs a program that's reading. The other failure needs one that isn't: a process busy compiling, sleeping, or stuck, while you paste a few thousand lines of stack trace at it. The bytes still have to go somewhere. On Linux they land in the line discipline's 4,096-byte read buffer, and once that fills, the line discipline stops accepting data and the bytes back up in the pty's flip buffers. The Linux pty driver has a pty_write_room() that reports how much space is left on the other side, and the flip buffers behind it have a byte cap, TTYB_DEFAULT_MEM_LIMIT in tty_buffer.c. When the room runs out, a write from the terminal app's side goes through n_tty_write(), which does one of two things. If the file descriptor is non-blocking, it returns -EAGAIN. If it's blocking, it sleeps in wait_woken() until the reader frees space. So the terminal app ends up waiting on a program that may never read again. If that write runs on a thread the rest of the app needs, or holds a lock the resize and close paths also take, the hang spreads from one tab to the whole window. Any terminal host has to queue paste data itself and feed it in as the PTY drains, never block on it. I build Hermes IDE, so weigh this part with that in mind. The 2.2.9 release on October 12, 2026 fixed this class of bug in Hermes. The note reads: "Pasting a lot of text into a terminal whose program isn't reading it no longer freezes the whole window when you resize, split or close that terminal." The note doesn't describe the patch, so I won't guess at the internals. A big log pasted into a terminal whose program has stopped reading, then a resize, split or close of that pane, is the case the note describes. Why agent TUIs switch to raw mode Interactive CLIs, coding agents included, don't want the kernel editing their input. They want every key the moment it's pressed, so they can draw their own prompt, handle arrow keys, and collapse a pasted block, which is what raw mode gives them. In Node, setRawMode(true) means "input is always available character-by-character" and "all special processing of input characters by the terminal is disabled, including echoing input characters." Even Ctrl-C stops sending SIGINT. Claude Code's interactive UI was built this way. A May 2025 issue shows the error it threw when it was run with piped input: "Raw mode is not supported on the current process.stdin, which Ink uses as input stream by default." Ink is the React renderer for terminals, and its setRawMode throws when stdin can't do it. Raw mode fixes the 1,024-byte wall, since the program reads bytes as they come and keeps the line in its own memory. It creates a new problem: a paste now looks like very fast typing. A newline inside the paste looks like you pressed Enter. That's what bracketed paste is for. The program sends ESC [ ? 2004 h, and from then on, per the xterm control sequences, the terminal wraps every paste in ESC [ 200 ~ and ESC [ 201 ~ "so that the program can differentiate pasted text from typed-in text." Ink's usePaste hook turns this on automatically, and GNU Readline has enable-bracketed-paste, whose default "is 'On'." One sharp edge: the end marker can be smuggled inside the pasted text. The Ghostty paste docs flag data containing \x1b[201~ as unsafe because it "can be used to exit bracketed paste mode and inject commands." Copying a command from a web page into an agent's prompt is still pasting into a terminal. What to do with this When a long paste comes out short or never submits, ask how the program on the other end reads before blaming the app. A plain read() in canonical mode is the usual suspect. - For long input to your own scripts, use a pipe or a file: pbpaste | ./tool or ./tool < trace.txt. Pipes and files have no line discipline. - If you write a CLI that takes pasted input, read in non-canonical mode or use a line editor that does, and turn on bracketed paste. - If you write anything that types into a terminal for the user, the way that deep link did, keep each line under 1,024 bytes on macOS or pass the payload through a file or environment variable. - If you build a terminal host, treat writes to the PTY as something that can wait forever. The 1,024 is one #define in a kernel header. It still decides whether your agent gets your whole stack trace. --- If this was useful The hang half of this post is a bounded buffer doing its job: when the consumer stops, the producer has to wait or drop data, and the only design choice is which. If you like thinking about where data waits and what happens when it can't, that's the ground covered by System Design Pocket Guide: Fundamentals, the core building blocks of systems that have to keep working under load. It's one of 8 short books in the Pocket Guides for Developers series, on agents, RAG, prompts, observability, events, databases and system design: the whole series is here. And if you run Claude Code and other agents side by side, Hermes IDE is free, source-available and on GitHub. ![System Design Pocket Guide: Fundamentals — Core Building Blocks for Scalable Systems](amazon.com)

  5. Building KaizenSQL - My Dev Journey TL;DR KaizenSQL is a CLI tool written in Go, it analyzes SQL code and supports three modes Mentor/Performance/Security. To give it AI-powered capabilities I used Groq api for free LLM tokens. Overall, the main concern and goal with this project was to make the tool fast and pleasant to use. Every decision to be implemented had to take that into account. In this post I'll share the faced scenarios, the good parts and bad ones. Code repo link at the end. ☕ --- HI fellow developer, pal, buddy, maybe a friend reading this 👋. I'm Juan Huanaco, a Systems Engineering student from Peru. 🇵🇪 Today's post is about a small project: KaizenSQL, a Go based CLI tool that analyzes SQL code. First question: Why? Do I need it? Not for now haha, I do not deal with tons of SQL these days. This time it's not about the final product but the <u>pillars and process experience</u> while building it. I like how reliable some CLI based programs are. Just type the command with some arguments and zap⚡! The result is at your screen. "Qué genial, quiero crear algo así..." was my thought. So I wanted to create a tool that reads a file/directory, sends that to an LLM for feedback AND has an intuitive and good-looking UI. On top of that, it had to be fast (as much as it could, I'm looking at you APIs falling several times lol). That's it, up next are the key decisions that made me pause and think through each implementation step. AI Agent brain - No need to think much I use Gemini chat, is smart enough to help me find good resources and give advices. But I didn't want to spend money on trying the API version. Their free tier limits leaves you sweating. For that reason I chose another LLM provider for this project. 💪 That's Groq API, it has a generous limit to use it properly as a sandbox for testing your AI interactions. Also it was designed to be mostly compatible with OpenAI's client library, so you can switch/scale with them or OpenAI APIs in the future without doing lots of changes on connections. It's fast and best part, no need to share card data for their free account. [2026.10.04] ADR-001: Modes definition for inspection After having the initial code for connecting with Groq API successfully, I had an LLM agent ready to deal with text. Now, "...tool to analyze SQL code...", great but what exactly should we monitor? Speed? Security? Both? If so, should it be a single scan for all? One of the constraints was that the specific model I used offered 8k tokens per minute. !Groq site showing available model's limits So I had to ensure to leave enough space for SQL code (which could be pretty big) but also injecting enough proper system prompt. Also this introduces a good reason to add command flags to my tool. The flag module comes in handy: import "flag" func main() { showBanner() checkModeMentor := flag.Bool("m", false, "Use mentor mode [Default]") checkModePerf := flag.Bool("p", false, "Use performance mode") checkModeSec := flag.Bool("s", false, "Use security mode") var chosenMode Mode flag.Parse() and depending on the selected mode (btw they don't add up), we sent a different system prompt to the agent. Also I added a Mentor mode, its value is that it does not solve the problem, but guides you a clue so that you can find the answer yourself. Since developers are directly responsible for coming up with the solution, the new concept is easily sticks in their mind. [2026-10-04] ADR-002: Output tokens limit excedeed when calling the agent 🫣 !A screenshot showing an OTPM error Well... this one was on me haha Of my three executions just one showed the error. Up to this point, I hadn't checked the output limits for qwen/qwen3.8-27b. To my surprise only 1k tokens were offered (7k in / 1k out). Time to change model? Nope, I liked how qwen performed, I used it on another project where I connected Foxit PDF API to a CLI tool. Btw i have a video about that, you can check it 👉 here (warning: behold, just pure and good spanish there 🧑‍⚖️) the fix? just to set the limit explicitly, that's ok for this little project's scope response, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ Model: k.agentModel, + MaxCompletionTokens: 900, Messages: []openai.ChatCompletionMessage{...} } --- The rest of the Journey Animated banner - Adding Bubbletea I was scrolling on blogs and found one about Github Copilot CLI. I knew some CLI tools add a nice-looking banner to their programs, however an animated one was something I didn't see too much. Easy to say I was impressed first time I saw this: !Github Copilot CLI animated banner In their blog they mentioned the use of Ink 🌈 (a React renderer for building CLIs with JSX). Seemed like a great fit, if only that's wasn't just for NodeJS based tools. 😔 After some googling I found bubbletea (a TUI framework), this awesome open-source solution was the speedtrack I was looking for. But things get better, because Cameron Foxly (principal brand designer at GitHub) created Ascii Motion a website to create ascii animations through an intuitive UI and export them to many formats, one of them was bubbletea anim. !ascii-motion page Wrapping up This concludes the developing journey for this little project. I hope you enjoyed this post as much as I did writing it. ✍️ The repo: KaizenSQL by jhuanaco Setup instructions and the binary -ready to use- live happily there. !KaizenSQL in action gif What's next While coding I noted these: - Add interactive file selection: Instead of passing the filename as a command-line argument, why not list all the .sql files on the current directory and let the user choose taking advantage of bubbletea keyboard interaction. - Heuristic pre-analysis: If we start to inject more rules into the system prompt, eventually we might exceed the ITPM (input token per minute) limit. A pre-analysis on found keywords (JOIN, SINGLE, INSERT, etc) might be used to inject on the prompt just as needed rules. A next step to that approach would be to implement a RAG solution. Will I add these ? Nah, lol The project scope is over, and I know myself enough to tell you that if I not set scope limits, I might keep refactoring, adding, removing code in an unhealthy loop 😂. Also I have other project ideas I want to cook. 👀🧑‍🍳 Thanks ❤️ Special thanks to the people on the open-source world, it was a fun dev experience to work with their great software. And thank you for reading this post. Until the next one. - Go-OpenAI by sashabaranov - Used for communicating with Groq API - Bubbletea by charmbracelet - Text User Interface framework used to add a nice banner. - Ascii Motion by CameronFoxly - Awesome ascii-art animation software

    2 reactions · 1 comment
  6. Terminal UIs for Harnesses in 2026: who builds on what, and why > We spent forty years escaping the command line. Then AI agents arrived, and the best-funded interface teams in software went back to a grid of monospaced characters. So what's happened? The terminal came back beautiful In 1984 the Macintosh promised we would never look at a blinking cursor again. In 2026, OpenAI, Anthropic, Google, and xAI all ship their flagship coding products as terminal apps. Not grudgingly, either. These are polished, animated, mouse-aware interfaces that happen to be drawn entirely out of text. Something strange happened on the way to the future, and it is worth understanding what. A screen made of cells !Diagram titled How a terminal actually works: a stream of bytes with escape codes is interpreted into a grid of fixed-width cells, where colors come from the codes, wide characters take two cells, and the cursor is one cell painted in reverse. A terminal gives a program almost nothing. There is no canvas, no DOM, no pixels. There is a grid of monospaced character cells, and two streams of bytes: one going out, one coming in. Print text and it simply fills the next cells in the grid. Everything else is a trick. The trick is called an escape sequence: a run of bytes starting with the escape character that the terminal reads as a command instead of text. \x1b[5;10H moves the cursor to row 5, column 10. \x1b[31m turns the next characters red. Other sequences clear lines, hide the cursor, or make mouse clicks arrive on the input stream as encoded bytes. An app drawing itself is just a program printing instructions; those are the boxed bytes in fig 01. A full-screen app adds two more moves. It switches the input to raw mode, so each keystroke arrives instantly instead of waiting for Enter; that is why a single j moves the cursor in vim. And it opens the alternate screen buffer, a second blank screen to draw on. Quit vim and your shell history is exactly where you left it. That is the alternate screen closing. So every TUI framework ever written has the same job. Hold a model of the grid in memory. Turn your interface into cells. Compare that with what is already on screen, and emit the fewest escape sequences that make the two match. It is the same paint, diff, and flush loop as a game engine, or as React against the DOM. The whole craft lives in that loop, and in one cursed detail: a Japanese character is two cells wide, an emoji is anyone's guess, and every miscount shows up as a broken border. Why we left, and what pulled us back We left because the terminal of 1985 deserved to be left. Sixteen colors on a good day. No mouse. No images. The GUI won on fidelity, then the web won on reach, and the terminal became the boiler room: always there, never decorated. !Timeline titled Where the interface investment went: terminal era from the 1978 VT100, the GUI era and the web from the 1984 Macintosh, and the return with GPU terminals in 2022 and AI coding agents in 2025-26. What pulled us back was not nostalgia. It was AI agents, and three unsentimental facts about where their work happens. First, the terminal is the one interface that already exists everywhere an agent works: every server, every SSH session, every CI box. Second, it composes. Pipes, logs, and replayable event journals are native citizens, and an agent's output is exactly that kind of material. Third, it is cheap to render. A status row updating thirty times a second costs nothing, which matters when the interesting part of your product is the model, not the chrome. A chat web app shows you what an agent says. The terminal is where the agent's actual work already lives. Putting the interface there removes a whole layer of translation. The hardware quietly got good The return would have looked terrible on the terminal we abandoned. It looks good now because the substrate improved while nobody was watching. Color came first. Two shades became sixteen, then 256, then 24-bit truecolor: 16.7 million colors per cell, standard in every serious emulator today. !Step chart titled Colors one terminal cell can show: 2 in 1978, 16 in the 1980s, 256 in 1999, 16.7 million truecolor in the 2010s. Then came the protocols that fixed the old embarrassments. Synchronized output lets an app wrap a whole frame in a pair of escape sequences so the terminal paints it atomically. No flicker, no tearing. Support landed in VS Code's terminal and tmux this year, work driven in part by Claude Code's anti-flicker push. The kitty graphics protocol transmits real pixels, and Ghostty, WezTerm, Konsole, iTerm2, and Warp all speak it. Where it is missing, there is a lovely fallback: the half-block character ▀, with the foreground color as the top pixel and the background as the bottom. Two pixels per cell, on any terminal made since the Reagan administration. !Diagram titled Two pixels per character cell: the half-block glyph draws two image pixels in one cell, foreground for the top pixel and background for the bottom, the image fallback used by chafa, viu, timg, ratatui-image and OpenTUI. Add GPU-rendered emulators like Ghostty, and beauty stopped being impossible. It just needed a reason. Millions of developers staring at agent harnesses all day turned out to be the reason. What the AI coding tools run on Look at what the major coding agents actually ship, and the field splits into camps. !Bar chart titled AI coding CLIs by TUI stack, August 2026: Ink and React family 5, Rust and ratatui 2, plain REPL 2, OpenTUI and Solid 1, Go and Bubble Tea 1. | Tool | Stack | Notes | |---|---|---| | Claude Code | TS, React, forked Ink | custom reconciler, 140+ components | | Codex CLI | Rust + ratatui | rewrote from TS + Ink in 2025 | | Gemini CLI | TS, React 19 + forked Ink | open source | | Grok Build | Rust + ratatui | open-sourced July 2026 | | opencode | OpenTUI + SolidJS | was Go + Bubble Tea | | Crush | Go + Bubble Tea v2 | the original opencode lineage | | Aider | prompt_toolkit + Rich | a REPL by choice | Half that chart says Ink, so Ink deserves a proper introduction. Ink is a renderer that points React at the terminal instead of the browser. You write ordinary React components with hooks and state, lay them out with flexbox through Yoga, the same layout engine React Native uses, and Ink turns the tree into styled lines of text, re-printing what changed. Its pull is simple: millions of developers already think in React, and Ink lets them keep thinking in it. Its limits are just as simple: it is pure JavaScript, line-oriented, capped around 30 frames per second, with no mouse support and no images. Those limits explain the table's real story. The biggest Ink users all ended up forking Ink rather than using it stock; Claude Code's build carries a custom React reconciler and its own layout engine. And OpenAI threw away a working TypeScript app to rewrite Codex in Rust on ratatui. When teams this well resourced keep rebuilding the same layer by hand, the layer is missing from the ecosystem. OpenTUI is the most direct answer to that gap, and its origin is the point. The team behind opencode (Anomaly, formerly the SST crew) started with a Go interface built on Bubble Tea, but wanted the whole product in TypeScript. Nothing in the JS world could draw fast enough, so they built their own framework and rebuilt opencode on it. OpenTUI keeps React's programming model but replaces Ink's machinery: a native rendering core written in Zig holds the screen as a buffer of cells, diffs each frame, and repaints only what changed. That buys the things Ink cannot do: no frame cap, full mouse support, real images over the kitty graphics protocol with half-block fallback. It is MIT-licensed, documented at opentui.com, runs opencode in production today, and is the closest thing the TypeScript terminal world has to a common foundation forming in real time. The other camps are healthy too. Charm shipped Bubble Tea v2 in February, hardened inside Crush. Rust has ratatui, and Python has Textual. Nobody is starving; the fight is over which model wins the next decade of terminal software. The parts everyone rebuilds Here is the gap all those forks point at. Chat components exist in every ecosystem: message bubbles, spinners, markdown renderers. But a harness is not a chat. A harness needs a status row that proves the agent is alive and tells you what this turn costs. An approval prompt that gates a real action on your real machine, with the safe option first. A home screen that says which model is pointed at which screen. Nobody ships those, so every team on that chart has built them from scratch, and several built them wrong in the same off-by-one-cell ways. A copy-paste culture is forming around the problem, in the mold of shadcn/ui: registries like termcn, TermUI, and InkUI distribute terminal component source you own and edit. Vercel's @ai-sdk/tui attacks the same space from the SDK side. --- I got tired of rebuilding the harness parts too, so I decided to open source the parts I use. agentparts is a small library of exactly these components, built on OpenTUI: the turn status row, the approval prompt, the shell layout, the prompt box. All the width arithmetic lives in a renderer-free core that runs on plain Node. Feel free to use for your harnesses! !Screenshot of AgentParts, terminal components for agent harnesses: model and policy info, a list of recent agent sessions, and a prompt box with key hints. > >> arthurkatcher/agentparts --- So you want to build one Start with an honest question: does this need to be full-screen at all? Aider and Goose are proof that a well-formatted REPL can carry a serious product. If your interface is a conversation with occasional structure, print good output and stop. Reach for a TUI framework when you need layout: panes, status rows, things that update in place. !Decision tree titled Choosing a TUI stack: if it doesn't need the whole screen, a REPL with good output; if it does, a full-screen framework by language: Ink or OpenTUI for TypeScript, ratatui for Rust, Bubble Tea v2 for Go, Textual for Python. If you do need one, the choice is mostly made by the language you already live in: | You write | Reach for | Why | |---|---|---| | TypeScript | Ink, or OpenTUI | Ink is mature; OpenTUI adds speed, mouse, images | | Rust | ratatui + crossterm | the default; start from ratatui/templates | | Go | Bubble Tea v2 + Bubbles | best component library, Lip Gloss for styling | | Python | Textual | CSS-like styling, rich widget gallery | The beautiful part is not the framework. It is discipline about the medium: Design a palette, then design its absence. Pick a handful of truecolor values on purpose, and decide what the interface looks like with color stripped away: plainly unstyled, never broken. Honor NO_COLOR, the environment variable users set when they want none. Get width right. Test with CJK text and emoji, and snapshot your layout at 40, 80, and 132 columns, because every TUI bug is an off-by-one-cell bug. Never flicker. Wrap each frame in synchronized output, the escape pair that tells the terminal to paint it in one go, and diff before you draw. Shed, don't squish. When the window gets narrow, drop the least important element entirely instead of truncating everything a little. And steal the parts that already exist: the spinners, the status rows, the prompts. Bubbles has them in Go, the registries have them in TypeScript. The chrome is the least differentiated thing you will build, which is exactly why you should not build it twice. Step back far enough and the cycle stops looking like a cycle. We did not return to the terminal we left. We brought everything back with us: React reconcilers running flexbox, CSS-like styling, component registries, GPU rendering, twenty years of interface craft applied to a character grid. The real trade was never terminal versus GUI. It is fidelity versus leverage, how rich an interface can be against how many places it can reach, and right now leverage is winning. Good design finally reached the terminal, one escape sequence at a time. --- Originally published on arthurkatcher.com.

    1 reaction
Trending awards, every completed day and week
Gold
01st place finishes
Silver
12nd place finishes
Bronze
33rd place finishes
Awarded repositories
1Owned by vadimdemedes now

Repositories

Ordered by gold, then silver, then bronze.

  1. inkBest: 2nd by stars, 592 stars013

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.