summaryrefslogtreecommitdiff
path: root/internal/tmuxedit/run.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-08 15:19:36 +0200
committerPaul Buetow <paul@buetow.org>2026-02-08 15:19:36 +0200
commit887d7bc186db90c3903851b0f1db2d24df5d7a7b (patch)
tree1cfb8055ddbb907bae9461b924dda1d2b3f15b46 /internal/tmuxedit/run.go
parent6da37034708dc7d4dcb7c71e890478a68e6ae4a1 (diff)
fix hexai-tmux-edit agent detection, multi-line extraction, and clearing
- Reorder agents: cursor first to avoid false positive from "Claude 4.5 Sonnet" model name appearing in cursor panes - Change cursor detect pattern to box-drawing UI elements instead of name-based matching - Change claude detect pattern to ❯ prompt instead of generic "claude" - Support multi-line prompt extraction using last-contiguous-block algorithm to ignore command-review and dialog boxes - Fix deduplicateText to always return full edited text (clear + resend) instead of stripping original prefix which caused double-removal - Replace vim-style clear (Escape gg dG i) with universal End+BSpace*200 since cursor's prompt is not actually vim - Add Key*N repeat syntax in ClearKeys (parsed by parseKeyRepeat, sent via tmux send-keys -N) - Add 300ms sleep after clear to let TUI drain queued backspaces before new text arrives - Add debug logging to /tmp/hexai-tmux-edit.log - Add strip pattern for "ctrl+c to stop" noise in cursor prompt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/tmuxedit/run.go')
-rw-r--r--internal/tmuxedit/run.go66
1 files changed, 56 insertions, 10 deletions
diff --git a/internal/tmuxedit/run.go b/internal/tmuxedit/run.go
index 173e936..dde91fa 100644
--- a/internal/tmuxedit/run.go
+++ b/internal/tmuxedit/run.go
@@ -4,6 +4,7 @@ import (
"fmt"
"log"
"os"
+ "os/exec"
"strings"
"codeberg.org/snonux/hexai/internal/appconfig"
@@ -56,9 +57,10 @@ var openEditorPopup = func(initial, popupW, popupH string) (string, error) {
return strings.TrimSpace(string(b)), nil
}
-// launchPopup runs `tmux display-popup` with the editor command.
-// The -E flag makes the popup close when the editor exits.
-func launchPopup(ed, path, width, height string) error {
+// launchPopup is the seam for running `tmux display-popup` with the editor.
+// The -E flag makes the popup close when the editor exits. Uses .Run()
+// (not .Output()) so the popup blocks until the user closes the editor.
+var launchPopup = func(ed, path, width, height string) error {
args := []string{"display-popup", "-E"}
if width != "" {
args = append(args, "-w", width)
@@ -67,8 +69,7 @@ func launchPopup(ed, path, width, height string) error {
args = append(args, "-h", height)
}
args = append(args, ed+" "+shellQuote(path))
- _, err := runCommand("tmux", args...)
- return err
+ return exec.Command("tmux", args...).Run()
}
// shellQuote wraps a path in single quotes for safe shell use.
@@ -99,23 +100,58 @@ func loadConfig(configPath string) appconfig.App {
return appconfig.LoadWithOptions(logger, lopts)
}
+// debugLog is the debug logger. Set to a real logger via initDebugLog().
+var debugLog *log.Logger
+
+// initDebugLog creates a debug log file at /tmp/hexai-tmux-edit.log.
+func initDebugLog() {
+ f, err := os.OpenFile("/tmp/hexai-tmux-edit.log", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
+ if err != nil {
+ return
+ }
+ debugLog = log.New(f, "", log.LstdFlags|log.Lmicroseconds)
+}
+
+func dbg(format string, args ...any) {
+ if debugLog != nil {
+ debugLog.Printf(format, args...)
+ }
+}
+
// runWithConfig executes the edit workflow using the provided config.
func runWithConfig(opts Options, cfg appconfig.App) error {
+ initDebugLog()
+ dbg("=== hexai-tmux-edit start ===")
+ dbg("opts: pane=%q agent=%q config=%q", opts.Pane, opts.Agent, opts.ConfigPath)
+
paneID, err := resolveTargetPane(opts.Pane)
if err != nil {
+ dbg("resolveTargetPane error: %v", err)
return err
}
+ dbg("resolved pane: %q", paneID)
+
content, err := capturePane(paneID)
if err != nil {
+ dbg("capturePane error: %v", err)
return err
}
+ dbg("captured %d bytes from pane", len(content))
+ // Log a few lines around the prompt
+ for i, line := range strings.Split(content, "\n") {
+ if strings.Contains(line, "│") || strings.Contains(line, "→") {
+ dbg(" pane line %d: %q", i, line)
+ }
+ }
+
agents := resolveAgents(cfg.TmuxEditAgents)
agent := pickAgent(opts.Agent, content, agents)
+ dbg("agent: name=%q detect=%q prompt=%q strip=%v clear=%v clearKeys=%q",
+ agent.Name, agent.DetectPattern, agent.PromptPattern, agent.StripPatterns, agent.ClearFirst, agent.ClearKeys)
- // Extract current prompt text from pane content
original := extractPrompt(content, agent)
+ dbg("extractPrompt result: %q", original)
- // Determine popup dimensions from config (with defaults)
popupW := cfg.TmuxEditPopupWidth
if popupW == "" {
popupW = "80%"
@@ -124,19 +160,29 @@ func runWithConfig(opts Options, cfg appconfig.App) error {
if popupH == "" {
popupH = "80%"
}
+ dbg("opening editor popup: w=%s h=%s initial=%q", popupW, popupH, original)
edited, err := openEditorPopup(original, popupW, popupH)
if err != nil {
+ dbg("openEditorPopup error: %v", err)
return err
}
+ dbg("editor returned: %q", edited)
- // Deduplicate: remove the pre-filled text if the user didn't change it
text := deduplicateText(original, edited)
+ dbg("deduplicateText result: %q", text)
if text == "" {
- return nil // nothing to send
+ dbg("nothing to send, exiting")
+ return nil
}
- return sendTextToPane(paneID, text, agent)
+ dbg("sending to pane %q: %q", paneID, text)
+ err = sendTextToPane(paneID, text, agent)
+ if err != nil {
+ dbg("sendTextToPane error: %v", err)
+ }
+ dbg("=== done ===")
+ return err
}
// pickAgent selects an agent by explicit name or auto-detection.