diff options
Diffstat (limited to 'docs/coverage.html')
| -rw-r--r-- | docs/coverage.html | 237 |
1 files changed, 121 insertions, 116 deletions
diff --git a/docs/coverage.html b/docs/coverage.html index 70dbf00..1694242 100644 --- a/docs/coverage.html +++ b/docs/coverage.html @@ -149,7 +149,7 @@ <option value="file46">codeberg.org/snonux/hexai/internal/tmuxedit/agent.go (82.1%)</option> - <option value="file47">codeberg.org/snonux/hexai/internal/tmuxedit/agentutil.go (94.2%)</option> + <option value="file47">codeberg.org/snonux/hexai/internal/tmuxedit/agentutil.go (47.1%)</option> <option value="file48">codeberg.org/snonux/hexai/internal/tmuxedit/capture.go (100.0%)</option> @@ -9534,9 +9534,9 @@ import ( "golang.org/x/sys/unix" ) -func tryLockFile(fd uintptr) error <span class="cov10" title="582">{ - if err := unix.Flock(int(fd), unix.LOCK_EX|unix.LOCK_NB); err != nil </span><span class="cov9" title="448">{ - if errors.Is(err, unix.EWOULDBLOCK) </span><span class="cov9" title="448">{ +func tryLockFile(fd uintptr) error <span class="cov10" title="559">{ + if err := unix.Flock(int(fd), unix.LOCK_EX|unix.LOCK_NB); err != nil </span><span class="cov9" title="425">{ + if errors.Is(err, unix.EWOULDBLOCK) </span><span class="cov9" title="425">{ return errLockWouldBlock }</span> <span class="cov0" title="0">return err</span> @@ -9651,7 +9651,7 @@ func Update(ctx context.Context, provider, model string, sentBytes, recvBytes in if b, rerr := os.ReadFile(path); rerr == nil </span><span class="cov5" title="125">{ _ = json.Unmarshal(b, &sf) }</span> - <span class="cov5" title="134">if sf.Version != fileVersion </span><span class="cov3" title="9">{ + <span class="cov5" title="134">if sf.Version != fileVersion </span><span class="cov2" title="9">{ sf = File{Version: fileVersion} }</span> <span class="cov5" title="134">now := time.Now() @@ -9664,12 +9664,12 @@ func Update(ctx context.Context, provider, model string, sentBytes, recvBytes in if len(sf.Events) > 0 </span><span class="cov5" title="134">{ // Find first >= cutoff i := 0 - for ; i < len(sf.Events); i++ </span><span class="cov6" title="267">{ + for ; i < len(sf.Events); i++ </span><span class="cov5" title="137">{ if !sf.Events[i].TS.Before(cutoff) </span><span class="cov5" title="134">{ break</span> } } - <span class="cov5" title="134">if i > 0 </span><span class="cov2" title="4">{ + <span class="cov5" title="134">if i > 0 </span><span class="cov1" title="3">{ sf.Events = append([]Event(nil), sf.Events[i:]...) }</span> } @@ -9704,30 +9704,30 @@ func Update(ctx context.Context, provider, model string, sentBytes, recvBytes in func acquireFileLock(ctx context.Context, f *os.File) (func() error, error) <span class="cov5" title="134">{ fd := f.Fd() - for </span><span class="cov6" title="582">{ + for </span><span class="cov6" title="559">{ err := tryLockFile(fd) if err == nil </span><span class="cov5" title="134">{ return func() error </span><span class="cov5" title="134">{ return unlockFile(fd) }</span>, nil } - <span class="cov6" title="448">if errors.Is(err, errLockWouldBlock) </span><span class="cov6" title="448">{ + <span class="cov6" title="425">if errors.Is(err, errLockWouldBlock) </span><span class="cov6" title="425">{ select </span>{ case <-ctx.Done():<span class="cov0" title="0"> return nil, ctx.Err()</span> - case <-time.After(5 * time.Millisecond):<span class="cov6" title="448"></span> + case <-time.After(5 * time.Millisecond):<span class="cov6" title="425"></span> } - <span class="cov6" title="448">continue</span> + <span class="cov6" title="425">continue</span> } <span class="cov0" title="0">return nil, err</span> } } // Snapshot reads and aggregates events within the configured window. -func TakeSnapshot() (Snapshot, error) <span class="cov5" title="76">{ +func TakeSnapshot() (Snapshot, error) <span class="cov4" title="76">{ dir, err := CacheDir() if err != nil </span><span class="cov0" title="0">{ return Snapshot{}, err }</span> - <span class="cov5" title="76">path := filepath.Join(dir, fileName) + <span class="cov4" title="76">path := filepath.Join(dir, fileName) b, err := os.ReadFile(path) if err != nil </span><span class="cov0" title="0">{ if errors.Is(err, os.ErrNotExist) </span><span class="cov0" title="0">{ @@ -9735,30 +9735,30 @@ func TakeSnapshot() (Snapshot, error) <span class="cov5" title="76">{ }</span> <span class="cov0" title="0">return Snapshot{}, err</span> } - <span class="cov5" title="76">var sf File + <span class="cov4" title="76">var sf File if err := json.Unmarshal(b, &sf); err != nil </span><span class="cov0" title="0">{ return Snapshot{}, err }</span> - <span class="cov5" title="76">win := time.Duration(sf.WindowSeconds) * time.Second + <span class="cov4" title="76">win := time.Duration(sf.WindowSeconds) * time.Second if win <= 0 </span><span class="cov0" title="0">{ win = Window() - }</span> else<span class="cov5" title="76"> { + }</span> else<span class="cov4" title="76"> { SetWindow(win) // align process with file window if changed elsewhere }</span> - <span class="cov5" title="76">cutoff := time.Now().Add(-win) + <span class="cov4" title="76">cutoff := time.Now().Add(-win) snap := Snapshot{Providers: make(map[string]ProviderEntry), Window: win} - for _, ev := range sf.Events </span><span class="cov10" title="15367">{ + for _, ev := range sf.Events </span><span class="cov10" title="19923">{ if ev.TS.Before(cutoff) </span><span class="cov0" title="0">{ continue</span> } - <span class="cov10" title="15367">snap.Global.Reqs++ + <span class="cov10" title="19923">snap.Global.Reqs++ snap.Global.Sent += ev.Sent snap.Global.Recv += ev.Recv pe := snap.Providers[ev.Provider] if pe.Models == nil </span><span class="cov6" title="478">{ pe.Models = make(map[string]Counters) }</span> - <span class="cov10" title="15367">pe.Totals.Reqs++ + <span class="cov10" title="19923">pe.Totals.Reqs++ pe.Totals.Sent += ev.Sent pe.Totals.Recv += ev.Recv mc := pe.Models[ev.Model] @@ -9768,17 +9768,17 @@ func TakeSnapshot() (Snapshot, error) <span class="cov5" title="76">{ pe.Models[ev.Model] = mc snap.Providers[ev.Provider] = pe</span> } - <span class="cov5" title="76">mins := win.Minutes() + <span class="cov4" title="76">mins := win.Minutes() if mins <= 0 </span><span class="cov0" title="0">{ mins = 0.001 }</span> - <span class="cov5" title="76">snap.RPM = float64(snap.Global.Reqs) / mins + <span class="cov4" title="76">snap.RPM = float64(snap.Global.Reqs) / mins return snap, nil</span> } // CacheDir resolves the cache directory for stats. -func CacheDir() (string, error) <span class="cov6" title="213">{ - if x := os.Getenv("XDG_CACHE_HOME"); stringsTrim(x) != "" </span><span class="cov5" title="81">{ +func CacheDir() (string, error) <span class="cov5" title="213">{ + if x := os.Getenv("XDG_CACHE_HOME"); stringsTrim(x) != "" </span><span class="cov4" title="81">{ return filepath.Join(x, "hexai"), nil }</span> <span class="cov5" title="132">home, err := os.UserHomeDir() @@ -9789,23 +9789,23 @@ func CacheDir() (string, error) <span class="cov6" title="213">{ } // stringsTrim is a tiny helper to avoid importing strings everywhere here. -func stringsTrim(s string) string <span class="cov6" title="213">{ +func stringsTrim(s string) string <span class="cov5" title="213">{ i := 0 j := len(s) for i < j && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r') </span><span class="cov0" title="0">{ i++ }</span> - <span class="cov6" title="213">for j > i && (s[j-1] == ' ' || s[j-1] == '\t' || s[j-1] == '\n' || s[j-1] == '\r') </span><span class="cov0" title="0">{ + <span class="cov5" title="213">for j > i && (s[j-1] == ' ' || s[j-1] == '\t' || s[j-1] == '\n' || s[j-1] == '\r') </span><span class="cov0" title="0">{ j-- }</span> - <span class="cov6" title="213">if i == 0 && j == len(s) </span><span class="cov6" title="213">{ + <span class="cov5" title="213">if i == 0 && j == len(s) </span><span class="cov5" title="213">{ return s }</span> <span class="cov0" title="0">return s[i:j]</span> } // DebugString returns a compact single-line view of a snapshot (useful for logs). -func (s Snapshot) DebugString() string <span class="cov2" title="3">{ +func (s Snapshot) DebugString() string <span class="cov1" title="3">{ return "Σ reqs=" + strconv.FormatInt(s.Global.Reqs, 10) + " rpm=" + fmt.Sprintf("%.2f", s.RPM) }</span> </pre> @@ -10471,6 +10471,7 @@ import ( "regexp" "strconv" "strings" + "time" ) // promptMatch holds a regex match result with its line number in the pane. @@ -10481,117 +10482,121 @@ type promptMatch struct { // 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 <span class="cov6" title="14">{ +func matchPromptLines(re *regexp.Regexp, paneContent string) []promptMatch { paneLines := strings.Split(paneContent, "\n") var matches []promptMatch - for i, line := range paneLines </span><span class="cov9" title="42">{ - m := re.FindStringSubmatch(line) - if len(m) >= 2 </span><span class="cov7" title="22">{ - matches = append(matches, promptMatch{lineNum: i, text: m[1]}) +</span> for i, line := range paneLines </span>{ + m := re.FindStr</span>ingSubmatch(line) + if len(m) >= 2 </span>{ + </span>matches = append(matches, promptMatch{lineNum: i, text: m[1]}) }</span> - } - <span class="cov6" title="14">return matches</span> + <span class="cov0" title="0">} +</span> <span class="cov6" title="14">return matches</span> } // 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 <span class="cov1" title="1">{ +// with newlines. Used when SectionPattern has already scoped to th<span class="cov0" title="0">e prompt area. +func joinAllMatches(matches []promptMatch, strips []string) string { var lines []string - for _, m := range matches </span><span class="cov3" title="3">{ - line := stripNoise(m.text, strips) - if line != "" </span><span class="cov2" title="2">{ - lines = append(lines, line) +</span> for _, m := range matches </span>{ + line := stripN</span>oise(m.text, strips) + if line != "" </span>{ + </span>lines = append(lines, line) }</span> - } - <span class="cov1" title="1">return strings.Join(lines, "\n")</span> + <span class="cov0" title="0">} +</span> <span class="cov1" title="1">return strings.Join(lines, "\n")</span> } // 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 <span class="cov6" title="12">{ +func joinLastContiguousBlock(matches []promptMatch, strips []string) string { last := len(matches) - 1 start := last - for start > 0 && matches[start].lineNum-matches[start-1].lineNum == 1 </span><span class="cov5" title="7">{ - start-- +</span> for start > 0 && matches[start].lineNum-matches[start-1].lineNum == 1 </span>{ + </span>start-- }</span> - <span class="cov6" title="12">var lines []string - for i := start; i <= last; i++ </span><span class="cov7" title="19">{ - line := stripNoise(matches[i].text, strips) - if line != "" </span><span class="cov7" title="18">{ - lines = append(lines, line) + var lines []string +</span> for i := start; i <= last; i++ </span>{ + line := stripN</span>oise(matches[i].text, strips) + if line != "" </span>{ + </span>lines = append(lines, line) }</span> - } - <span class="cov6" title="12">return strings.Join(lines, "\n")</span> + <span class="cov0" title="0">} +</span> <span class="cov6" title="12">return strings.Join(lines, "\n")</span> } // 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 <span class="cov7" title="16">{ - if sectionPattern == "" </span><span class="cov3" title="3">{ - return paneContent +func scopeToLastSection(p</span><span class="cov0" title="0">aneContent, sectionPattern string) string { + if sectionPattern == "" { + </span></span>return paneContent }</span> - <span class="cov6" title="13">re, err := regexp.Compile(sectionPattern) - if err != nil </span><span class="cov1" title="1">{ - return paneContent + re, err := reg</span>exp.Compile(sectionPattern) + if err != nil </span>{ + </span>return paneContent }</span> - <span class="cov6" title="12">lines := strings.Split(paneContent, "\n") + lines := strings.Split(paneContent, "\n") var delimLines []int - for i, line := range lines </span><span class="cov10" title="58">{ - if re.MatchString(line) </span><span class="cov8" title="24">{ - delimLines = append(delimLines, i) +</span> for i, line := range lines </span>{ +</span> if re.MatchString(line) { + </span></span>delimLines = append(delimLines, i) }</span> - } - <span class="cov6" title="12">if len(delimLines) < 2 </span><span class="cov4" title="4">{ - return paneContent + <span class="cov0" title="0">} +</span> if len(delimLines) < 2 { + </span></span>return paneContent }</span> - <span class="cov5" title="8">start := delimLines[len(delimLines)-2] + 1 - end := delimLines[len(delimLines)-1] - if start >= end </span><span class="cov0" title="0">{ - return paneContent + start := delimLines[len(delimLines)-2] + 1 + end := delimLine</span>s[len(delimLines)-1] + if start >= end </span>{ + </span>return paneContent }</span> - <span class="cov5" title="8">return strings.Join(lines[start:end], "\n")</span> +</span> <span class="cov5" title="8">return strings.Join(lines[start:end], "\n")</span> } // stripNoise removes each of the agent's StripPatterns from text and trims // whitespace. -func stripNoise(text string, patterns []string) string <span class="cov8" title="35">{ - for _, p := range patterns </span><span class="cov9" title="44">{ - text = strings.ReplaceAll(text, p, "") +func stripNoise(text string,</span><span class="cov0" title="0"> patterns []string) string { + for _, p := range patterns { + </span></span>text = strings.ReplaceAll(text, p, "") }</span> - <span class="cov8" title="35">return strings.TrimSpace(text)</span> +</span> <span class="cov8" title="35">return strings.TrimSpace(text)</span> } // 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 <span class="cov4" title="4">{ - for _, token := range strings.Fields(clearKeys) </span><span class="cov7" title="16">{ - key, count := parseKeyRepeat(token) - if count > 1 </span><span class="cov2" title="2">{ - if err := sendRepeatedKey(paneID, key, count); err != nil </span><span class="cov0" title="0">{ - return fmt.Errorf("clear key %q*%d failed: %w", key, count, err) +// sent N times using tmux send-keys -N for efficient b<span class="cov0" title="0">ulk repeats. +func sendClearSequence(paneID, clearKeys string) </span><span class="cov0" title="0">error { + for _, token := range strings.Fields(clearKeys) { + key, count :=</span></span> parseKeyRepeat(token) + if count > 1 </span>{ +</span> if err := sendRepeatedKey(paneID, key, count); err != nil </span>{ + </span>return fmt.Errorf("clear key %q*%d failed: %w", key, count, err) }</span> - } else<span class="cov6" title="14"> { - if err := sendKeys(paneID, key); err != nil </span><span class="cov0" title="0">{ - return fmt.Errorf("clear key %q failed: %w", key, err) + } else { +</span> if err := sendKeys(paneID, key); err != nil </span>{ + </span>return fmt.Errorf("clear key %q failed: %w", key, err) }</span> } + <span class="cov0" title="0"> // Add de</span>lay after Escape to let Vim/Claude exit INSERT mode + <span class="cov5" title="8">if key == "Escape" </span><span class="cov0" title="0">{ + time.Sleep(150 * time.Millisecond) + }</span> } <span class="cov4" title="4">return nil</span> } -// 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) <span class="cov8" title="24">{ - idx := strings.LastIndex(token, "*") - if idx < 1 || idx >= len(token)-1 </span><span class="cov7" title="17">{ - return token, 1 +</span>// parseKeyRepeat splits "Key*N" into (Key, N). Returns (token, 1) if no +//</span> repeat suffix is present or the suffix is invalid. +f<span class="cov0" title="0">unc parseKeyRepeat(token string) (string, int) { + idx := strings.LastInde</span>x(token, "*") + if idx < 1 || idx >= len(token)-1 </span>{ + </span>return token, 1 }</span> - <span class="cov5" title="7">n, err := strconv.Atoi(token[idx+1:]) +</span> <span class="cov5" title="7">n, err := strconv.Atoi(token[idx+1:]) if err != nil || n < 1 </span><span class="cov2" title="2">{ return token, 1 }</span> @@ -10599,22 +10604,22 @@ func parseKeyRepeat(token string) (string, int) <span class="cov8" title="24">{ } // 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 +// newline key between lines</span><span class="cov0" title="0">. If newlineKeys is empty, "Enter" is used as +// fallback. This is the shared text-sending lo</span><span class="cov0" title="0">gic used by agent SendText // implementations. -func sendLines(paneID, text, newlineKeys string) error <span class="cov5" title="7">{ +fun</span>c sendLines(paneID, text, newlineKeys string) error <span class="cov5" title="7">{ lines := strings.Split(text, "\n") - for i, line := range lines </span><span class="cov6" title="11">{ - if err := sendKeys(paneID, line); err != nil </span><span class="cov1" title="1">{ - return fmt.Errorf("send line %d failed: %w", i, err) - }</span> - // Insert inter-line newline (except after the last line) - <span class="cov6" title="10">if i < len(lines)-1 </span><span class="cov4" title="4">{ - nlKey := newlineKeys - if nlKey == "" </span><span class="cov1" title="1">{ - nlKey = "Enter" + for i, line := range lines </span>{ +</span> if err := sendKeys(paneID, line); err != nil { + return fmt.Erro</span></span>rf("send line %d failed: %w", i, err) + }</span> + //</span> Insert inter-line newline (except after the last line) + <span class="cov6" title="10">if i < len(lines)-1 </span>{ +</span> nlKey := newlineKeys + if nlKey == "" </span>{ +</span> nlKey = "Enter" }</span> - <span class="cov4" title="4">if err := sendKeys(paneID, nlKey); err != nil </span><span class="cov0" title="0">{ + <span class="cov0" title="0"> if err :</span>= sendKeys(paneID, nlKey); err != nil </span><span class="cov0" title="0">{ return fmt.Errorf("newline after line %d failed: %w", i, err) }</span> } @@ -10665,7 +10670,7 @@ func newClaudeAgent() *claudeAgent <span class="cov10" title="18">{ sectionPat: `^─{5,}`, promptPat: `(?m)❯\s*(.+)$`, clearFirst: true, - clearKeys: "Escape gg C-v G d i", + clearKeys: "C-a C-k", newlineKeys: "S-Enter", submitKeys: "Enter", }} @@ -10750,16 +10755,16 @@ func builtinAgents() []Agent <span class="cov10" title="15">{ return []Agent{ newCursorAgent(), newClaudeAgent(), - &configAgent{baseAgent{ - name: "amp", - displayName: "Amp", - detectPattern: `(?i)(amp|sourcegraph)`, - promptPat: `(?m)│\s*(.+?)\s*│\s*$`, - clearFirst: true, - clearKeys: "C-u", - newlineKeys: "S-Enter", - submitKeys: "Enter", - }}, + &configAgent{baseAgent{ + name: "amp", + displayName: "Amp", + detectPattern: `(?i)(amp|sourcegraph)`, + promptPat: `(?m)│\s*(.+?)\s*│\s*$`, + clearFirst: true, + clearKeys: "C-u", + newlineKeys: "S-Enter", + submitKeys: "Enter", + }}, &configAgent{baseAgent{ name: "aider", displayName: "Aider", |
