summaryrefslogtreecommitdiff
path: root/internal/tmuxedit/claude_agent.go
blob: b84c77e57dcb5783e70291d3571a6868c1c3b10f (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
package tmuxedit

import (
	"regexp"
	"strings"
)

// claudeAgent handles Claude Code's ❯ prompt between ──── horizontal rules.
// Claude Code runs in actual vim mode, so clearing uses vim commands.
// Wrapped text appears as indented continuation lines without ❯.
type claudeAgent struct{ baseAgent }

// newClaudeAgent returns a claudeAgent with the default configuration.
// SectionPattern scopes extraction to the last ─── delimited area, avoiding
// false positives from ❯ in previous messages.
func newClaudeAgent() *claudeAgent {
	return &claudeAgent{baseAgent{
		name:          "claude",
		displayName:   "Claude Code",
		detectPattern: `(❯|(?i)claude code|(?i)anthropic)`,
		sectionPat:    `^─{5,}`,
		promptPat:     `(?m)❯\s*(.+)$`,
		clearFirst:    true,
		clearKeys:     "C-a C-k",
		newlineKeys:   "S-Enter",
		submitKeys:    "Enter",
	}}
}

// ExtractPrompt extracts the prompt text from the last section between ─────
// rules. Within the scoped section, all non-empty lines are collected:
// ❯-prefixed lines have the prefix stripped, and indented continuation lines
// (wrapped text without ❯) are included as-is after trimming.
func (c *claudeAgent) ExtractPrompt(paneContent string) string {
	if c.promptPat == "" {
		return ""
	}
	re, err := regexp.Compile(c.promptPat)
	if err != nil {
		return ""
	}
	// Scope to the last section between ───── delimiters
	content := scopeToLastSection(paneContent, c.sectionPat)
	// Collect ❯-prefixed lines and their continuation lines (indented
	// wrapped text without ❯). Only include non-❯ lines that directly
	// follow a ❯-matched line to avoid picking up unrelated content.
	paneLines := strings.Split(content, "\n")
	var lines []string
	inPrompt := false
	for _, line := range paneLines {
		m := re.FindStringSubmatch(line)
		if len(m) >= 2 {
			// ❯-prefixed line: use the captured text
			cleaned := stripNoise(m[1], c.stripPatterns)
			if cleaned != "" {
				lines = append(lines, cleaned)
			}
			inPrompt = true
		} else if inPrompt {
			// Non-❯ line after a prompt: include indented continuation text
			trimmed := strings.TrimSpace(line)
			if trimmed != "" {
				lines = append(lines, trimmed)
			} else {
				// Empty line breaks the continuation
				inPrompt = false
			}
		}
	}
	return strings.Join(lines, "\n")
}

// ClearInput sends vim commands to clear Claude Code's input:
// Escape to ensure normal mode, gg to go to top, C-v G d to visual-block
// select all and delete, then i to re-enter insert mode.
func (c *claudeAgent) ClearInput(paneID string) error {
	if !c.clearFirst || c.clearKeys == "" {
		return nil
	}
	if err := sendClearSequence(paneID, c.clearKeys); err != nil {
		return err
	}
	sleepAfterClear()
	return nil
}