// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later // Package prompt recognizes fresh prompts and work-completion lines. package prompt import ( "fmt" "regexp" "strings" "git.stormux.org/storm/fenrir/internal/screen" ) var defaultPatterns = []*regexp.Regexp{ regexp.MustCompile(`^(?:[$#>]|\[.*\]\s*[\\$#>]|[a-zA-Z0-9._-]+[\\$#>]|\S+@\S+.*[$#>])\s*$`), regexp.MustCompile(`(?i)\?\s*(?:\[(?:y/n|yes/no)\]|\((?:y/n|yes/no)\))\s*:?\s*$`), regexp.MustCompile(`(?i)^.*\bok\s*\[y/n\]\s*:?\s*$`), regexp.MustCompile(`(?i)^(?:\[sudo\]\s*password\s+for\s+.+|.*\bpassword|(?:please\s+)?enter\s+passphrase.*)\s*:\s*$`), regexp.MustCompile(`(?i)^(?:press\s+any\s+key\s+to\s+continue.*|are\s+you\s+sure\?.*|please\s+confirm.*)$`), } var completionPattern = regexp.MustCompile(`^Worked for (?:[0-9]+h(?: [0-9]+m)?(?: [0-9]+s)?|[0-9]+m(?: [0-9]+s)?|[0-9]+s)\s+•\s+(?:[1-9]|1[0-2]):[0-5][0-9]\s+(?:AM|PM)$`) type Detector struct { exactMatches []string patterns []*regexp.Regexp previous map[string]int terminal int generation uint64 rows, cols int active bool } func New(exactMatches, customPatterns []string) (*Detector, error) { detector := &Detector{exactMatches: append([]string(nil), exactMatches...)} for _, pattern := range customPatterns { compiled, err := regexp.Compile(pattern) if err != nil { return nil, fmt.Errorf("invalid prompt custom pattern %q: %w", pattern, err) } detector.patterns = append(detector.patterns, compiled) } return detector, nil } func (d *Detector) matches_prompt(text string) bool { if text == "" { return false } for _, exact := range d.exactMatches { if text == strings.TrimSpace(exact) { return true } } for _, patterns := range [][]*regexp.Regexp{d.patterns, defaultPatterns} { for _, pattern := range patterns { if pattern.MatchString(text) { return true } } } return false } // Observe runs even when speech is enabled, establishing a baseline before // silence. Counts ignore row movement, so old prompts/completions scrolling // through the screen cannot restore speech. Session/buffer/size changes only // establish a new baseline; they never restore from another screen's history. // Shell/interactive prompts must be on the cursor row. Completion lines can // appear above an input area, as in a terminal coding assistant. func (d *Detector) Observe(next screen.Snapshot) string { counts := make(map[string]int) for row := 0; row < next.Rows; row++ { counts[strings.TrimSpace(next.Text(row))]++ } stable := d.previous != nil && d.active && next.Active && d.terminal == next.Terminal && d.generation == next.Generation && d.rows == next.Rows && d.cols == next.Cols var result string if stable { for row := 0; row < next.Rows; row++ { text := strings.TrimSpace(next.Text(row)) if counts[text] > d.previous[text] && (completionPattern.MatchString(text) || row == next.Y && d.matches_prompt(text)) { result = text break } } } d.previous = counts d.terminal, d.generation, d.rows, d.cols, d.active = next.Terminal, next.Generation, next.Rows, next.Cols, next.Active return result }