summaryrefslogtreecommitdiff
path: root/internal/tmuxedit/agent.go
blob: 1ae8f133547f921d121b7725960076d7ff6a64a8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// Package tmuxedit implements a tmux popup editor for composing AI agent prompts.
// agent.go defines the Agent interface, the baseAgent struct with default
// implementations, and agent detection/resolution helpers.
package tmuxedit

import (
	"regexp"
	"strings"
)

// Agent defines how to interact with a specific AI agent in a tmux pane.
// Each implementation encapsulates its own detection, extraction, clearing,
// and sending logic since agents differ fundamentally in their UI structure.
type Agent interface {
	Name() string
	DisplayName() string
	Detect(paneContent string) bool
	ExtractPrompt(paneContent string) string
	ClearInput(paneID string) error
	SendText(paneID, text string) error
}

// Configurable provides access to a baseAgent's fields for config merging.
// Agent implementations that embed baseAgent automatically satisfy this.
type Configurable interface {
	Base() *baseAgent
}

// baseAgent holds configurable fields and provides default implementations
// of the Agent interface. Specialized agents (e.g. cursor) embed baseAgent
// and override methods where behavior differs from the defaults.
type baseAgent struct {
	name          string
	displayName   string
	detectPattern string
	sectionPat    string   // optional regex to delimit the prompt area
	promptPat     string   // regex with capture group (1) for prompt text
	stripPatterns []string // substrings removed from extracted text
	clearFirst    bool     // whether to clear existing input before sending
	clearKeys     string   // tmux key sequence to clear input
	newlineKeys   string   // tmux key to insert a newline
	submitKeys    string   // tmux key to submit the prompt
}

// Base returns a pointer to the baseAgent for config merging.
func (b *baseAgent) Base() *baseAgent { return b }

// Name returns the agent's short identifier (e.g. "cursor", "amp").
func (b *baseAgent) Name() string { return b.name }

// DisplayName returns the agent's human-readable name.
func (b *baseAgent) DisplayName() string { return b.displayName }

// Detect checks whether the pane content matches this agent's detection
// pattern. Returns false if no pattern is set or the regex is invalid.
func (b *baseAgent) Detect(paneContent string) bool {
	if b.detectPattern == "" {
		return false
	}
	re, err := regexp.Compile(b.detectPattern)
	if err != nil {
		return false
	}
	return re.MatchString(paneContent)
}

// ExtractPrompt uses the agent's prompt pattern to extract the current prompt
// text from pane content. If sectionPat is set, extraction is scoped to the
// last section between two delimiter lines and all matches are joined.
// Without sectionPat, the last contiguous group of matched lines is used.
// Returns empty string if no pattern or no match.
func (b *baseAgent) ExtractPrompt(paneContent string) string {
	if b.promptPat == "" {
		return ""
	}
	re, err := regexp.Compile(b.promptPat)
	if err != nil {
		return ""
	}
	scoped := b.sectionPat != ""
	content := scopeToLastSection(paneContent, b.sectionPat)
	allMatches := matchPromptLines(re, content)
	if len(allMatches) == 0 {
		return ""
	}
	if scoped {
		return joinAllMatches(allMatches, b.stripPatterns)
	}
	return joinLastContiguousBlock(allMatches, b.stripPatterns)
}

// ClearInput clears existing input in the pane using the configured key
// sequence. Skipped if clearFirst is false or clearKeys is empty.
func (b *baseAgent) ClearInput(paneID string) error {
	if !b.clearFirst || b.clearKeys == "" {
		return nil
	}
	if err := sendClearSequence(paneID, b.clearKeys); err != nil {
		return err
	}
	sleepAfterClear()
	return nil
}

// SendText sends the given text to the target pane line-by-line, using the
// agent's newline key between lines.
func (b *baseAgent) SendText(paneID, text string) error {
	if strings.TrimSpace(text) == "" {
		return nil
	}
	return sendLines(paneID, text, b.newlineKeys)
}

// detectAgent tries each agent's Detect method against pane content.
// First match wins. Returns genericAgent() if no agent matches.
func detectAgent(paneContent string, agents []Agent) Agent {
	for _, a := range agents {
		if a.Detect(paneContent) {
			return a
		}
	}
	return genericAgent()
}

// findAgentByName returns the agent with the given name (case-insensitive),
// falling back to genericAgent() if not found.
func findAgentByName(name string, agents []Agent) Agent {
	for _, a := range agents {
		if strings.EqualFold(a.Name(), name) {
			return a
		}
	}
	return genericAgent()
}