diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-08 17:33:04 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-08 17:33:04 +0200 |
| commit | 944838bb0f753a0920ddb2f506758c410ed7ca43 (patch) | |
| tree | 12d6ae9e2de8878891159e363134a3f07686ed7a /prompts | |
| parent | c802ba5803de1a53749bb5c4ecbc0159fceb385f (diff) | |
Fix amp agent prompt extraction to use TUI box pattern
Amp CLI runs in TUI mode with box-drawing UI (│ text │) similar to
Cursor, not shell-style (> prompt). Updated prompt pattern from
`(?m)>\s*(.+)$` to `(?m)│\s*(.+?)\s*│\s*$` to correctly extract
text from amp's box UI.
Changes:
- internal/tmuxedit/config_agent.go: Update amp promptPat to box pattern
- internal/tmuxedit/config_agent_test.go: Update test to use box format
- docs/usage.md: Document detection order and clear methods for all agents
- docs/tmux.md: Clarify input mode handling (Vim vs Emacs/readline)
- config.toml.example: Add detailed agent descriptions and patterns
- prompts/tmux-edit-integration-tests.md: Add test status, mock editor best practices
Integration tests verified:
- Amp detection: amp/sourcegraph keywords
- Prompt extraction: "hello world test" correctly captured
- End-to-end workflow: text modification and sending works
- Multi-line support: all lines delivered correctly
- All unit tests pass (67/67)
- Coverage: 80.9% (meets requirement)
- Cursor and Claude implementations unchanged
Co-authored-by: Cursor <cursoragent@cursor.com>
Diffstat (limited to 'prompts')
| -rw-r--r-- | prompts/tmux-edit-integration-tests.md | 445 |
1 files changed, 445 insertions, 0 deletions
diff --git a/prompts/tmux-edit-integration-tests.md b/prompts/tmux-edit-integration-tests.md new file mode 100644 index 0000000..a0525b0 --- /dev/null +++ b/prompts/tmux-edit-integration-tests.md @@ -0,0 +1,445 @@ +# hexai-tmux-edit Integration Test Runbook + +Real-life integration tests against actual tmux panes running AI agent CLIs. +These tests verify prompt capture, agent detection, multi-line extraction, +text clearing, and the full edit-and-replace flow. + +**Test Status**: +- ✅ **cursor-agent**: All tests passed (Feb 8, 2026) +- ✅ **amp**: All tests passed (Feb 8, 2026) - Note: amp uses TUI mode with Emacs/readline keybindings +- ⏳ **claude**: Needs testing +- ⏳ **aider**: Needs testing + +**Important**: Agent detection and prompt extraction rely on regex patterns +matched against each agent's TUI output (box-drawing characters, prompt +symbols, status text). When agents update their TUI, these patterns may +break. If tests fail after an agent update, check the built-in patterns in +`internal/tmuxedit/agent.go` `builtinAgents()` and adjust the +`DetectPattern`, `PromptPattern`, and `StripPatterns` fields accordingly. +Users can also override patterns via `[[tmux_edit.agents]]` in config +without code changes. + +## Prerequisites + +- Must be running inside a tmux session +- `hexai-tmux-edit` binary must be installed (`go build -o ~/go/bin/hexai-tmux-edit ./cmd/hexai-tmux-edit/`) +- At least one tmux pane running `cursor-agent` with an empty prompt +- All unit tests must pass first: `go test ./internal/tmuxedit/` + +## Finding a test pane + +List all panes and pick one running cursor-agent with an idle prompt: + +```sh +tmux list-panes -a -F '#{pane_id} #{window_name} #{pane_current_command}' +``` + +Look for a pane running `cursor-agent`. Use its `%NN` pane ID throughout. +Verify it has an empty input prompt: + +```sh +tmux capture-pane -p -t '%NN' | tail -10 +``` + +You should see the box-drawing prompt: +``` + ┌─────────────────────┐ + │ → Add a follow-up │ + └─────────────────────┘ +``` + +If the prompt has text in it, clear it first: + +```sh +tmux send-keys -t '%NN' End && sleep 0.1 && tmux send-keys -t '%NN' -N 200 BSpace +``` + +## Test 1: Single-line prompt capture + +**Goal**: Verify that text typed into cursor-agent's prompt is correctly +detected and extracted. + +```sh +# 1. Send test text to the prompt +tmux send-keys -t '%NN' 'hello world test' + +# 2. Wait for the TUI to render +sleep 1 + +# 3. Verify text appeared in prompt +tmux capture-pane -p -t '%NN' | grep '│.*→' +# Expected: │ → hello world test │ + +# 4. Write a Go test script to verify extraction logic +cat > /tmp/test_capture.go << 'GOEOF' +//go:build ignore + +package main + +import ( + "fmt" + "os" + "os/exec" + "regexp" + "strings" +) + +func main() { + pane := os.Args[1] + out, err := exec.Command("tmux", "capture-pane", "-p", "-t", pane).Output() + if err != nil { + fmt.Printf("FAIL: capture error: %v\n", err) + os.Exit(1) + } + content := string(out) + + // Verify cursor agent detected + detectRe := regexp.MustCompile(`(│\s*→|/ commands · @ files)`) + if !detectRe.MatchString(content) { + fmt.Println("FAIL: cursor agent not detected") + os.Exit(1) + } + fmt.Println("PASS: cursor agent detected") + + // Verify prompt extracted + promptRe := regexp.MustCompile(`(?m)│\s*→?\s*(.+?)\s*│\s*$`) + m := promptRe.FindStringSubmatch(content) + if len(m) < 2 { + fmt.Println("FAIL: no prompt match") + os.Exit(1) + } + text := m[1] + for _, s := range []string{"INSERT", "Add a follow-up", "ctrl+c to stop"} { + text = strings.ReplaceAll(text, s, "") + } + text = strings.TrimSpace(text) + fmt.Printf("PASS: extracted %q\n", text) +} +GOEOF + +go run /tmp/test_capture.go '%NN' +# Expected: PASS: cursor agent detected +# PASS: extracted "hello world test" + +# 5. Clean up +tmux send-keys -t '%NN' End && sleep 0.1 && tmux send-keys -t '%NN' -N 200 BSpace +rm /tmp/test_capture.go +``` + +## Test 2: Multi-box disambiguation + +**Goal**: Verify that when cursor-agent shows multiple box-drawing sections +(e.g. follow-ups box + input prompt), only the last box (the input prompt) +is captured. + +This test requires a pane that has a follow-ups section above the input +prompt. You can create this state by sending text with Shift+Enter (which +cursor interprets as submit-and-queue-follow-up). + +```sh +# 1. Verify the pane has multiple boxes visible +tmux capture-pane -p -t '%NN' | grep '│' | head -20 +# Look for multiple │ lines from different boxes + +# 2. Type text into the (bottom) input prompt +tmux send-keys -t '%NN' 'only this should be captured' +sleep 1 + +# 3. Run extraction and verify only the last box is picked +cat > /tmp/test_multibox.go << 'GOEOF' +//go:build ignore + +package main + +import ( + "fmt" + "os" + "os/exec" + "regexp" + "strings" +) + +type promptMatch struct { + lineNum int + text string +} + +func main() { + pane := os.Args[1] + out, err := exec.Command("tmux", "capture-pane", "-p", "-t", pane).Output() + if err != nil { + fmt.Printf("FAIL: capture error: %v\n", err) + os.Exit(1) + } + content := string(out) + re := regexp.MustCompile(`(?m)│\s*→?\s*(.+?)\s*│\s*$`) + strips := []string{"INSERT", "Add a follow-up", "ctrl+c to stop"} + + // Find all matches with line numbers + paneLines := strings.Split(content, "\n") + var matches []promptMatch + for i, line := range paneLines { + m := re.FindStringSubmatch(line) + if len(m) >= 2 { + matches = append(matches, promptMatch{i, m[1]}) + } + } + fmt.Printf("Total matches across all boxes: %d\n", len(matches)) + + // Take last contiguous block + if len(matches) == 0 { + fmt.Println("FAIL: no matches found") + os.Exit(1) + } + last := len(matches) - 1 + start := last + for start > 0 && matches[start].lineNum-matches[start-1].lineNum == 1 { + start-- + } + + fmt.Printf("Last contiguous block: lines %d-%d (%d matches)\n", + matches[start].lineNum, matches[last].lineNum, last-start+1) + + var lines []string + for i := start; i <= last; i++ { + text := matches[i].text + for _, s := range strips { + text = strings.ReplaceAll(text, s, "") + } + text = strings.TrimSpace(text) + if text != "" { + lines = append(lines, text) + } + } + result := strings.Join(lines, "\n") + fmt.Printf("EXTRACTED: %q\n", result) + + if strings.Contains(result, "only this should be captured") { + fmt.Println("PASS: last box correctly isolated") + } else { + fmt.Println("FAIL: wrong box content captured") + os.Exit(1) + } +} +GOEOF + +go run /tmp/test_multibox.go '%NN' +# Expected: Total matches > 1 (from multiple boxes) +# PASS: last box correctly isolated + +# 3. Clean up +tmux send-keys -t '%NN' End && sleep 0.1 && tmux send-keys -t '%NN' -N 200 BSpace +rm /tmp/test_multibox.go +``` + +## Test 3: Clear and retype flow + +**Goal**: Verify that `End + BSpace*200` clears the existing prompt text +and new text can be inserted afterwards. + +```sh +# 1. Type original text +tmux send-keys -t '%NN' 'original text here' +sleep 0.5 +tmux capture-pane -p -t '%NN' | grep '│.*→' +# Expected: │ → original text here │ + +# 2. Clear using the same method hexai-tmux-edit uses +tmux send-keys -t '%NN' End +sleep 0.1 +tmux send-keys -t '%NN' -N 200 BSpace +sleep 0.5 + +# 3. Verify prompt is empty +tmux capture-pane -p -t '%NN' | grep '│.*→' +# Expected: │ → Add a follow-up │ (placeholder = empty) + +# 4. Type replacement text +tmux send-keys -t '%NN' 'replacement text here' +sleep 0.5 +tmux capture-pane -p -t '%NN' | grep '│.*→' +# Expected: │ → replacement text here │ + +# 5. Clean up +tmux send-keys -t '%NN' End && sleep 0.1 && tmux send-keys -t '%NN' -N 200 BSpace +``` + +## Test 4: Full end-to-end with mock editor + +**Goal**: Run the actual `hexai-tmux-edit` binary against a real pane, +using a mock editor to automate the popup interaction. Verifies the +complete flow: capture -> detect agent -> extract prompt -> edit -> clear +-> send back. + +**Note**: Mock editors should include a small delay before exiting to ensure +tmux popups close cleanly. Without the delay, the popup might not register +the editor exit properly and could hang. + +```sh +# 1. Type text into the prompt +tmux send-keys -t '%NN' 'hello world' +sleep 0.5 +tmux capture-pane -p -t '%NN' | grep '│.*→' +# Expected: │ → hello world │ + +# 2. Create a mock editor that replaces content (with delay for clean popup close) +cat > /tmp/mock-editor.sh << 'SH' +#!/bin/sh +# Write new content +echo "hello universe" > "$1" +# Small delay ensures tmux popup closes cleanly +sleep 0.1 +SH +chmod +x /tmp/mock-editor.sh + +# 3. Run hexai-tmux-edit with the mock editor +HEXAI_EDITOR=/tmp/mock-editor.sh hexai-tmux-edit --pane '%NN' +# Wait for popup to close +sleep 0.5 + +# 4. Verify the prompt was updated +tmux capture-pane -p -t '%NN' | grep '│.*→' +# Expected: │ → hello universe │ + +# 5. Check debug log for the full trace +cat /tmp/hexai-tmux-edit.log +# Expected log should show: +# - agent detected as "cursor" +# - extractPrompt result: "hello world" +# - editor returned: "hello universe" +# - deduplicateText result: "hello universe" +# - sending to pane: "hello universe" +# - === done === + +# 6. Clean up +rm /tmp/mock-editor.sh +tmux send-keys -t '%NN' End && sleep 0.1 && tmux send-keys -t '%NN' -N 200 BSpace +``` + +## Test 5: Agent detection does not false-positive on model names + +**Goal**: Verify that a cursor-agent pane showing "Claude 4.5 Sonnet" as +the model name is detected as cursor (not claude). + +```sh +# 1. Capture pane content and check for model name +tmux capture-pane -p -t '%NN' | grep -i 'claude\|sonnet' +# This may show "Claude 4.5 Sonnet (Thinking)" in the status line + +# 2. Verify detection is still "cursor" +cat > /tmp/test_detect.go << 'GOEOF' +//go:build ignore + +package main + +import ( + "fmt" + "os" + "os/exec" + "regexp" +) + +func main() { + pane := os.Args[1] + out, _ := exec.Command("tmux", "capture-pane", "-p", "-t", pane).Output() + content := string(out) + + agents := []struct { + name string + detect string + }{ + {"cursor", `(│\s*→|/ commands · @ files)`}, + {"claude", `(❯|claude code|anthropic)`}, + } + for _, a := range agents { + re := regexp.MustCompile(a.detect) + if re.MatchString(content) { + fmt.Printf("Detected: %s\n", a.name) + if a.name == "cursor" { + fmt.Println("PASS: correctly detected as cursor") + } else { + fmt.Println("FAIL: should be cursor, not " + a.name) + os.Exit(1) + } + return + } + } + fmt.Println("FAIL: no agent detected") + os.Exit(1) +} +GOEOF + +go run /tmp/test_detect.go '%NN' +# Expected: PASS: correctly detected as cursor + +rm /tmp/test_detect.go +``` + +## Test 6: Unchanged text results in no-op + +**Goal**: If the user opens the editor and saves without changes, +nothing should be sent to the pane. + +```sh +# 1. Type text into the prompt +tmux send-keys -t '%NN' 'do not change me' +sleep 0.5 + +# 2. Create a mock editor that keeps the content unchanged (with delay) +cat > /tmp/mock-noop-editor.sh << 'SH' +#!/bin/sh +# Do nothing -- file already has the pre-filled content +# Small delay for clean popup close +sleep 0.1 +SH +chmod +x /tmp/mock-noop-editor.sh + +# 3. Run hexai-tmux-edit +HEXAI_EDITOR=/tmp/mock-noop-editor.sh hexai-tmux-edit --pane '%NN' +sleep 0.5 + +# 4. Check debug log -- should show "nothing to send" +grep 'nothing to send' /tmp/hexai-tmux-edit.log +# Expected: "nothing to send, exiting" + +# 5. Verify prompt text is unchanged (still has original text) +tmux capture-pane -p -t '%NN' | grep '│.*→' +# Expected: │ → do not change me │ + +# 6. Clean up +rm /tmp/mock-noop-editor.sh +tmux send-keys -t '%NN' End && sleep 0.1 && tmux send-keys -t '%NN' -N 200 BSpace +``` + +## Cleanup + +After all tests, remove any leftover temp files and check for hung popups: + +```sh +# Remove test files +rm -f /tmp/test_capture.go /tmp/test_multibox.go /tmp/test_detect.go +rm -f /tmp/mock-editor.sh /tmp/mock-noop-editor.sh + +# Check for any hung tmux popups (should show nothing) +tmux list-panes -a | grep popup || echo "✓ No hung popups" + +# If popups are hung, kill them +# pkill -f 'tmux.*popup' || true +``` + +## Troubleshooting + +**Popup doesn't close**: Mock editors that exit instantly (< 50ms) might not +give tmux enough time to register the exit. Add `sleep 0.1` before the editor +script exits. + +**Popup hangs**: If a popup is stuck open: +```sh +# Find popup panes +tmux list-panes -a | grep popup + +# Kill popup processes +pkill -f 'tmux.*popup' + +# Or press Escape in the tmux session to close active popup +``` |
