The earlier posts in this series covered the Esc-handling fix (rapid typing after Esc was eating the next character) and bracketed paste mode (multi-line pastes were firing each line as a separate query). Both bugs surfaced when the REPL got exercised through real PTY input rather than just inspected as code.
This week's keyboard probes continued through the rest of the surface. Most of it was already working — Ctrl-W word delete, Ctrl-L clear screen, Ctrl-A jump to start, left and right arrow navigation, Ctrl-E end of line, the standard emacs-style bindings that Node's readline provides for free. Backspace works. History navigation with the up arrow works. Long pastes work, multi-line pastes work as of last week.
What surfaced this week were two issues. Different in kind from each other, similar in flavor: code that existed in the source but didn't actually take effect, and a behavior that didn't match the convention every other shell uses.
Tab completion: defined but never wired
Tab completion in readline is straightforward. You write a function — by convention called completer — that takes the current input line and returns an array of candidate completions. You pass it to createInterface as the completer option. Readline handles the rest: a single matching candidate replaces the input, multiple candidates print a list, no candidates do nothing.
The function in this tool's source had been written months ago. It contained a list of common queries ("morning brief", "what changed today", "any new mail?") and all the slash commands ("/dashboard", "/find", "/events", "/tasks", "/open", "/guide", etc.). It was correctly structured — given an input line, it returned the matching candidates in the format readline expects.
What it wasn't, until this fix, was actually wired to readline. The createInterface call had every other option in its bag — input, output, prompt, terminal, history, escapeCodeTimeout — but no completer. Tab in the prompt just inserted a literal tab character into the input buffer.
An attorney who'd been using the tool for weeks would have hit Tab at some point, expected something to happen, seen nothing, and stopped trying. The feature looked like it didn't exist. Reading the code, you'd see a complete-looking completer function and assume Tab worked. Running the code, it didn't.
The fix was one line: add completer to the options object. After the fix, typing /d + Tab completes to /dashboard. Double-Tab on just / shows the full list of slash commands. Empty-line Tab shows the common-query candidates. All the structure was already there; just the connecting line was missing.
Ctrl-C: shell convention vs. instant kill
The second issue was different. The behavior was deliberate — there's a SIGINT handler that explicitly exits on Ctrl-C when the REPL isn't busy. The deliberate choice was the wrong one.
Imagine the attorney is 200 characters into a careful question. They notice a typo, fumble for the backspace key, and accidentally hit Ctrl-C instead. The old behavior: process.exit(0). The session terminates immediately. The 200 characters are gone. The session has to be restarted, which takes a few seconds and dumps a fresh header into the terminal. Then the question has to be retyped from scratch.
Compare to bash: Ctrl-C clears the current line, returns you to a fresh prompt. You stay in your shell session. Python REPL: same. Node REPL: same. ipython: same. The convention is decades old and consistent across every interactive shell that humans actually type into. The convention exists exactly because the alternative ("kill the session on the first Ctrl-C") is hostile to the user's typing flow.
The aaicase tool wasn't matching the convention. Probably because Ctrl-C was added before the typed-text-loss case was considered — at the time, "Ctrl-C exits cleanly" was the easy thing to implement, and nobody had typed 200 characters into the prompt and then accidentally pressed it yet.
The fix matches the standard convention now. Ctrl-C with text typed: clears the line in place, just like Esc does. Ctrl-C on an empty prompt: prints a hint ("Use 'exit' or press Ctrl-C again to leave"), and a second Ctrl-C within two seconds actually exits. So a fumble produces an annoying hint message at worst; a deliberate exit takes two presses.
The implementation was harder than the Tab-completion fix. Readline's default behavior on Ctrl-C is to close the interface, which triggers the rl.on('close', () => process.exit(0)) we have. Just adding a SIGINT handler at the process level wasn't enough — readline still closed itself first, and the close handler still fired the exit. The fix required wrapping readline's _ttyWrite method (the same wrap point we used for bracketed paste) to intercept the 0x03 byte before readline saw it as a Ctrl-C. The intercept emits SIGINT manually so the process-level handler runs with the new line-clear vs. exit logic.
The pattern
Both bugs are flavors of the same thing: code that exists in the source but doesn't actually take effect in the running system. Tab completion was complete but unwired. Ctrl-C had a handler but the handler did the wrong thing (because the implementer hadn't considered the conventional behavior). Reading the source, both looked fine. Running it, both failed.
The lesson is the same one we've been arriving at all month. Verification by inspection isn't sufficient for code that interacts with humans. The interactive surfaces have to be exercised — through a PTY, through a real keyboard, through an automated harness that simulates one. Inspection catches type errors and logic bugs. It doesn't catch "I forgot to wire this up" or "I implemented the wrong convention."
The keyboard probe series has now found four real interactive bugs:
- Esc + rapid type → first character lost (4.5.76)
- Multi-line paste → each line submitted as separate query (4.5.77)
- Tab → literal tab character, no completion (4.5.78)
- Ctrl-C with typed text → session kill (4.5.79)
Four bugs, all invisible to code inspection. All visible the first time the relevant key was actually pressed under realistic conditions. Every one of them affected real attorney workflow.
The next probe pass will look at edge cases — what happens when the terminal window is resized mid-input, what happens with extremely long input lines that wrap, what happens with Unicode characters that occupy two columns in the terminal. The pattern of "test the keyboard surface directly" has been productive enough that I expect more findings.
The version on the install URL is 4.5.79. The work continues at the pace of one verified release at a time, and the part of the system the attorney actually touches is getting steadily better at meeting them where they are.