summaryrefslogtreecommitdiff
path: root/prompts
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-08 17:33:04 +0200
committerPaul Buetow <paul@buetow.org>2026-02-08 17:33:04 +0200
commit944838bb0f753a0920ddb2f506758c410ed7ca43 (patch)
tree12d6ae9e2de8878891159e363134a3f07686ed7a /prompts
parentc802ba5803de1a53749bb5c4ecbc0159fceb385f (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.md445
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
+```