summaryrefslogtreecommitdiff
path: root/internal/tmuxedit/send.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-08 15:19:36 +0200
committerPaul Buetow <paul@buetow.org>2026-02-08 15:19:36 +0200
commit887d7bc186db90c3903851b0f1db2d24df5d7a7b (patch)
tree1cfb8055ddbb907bae9461b924dda1d2b3f15b46 /internal/tmuxedit/send.go
parent6da37034708dc7d4dcb7c71e890478a68e6ae4a1 (diff)
fix hexai-tmux-edit agent detection, multi-line extraction, and clearing
- Reorder agents: cursor first to avoid false positive from "Claude 4.5 Sonnet" model name appearing in cursor panes - Change cursor detect pattern to box-drawing UI elements instead of name-based matching - Change claude detect pattern to ❯ prompt instead of generic "claude" - Support multi-line prompt extraction using last-contiguous-block algorithm to ignore command-review and dialog boxes - Fix deduplicateText to always return full edited text (clear + resend) instead of stripping original prefix which caused double-removal - Replace vim-style clear (Escape gg dG i) with universal End+BSpace*200 since cursor's prompt is not actually vim - Add Key*N repeat syntax in ClearKeys (parsed by parseKeyRepeat, sent via tmux send-keys -N) - Add 300ms sleep after clear to let TUI drain queued backspaces before new text arrives - Add debug logging to /tmp/hexai-tmux-edit.log - Add strip pattern for "ctrl+c to stop" noise in cursor prompt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/tmuxedit/send.go')
-rw-r--r--internal/tmuxedit/send.go92
1 files changed, 69 insertions, 23 deletions
diff --git a/internal/tmuxedit/send.go b/internal/tmuxedit/send.go
index b85f7d3..ea63057 100644
--- a/internal/tmuxedit/send.go
+++ b/internal/tmuxedit/send.go
@@ -2,7 +2,9 @@ package tmuxedit
import (
"fmt"
+ "strconv"
"strings"
+ "time"
)
// sendKeys is the seam for `tmux send-keys`. Override in tests.
@@ -15,45 +17,41 @@ var sendKeys = func(paneID string, keys ...string) error {
return nil
}
-// deduplicateText removes the original (pre-filled) text from the edited
-// result. If the user kept the original and appended, only the new text is
-// returned. If the user rewrote everything, the full new text is returned.
+// deduplicateText compares the original (pre-filled) text with what the user
+// returned from the editor. Returns empty string if unchanged (no-op), or
+// the full edited text if anything changed. The caller is responsible for
+// clearing existing pane input before sending the result, so we always return
+// the complete text rather than stripping the original prefix.
func deduplicateText(original, edited string) string {
original = strings.TrimSpace(original)
edited = strings.TrimSpace(edited)
- if edited == "" {
+ if edited == "" || edited == original {
return ""
}
- if original == "" {
- return edited
- }
- // If the edited text starts with the original, return only the appended part
- if strings.HasPrefix(edited, original) {
- appended := strings.TrimSpace(edited[len(original):])
- if appended != "" {
- return appended
- }
- // User didn't change anything; return empty to signal no-op
- return ""
- }
- // User rewrote the prompt; return the full new text
return edited
}
// sendTextToPane sends the given text to the target pane. It optionally
-// clears existing input first (using the agent's ClearKeys), then sends
-// text line-by-line using the agent's NewlineKeys between lines.
+// clears existing input first (using the agent's ClearKeys sequence), then
+// sends text line-by-line using the agent's NewlineKeys between lines.
+// ClearKeys is space-separated; tokens like "BSpace*200" repeat a key N times
+// via tmux send-keys -N. Example: "End BSpace*200" moves to end then
+// sends 200 backspaces to clear the entire prompt buffer.
func sendTextToPane(paneID, text string, agent AgentConfig) error {
if strings.TrimSpace(text) == "" {
return nil
}
- // Clear existing input if configured
+ // Clear existing input using the key sequence (space-separated).
+ // Each token is sent as a separate tmux send-keys call.
+ // A short pause after clearing lets the TUI process all queued
+ // keystrokes (e.g. 200 backspaces) before new text arrives.
if agent.ClearFirst && agent.ClearKeys != "" {
- if err := sendKeys(paneID, agent.ClearKeys); err != nil {
- return fmt.Errorf("clear failed: %w", err)
+ if err := sendClearSequence(paneID, agent.ClearKeys); err != nil {
+ return err
}
+ sleepAfterClear()
}
- // Send text line by line, inserting newline keys between lines
+ // Send text line-by-line, inserting newline keys between lines
lines := strings.Split(text, "\n")
for i, line := range lines {
if err := sendKeys(paneID, line); err != nil {
@@ -72,3 +70,51 @@ func sendTextToPane(paneID, text string, agent AgentConfig) error {
}
return nil
}
+
+// sleepAfterClear pauses to let the TUI drain queued keystrokes (like bulk
+// backspaces) before new text is sent. Override in tests to avoid delays.
+var sleepAfterClear = func() { time.Sleep(300 * time.Millisecond) }
+
+// sendClearSequence parses a space-separated key sequence and sends each
+// token individually. Tokens with a "*N" suffix (e.g. "BSpace*200") are
+// sent N times using tmux send-keys -N for efficient bulk repeats.
+func sendClearSequence(paneID, clearKeys string) error {
+ for _, token := range strings.Fields(clearKeys) {
+ key, count := parseKeyRepeat(token)
+ if count > 1 {
+ if err := sendRepeatedKey(paneID, key, count); err != nil {
+ return fmt.Errorf("clear key %q*%d failed: %w", key, count, err)
+ }
+ } else {
+ if err := sendKeys(paneID, key); err != nil {
+ return fmt.Errorf("clear key %q failed: %w", key, err)
+ }
+ }
+ }
+ return nil
+}
+
+// sendRepeatedKey is the seam for `tmux send-keys -N <count>`. Override in
+// tests. Uses -N for efficient bulk key repeats (e.g. 200 backspaces).
+var sendRepeatedKey = func(paneID, key string, count int) error {
+ args := []string{"send-keys", "-t", paneID, "-N", strconv.Itoa(count), key}
+ _, err := runCommand("tmux", args...)
+ if err != nil {
+ return fmt.Errorf("send-keys -N failed: %w", err)
+ }
+ return nil
+}
+
+// parseKeyRepeat splits "Key*N" into (Key, N). Returns (token, 1) if no
+// repeat suffix is present or the suffix is invalid.
+func parseKeyRepeat(token string) (string, int) {
+ idx := strings.LastIndex(token, "*")
+ if idx < 1 || idx >= len(token)-1 {
+ return token, 1
+ }
+ n, err := strconv.Atoi(token[idx+1:])
+ if err != nil || n < 1 {
+ return token, 1
+ }
+ return token[:idx], n
+}