summaryrefslogtreecommitdiff
path: root/internal/tmuxedit/agent.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/agent.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/agent.go')
-rw-r--r--internal/tmuxedit/agent.go86
1 files changed, 69 insertions, 17 deletions
diff --git a/internal/tmuxedit/agent.go b/internal/tmuxedit/agent.go
index 2e07824..7be38ed 100644
--- a/internal/tmuxedit/agent.go
+++ b/internal/tmuxedit/agent.go
@@ -24,26 +24,36 @@ type AgentConfig struct {
SubmitKeys string // tmux key to submit the prompt (e.g. "Enter")
}
-// builtinAgents returns the default set of agent configurations. These are
-// overridden/extended by user config in [tmux_edit.agents].
+// builtinAgents returns the default set of agent configurations. Order
+// matters: agents with distinctive UI elements (box-drawing, etc.) are
+// checked first to avoid false positives from model names like "Claude
+// 4.5 Sonnet" appearing in other agents' panes. Overridden/extended by
+// user config in [tmux_edit.agents].
func builtinAgents() []AgentConfig {
return []AgentConfig{
{
- Name: "claude",
- DisplayName: "Claude Code",
- DetectPattern: `(?i)(claude|anthropic)`,
- PromptPattern: `(?m)>\s*(.+)$`,
+ // Cursor Agent uses a distinctive box-drawing │ → prompt │ UI.
+ // Detect by the box structure or "/ commands" footer. Checked
+ // first because cursor panes show model names like "Claude 4.5".
+ // Clear uses End + bulk backspace to delete all existing text.
+ // The *200 suffix sends 200 backspaces via tmux send-keys -N.
+ Name: "cursor",
+ DisplayName: "Cursor",
+ DetectPattern: `(│\s*→|/ commands · @ files)`,
+ PromptPattern: `(?m)│\s*→?\s*(.+?)\s*│\s*$`,
+ StripPatterns: []string{"INSERT", "Add a follow-up", "ctrl+c to stop"},
ClearFirst: true,
- ClearKeys: "C-u",
+ ClearKeys: "End BSpace*200",
NewlineKeys: "S-Enter",
SubmitKeys: "Enter",
},
{
- Name: "cursor",
- DisplayName: "Cursor",
- DetectPattern: `(?i)cursor`,
- PromptPattern: `(?m)│\s*(.+)$`,
- StripPatterns: []string{"INSERT", "Add a follow-up"},
+ // Claude Code uses ❯ prompt between ──── horizontal rules.
+ // Detect by the ❯ prompt or explicit "claude code" banner.
+ Name: "claude",
+ DisplayName: "Claude Code",
+ DetectPattern: `(❯|claude code|anthropic)`,
+ PromptPattern: `(?m)❯\s*(.+)$`,
ClearFirst: true,
ClearKeys: "C-u",
NewlineKeys: "S-Enter",
@@ -185,7 +195,10 @@ func findAgentByName(name string, agents []AgentConfig) AgentConfig {
}
// extractPrompt uses the agent's PromptPattern to extract the current prompt
-// text from pane content. Returns empty string if no pattern or no match.
+// text from pane content. For multi-line prompts (e.g. cursor's box-drawing
+// │...│ UI) it takes only the last contiguous group of matched lines, which
+// avoids picking up command-review or dialog boxes that use the same border
+// characters. Returns empty string if no pattern or no match.
func extractPrompt(paneContent string, agent AgentConfig) string {
if agent.PromptPattern == "" {
return ""
@@ -194,12 +207,51 @@ func extractPrompt(paneContent string, agent AgentConfig) string {
if err != nil {
return ""
}
- m := re.FindStringSubmatch(paneContent)
- if len(m) < 2 {
+ allMatches := matchPromptLines(re, paneContent)
+ if len(allMatches) == 0 {
return ""
}
- text := m[1]
- return stripNoise(text, agent.StripPatterns)
+ return joinLastContiguousBlock(allMatches, agent.StripPatterns)
+}
+
+// promptMatch holds a regex match result with its line number in the pane.
+type promptMatch struct {
+ lineNum int
+ text string // capture group 1
+}
+
+// matchPromptLines runs the prompt regex against each pane line, returning
+// matches with their line numbers for contiguity analysis.
+func matchPromptLines(re *regexp.Regexp, paneContent string) []promptMatch {
+ paneLines := strings.Split(paneContent, "\n")
+ var matches []promptMatch
+ for i, line := range paneLines {
+ m := re.FindStringSubmatch(line)
+ if len(m) >= 2 {
+ matches = append(matches, promptMatch{lineNum: i, text: m[1]})
+ }
+ }
+ return matches
+}
+
+// joinLastContiguousBlock takes the last group of matches on consecutive line
+// numbers, strips noise from each, and joins the non-empty results with
+// newlines. This ensures that only the bottom-most box (the input prompt)
+// is captured when multiple box-drawing sections exist in the pane.
+func joinLastContiguousBlock(matches []promptMatch, strips []string) string {
+ last := len(matches) - 1
+ start := last
+ for start > 0 && matches[start].lineNum-matches[start-1].lineNum == 1 {
+ start--
+ }
+ var lines []string
+ for i := start; i <= last; i++ {
+ line := stripNoise(matches[i].text, strips)
+ if line != "" {
+ lines = append(lines, line)
+ }
+ }
+ return strings.Join(lines, "\n")
}
// stripNoise removes each of the agent's StripPatterns from text and trims