惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

宝玉的分享
宝玉的分享
小众软件
小众软件
J
Java Code Geeks
I
InfoQ
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
腾讯CDC
L
LangChain Blog
博客园 - 司徒正美
量子位
Y
Y Combinator Blog
C
Check Point Blog
T
Tailwind CSS Blog
D
DataBreaches.Net
Blog — PlanetScale
Blog — PlanetScale
N
Netflix TechBlog - Medium
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
F
Fortinet All Blogs
云风的 BLOG
云风的 BLOG
A
About on SuperTechFans
B
Blog RSS Feed
酷 壳 – CoolShell
酷 壳 – CoolShell
大猫的无限游戏
大猫的无限游戏
V
V2EX
阮一峰的网络日志
阮一峰的网络日志

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
From "I Can't click" to a Full Testing Harness: How We Bu...
Eugene Yakhnenko · 2026-06-28 · via DEV Community

I'm building TTT -- a terminal text editor and IDE written in Go. Single binary, zero config, runs anywhere. Think VS Code but in your terminal. It has syntax highlighting, LSP integration, a plugin system, an integrated terminal, git integration, etc...

The source is on GitHub and I develop it with Claude Code as my pair programmer. This is the story of how a frustrating limitation turned into something genuinely useful: a built-in scripted interaction system that lets AI agents (or anyone) drive the editor like Playwright drives a browser.

The problem

I was deep in revamping the widget system and building out a Lua plugin API. Phases of work stacking up -- widget rendering, panel support, tree views, input fields, command registration, keybinding hooks. The kind of work where you need to see what's happening. Click a tree node, check if it expands. Open a panel, verify focus moves correctly. Run a plugin, confirm the dialog appears.

Here's the thing: Claude Code can run shell commands and read files. It cannot interact with a live TUI session. The editor launches, takes over the terminal, and that's it -- Claude is blind.

Step 1: tui-use (what we had)

The project already had functional tests using tui-use, a JavaScript library that drives a real terminal binary. It can type, press keys, wait for text to appear, and take snapshots:

const tui = await start("bin/ttt", ["test-file.go"]);
await tui.waitFor("test-file.go");
await tui.exec("editor.joinLines");
const screen = await tui.snapshot();
expect(screen).toContain("joined line");

This works. But it's slow -- each test spawns the binary, waits for screen renders, polls with timeouts, and parses terminal escape codes. And critically, it can't click. Mouse events aren't supported. For a widget system with tree views, buttons, and split panels, that's a dealbreaker.

Step 2: Debug commands (the workaround)

So we added a Debug: Simulate Click command to the editor itself. Open the command palette, type coordinates, it fires a synthetic mouse event. Problem solved? Kind of. Claude still can't drive a live session to use it.

But it planted a seed: the editor can simulate its own input.

Step 3: Lua as a test harness

TTT has a Lua plugin system. Plugins can register panels, commands, keybindings, and interact with the editor through a ttt module. We added ttt.click(x, y) directly to the Lua API:

local ttt = require("ttt")
ttt.click(10, 5)

Then ttt.screenshot(path) to dump the screen to a file, and ttt.debug(path) to dump the full internal state -- widget tree, focus, selection, panels, cursor position -- as JSON.

Now a Lua script could interact with the editor and capture results to files that Claude can read:

local ttt = require("ttt")
ttt.click(10, 5)
ttt.screenshot("/tmp/after-click.txt")
ttt.debug("/tmp/state.json")
ttt.quit()

We added a --plugin CLI flag to load a Lua file on startup, and --size WxH to force deterministic screen dimensions:

bin/ttt --plugin test.lua --size 120x40
cat /tmp/after-click.txt
cat /tmp/state.json

This was a breakthrough. Claude could now write a Lua script, run the editor, and inspect the results. But writing a Lua file for every quick check felt heavy.

Step 4: --exec (the final form)

The insight: most debugging interactions are simple sequences. Click here, press that key, take a screenshot. Why write a file?

We added --exec, which takes a semicolon-separated string of commands:

bin/ttt --size 120x40 --exec "wait 200; screenshot /tmp/s1.txt; click 10 5; wait 100; screenshot /tmp/s2.txt; quit"

The supported commands:

Command What it does
click X Y Simulate a mouse click
key COMBO Simulate a key press (ctrl+p, enter, etc.)
type TEXT Type a string character by character
exec "Command" Run a command by title
screenshot PATH Dump screen text to a file
debug PATH Dump full state JSON (widget tree, focus, etc.)
wait MS Wait milliseconds
quit Exit

Now Claude can do this in a single bash command:

bin/ttt --size 120x40 file.go --exec "wait 200; screenshot /tmp/screen.txt; debug /tmp/state.json; quit" && cat /tmp/screen.txt

And instantly see what's on screen. No Lua file, no polling, no escape codes. Build, run, inspect -- milliseconds, not seconds.

What the debug dump looks like

The debug command captures everything you'd want to assert on:

{
  "screen": { "width": 120, "height": 40 },
  "cursor": { "line": 5, "col": 10 },
  "buffer": { "path": "file.go", "lines": 42, "modified": false },
  "focus": "editor",
  "sidebar": { "visible": true, "active": "explorer", "panels": ["explorer", "search", "changes"] },
  "bottom_panel": { "visible": false, "active": "output" },
  "tabs": [{ "path": "file.go", "modified": false }],
  "selection": { "active": false },
  "output": [],
  "widget_tree": {
    "type": "VStack",
    "rect": { "x": 0, "y": 0, "w": 120, "h": 40 },
    "children": [
      {
        "type": "MenuBar",
        "rect": { "x": 0, "y": 0, "w": 120, "h": 1 }
      },
      {
        "type": "SplitPanel",
        "props": { "show_left": true, "divider_pos": 30 },
        "children": [
          {
            "type": "Sidebar",
            "props": { "visible": true, "active": "explorer" },
            "children": [
              {
                "type": "Tree",
                "props": { "items": 12, "selected": 3 },
                "focused": true
              }
            ]
          }
        ]
      }
    ]
  }
}

The screenshot gives you the text. The debug dump gives you the state. Between the two, you can verify anything -- layout, focus, widget hierarchy, selection, which panel is active, how many items are in a tree.

The progression

Looking back, each step was small but unlocked something the previous couldn't:

  1. tui-use -- keyboard-only blackbox testing, slow, no mouse
  2. Debug: Simulate Click -- mouse support, but only through command palette
  3. ttt.click() in Lua -- programmatic mouse, but requires writing a plugin file
  4. ttt.screenshot() + ttt.debug() -- capture both visual and internal state
  5. --exec -- one-liner scripted interaction, no files needed

What started as "I can't click in tests" ended up as a general-purpose testing and debugging harness that's faster than Playwright, gives deeper insight (you get the widget tree, not just pixels), and works for both AI agents and human developers.

Using it for real

The PR that adds all of this: feat/debug-commands #274

The immediate use case: I'm working through an audit of the plugin and widget system. Dozens of items to fix -- tree expand ordering, box padding, focus management, API consistency. For each fix, I can now:

  1. Write the fix
  2. bin/ttt --size 120x40 --exec "wait 200; debug /tmp/state.json; quit"
  3. Assert on the state
  4. Move on

No test file to maintain. No polling. No flaky waits. Just build, run, check.

Try it

TTT is open source and available at tttedit.dev. Install it, open a file, run Debug: Dump State from the command palette, and look at the JSON. The full widget tree is right there.

If you're building a TUI application and struggling with testing, consider this pattern: expose your internal state through a debug dump, add a scripted command interface, and let your tools (AI or otherwise) drive it programmatically. It's surprisingly little code for a lot of capability.