diff options
Diffstat (limited to 'internal/tmuxedit/agentutil.go')
| -rw-r--r-- | internal/tmuxedit/agentutil.go | 160 |
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 +} |
