summaryrefslogtreecommitdiff
path: root/internal/tmuxedit/agentutil.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/tmuxedit/agentutil.go')
-rw-r--r--internal/tmuxedit/agentutil.go160
1 files changed, 160 insertions, 0 deletions
diff --git a/internal/tmuxedit/agentutil.go b/internal/tmuxedit/agentutil.go
new file mode 100644
index 0000000..924a4a8
--- /dev/null
+++ b/internal/tmuxedit/agentutil.go
@@ -0,0 +1,160 @@
+// Package tmuxedit implements a tmux popup editor for composing AI agent prompts.
+// agentutil.go provides shared helpers for prompt extraction and tmux key sending
+// used by individual agent implementations.
+package tmuxedit
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+// 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
+}
+
+// joinAllMatches strips noise from all matches and joins the non-empty results
+// with newlines. Used when SectionPattern has already scoped to the prompt area.
+func joinAllMatches(matches []promptMatch, strips []string) string {
+ var lines []string
+ for _, m := range matches {
+ line := stripNoise(m.text, strips)
+ if line != "" {
+ lines = append(lines, line)
+ }
+ }
+ return strings.Join(lines, "\n")
+}
+
+// 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")
+}
+
+// scopeToLastSection extracts the content between the last two lines matching
+// the section delimiter pattern. This isolates the prompt area (e.g. Claude's
+// ─── rules) from previous conversation content. Returns the full content if
+// no pattern is set or fewer than two delimiters are found.
+func scopeToLastSection(paneContent, sectionPattern string) string {
+ if sectionPattern == "" {
+ return paneContent
+ }
+ re, err := regexp.Compile(sectionPattern)
+ if err != nil {
+ return paneContent
+ }
+ lines := strings.Split(paneContent, "\n")
+ var delimLines []int
+ for i, line := range lines {
+ if re.MatchString(line) {
+ delimLines = append(delimLines, i)
+ }
+ }
+ if len(delimLines) < 2 {
+ return paneContent
+ }
+ start := delimLines[len(delimLines)-2] + 1
+ end := delimLines[len(delimLines)-1]
+ if start >= end {
+ return paneContent
+ }
+ return strings.Join(lines[start:end], "\n")
+}
+
+// stripNoise removes each of the agent's StripPatterns from text and trims
+// whitespace.
+func stripNoise(text string, patterns []string) string {
+ for _, p := range patterns {
+ text = strings.ReplaceAll(text, p, "")
+ }
+ return strings.TrimSpace(text)
+}
+
+// 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
+}
+
+// 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
+}
+
+// sendLines sends text line-by-line to a tmux pane, inserting the specified
+// newline key between lines. If newlineKeys is empty, "Enter" is used as
+// fallback. This is the shared text-sending logic used by agent SendText
+// implementations.
+func sendLines(paneID, text, newlineKeys string) error {
+ lines := strings.Split(text, "\n")
+ for i, line := range lines {
+ if err := sendKeys(paneID, line); err != nil {
+ return fmt.Errorf("send line %d failed: %w", i, err)
+ }
+ // Insert inter-line newline (except after the last line)
+ if i < len(lines)-1 {
+ nlKey := newlineKeys
+ if nlKey == "" {
+ nlKey = "Enter"
+ }
+ if err := sendKeys(paneID, nlKey); err != nil {
+ return fmt.Errorf("newline after line %d failed: %w", i, err)
+ }
+ }
+ }
+ return nil
+}