diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-02 09:38:04 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-02 09:38:04 +0300 |
| commit | a68228bfa12f4d8a51fe53e244fcd2e66c1ef692 (patch) | |
| tree | 94e257b21b93419fef655c9813f68190204c3d0d /internal | |
| parent | 9f0e96ce62339ddefa8771891e0864ede9af5064 (diff) | |
Remove hexai-tmux-edit popup editor featurev0.42.0
The tmux popup editor and its per-agent detection (Cursor/Amp/Aider)
added maintenance surface without enough use to justify it; Codex and
Claude Code already support external-editor mode natively via Ctrl+G.
Drops internal/tmuxedit, cmd/hexai-tmux-edit, the [tmux_edit] config
schema, the Mage build target, and all related docs/README mentions.
Bump version to 0.42.0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'internal')
28 files changed, 5 insertions, 2980 deletions
diff --git a/internal/appconfig/app_feature_sections.go b/internal/appconfig/app_feature_sections.go index 2513946..da944d3 100644 --- a/internal/appconfig/app_feature_sections.go +++ b/internal/appconfig/app_feature_sections.go @@ -20,13 +20,6 @@ func (a *App) StatsSection() StatsConfig { return a.StatsConfig } -// TmuxEditSection returns a copy of the tmux popup editor settings. -func (a *App) TmuxEditSection() TmuxEditConfig { - c := a.TmuxEditConfig - c.TmuxEditAgents = append([]TmuxEditAgentCfg{}, a.TmuxEditAgents...) - return c -} - // TmuxActionSection returns a copy of the tmux action menu settings. func (a *App) TmuxActionSection() TmuxActionConfig { c := a.TmuxActionConfig diff --git a/internal/appconfig/app_feature_sections_test.go b/internal/appconfig/app_feature_sections_test.go index 1f9ac2d..b217146 100644 --- a/internal/appconfig/app_feature_sections_test.go +++ b/internal/appconfig/app_feature_sections_test.go @@ -33,18 +33,6 @@ func TestStatsSectionReads(t *testing.T) { } } -func TestTmuxEditSectionCopies(t *testing.T) { - cfg := buildFeatureApp() - got := cfg.TmuxEditSection() - if got.TmuxEditDefaultAgent != "codex" || len(got.TmuxEditAgents) != 1 { - t.Fatalf("unexpected tmux edit section: %+v", got) - } - got.TmuxEditAgents[0].Name = "mutated" - if cfg.TmuxEditAgents[0].Name == "mutated" { - t.Fatal("TmuxEditSection did not return a defensive copy") - } -} - func TestTmuxActionSectionCopies(t *testing.T) { cfg := App{} cfg.TmuxActionMenu = []TmuxActionMenuEntry{{Kind: "rewrite"}} diff --git a/internal/appconfig/app_sections.go b/internal/appconfig/app_sections.go index 5919db1..afa7bf0 100644 --- a/internal/appconfig/app_sections.go +++ b/internal/appconfig/app_sections.go @@ -111,7 +111,6 @@ type PromptConfig struct { type FeatureConfig struct { StatsConfig // usage statistics window IgnoreConfig // gitignore-aware file filtering for LSP - TmuxEditConfig // popup editor settings for hexai-tmux-edit TmuxActionConfig // configurable main menu for hexai-tmux-action MCPConfig // Model Context Protocol server settings } @@ -200,7 +199,6 @@ func (a *App) ApplyPromptSection(prompts PromptConfig) { func (a *App) FeatureSection() FeatureConfig { f := a.FeatureConfig f.IgnoreExtraPatterns = slices.Clone(a.IgnoreExtraPatterns) - f.TmuxEditAgents = append([]TmuxEditAgentCfg{}, a.TmuxEditAgents...) f.TmuxActionMenu = append([]TmuxActionMenuEntry{}, a.TmuxActionMenu...) return f } @@ -210,6 +208,5 @@ func (a *App) FeatureSection() FeatureConfig { func (a *App) ApplyFeatureSection(features FeatureConfig) { a.FeatureConfig = features a.IgnoreExtraPatterns = slices.Clone(features.IgnoreExtraPatterns) - a.TmuxEditAgents = append([]TmuxEditAgentCfg{}, features.TmuxEditAgents...) a.TmuxActionMenu = append([]TmuxActionMenuEntry{}, features.TmuxActionMenu...) } diff --git a/internal/appconfig/app_sections_test.go b/internal/appconfig/app_sections_test.go index bcd1cbe..2ff002d 100644 --- a/internal/appconfig/app_sections_test.go +++ b/internal/appconfig/app_sections_test.go @@ -28,7 +28,6 @@ func TestSectionsDefensiveCopies(t *testing.T) { sections.Providers.CLIConfigs[0].Model = "mutated" sections.Prompts.CustomActions[0].Title = "mutated" sections.Features.IgnoreExtraPatterns[0] = "mutated" - sections.Features.TmuxEditAgents[0].Name = "mutated" assertNotEqual(t, cfg.TriggerCharacters[0], "mutated", "trigger characters") assertNotEqual(t, cfg.ChatPrefixes[0], "mutated", "chat prefixes") @@ -36,7 +35,6 @@ func TestSectionsDefensiveCopies(t *testing.T) { assertNotEqual(t, cfg.CLIConfigs[0].Model, "mutated", "cli configs") assertNotEqual(t, cfg.CustomActions[0].Title, "mutated", "custom actions") assertNotEqual(t, cfg.IgnoreExtraPatterns[0], "mutated", "ignore patterns") - assertNotEqual(t, cfg.TmuxEditAgents[0].Name, "mutated", "tmux agents") out := cfg.Sections() out.Core.TriggerCharacters[0] = "mutated" @@ -45,7 +43,6 @@ func TestSectionsDefensiveCopies(t *testing.T) { out.Providers.CLIConfigs[0].Model = "mutated" out.Prompts.CustomActions[0].Title = "mutated" out.Features.IgnoreExtraPatterns[0] = "mutated" - out.Features.TmuxEditAgents[0].Name = "mutated" assertNotEqual(t, cfg.TriggerCharacters[0], "mutated", "sections trigger characters") assertNotEqual(t, cfg.ChatPrefixes[0], "mutated", "sections chat prefixes") @@ -53,7 +50,6 @@ func TestSectionsDefensiveCopies(t *testing.T) { assertNotEqual(t, cfg.CLIConfigs[0].Model, "mutated", "sections cli configs") assertNotEqual(t, cfg.CustomActions[0].Title, "mutated", "sections custom actions") assertNotEqual(t, cfg.IgnoreExtraPatterns[0], "mutated", "sections ignore patterns") - assertNotEqual(t, cfg.TmuxEditAgents[0].Name, "mutated", "sections tmux agents") } func assertNotEqual(t *testing.T, got, want, field string) { @@ -175,23 +171,6 @@ func testFeatureConfig() FeatureConfig { IgnoreExtraPatterns: []string{"vendor/**", "tmp/**"}, IgnoreLSPNotify: sectionBoolPtr(false), }, - TmuxEditConfig: TmuxEditConfig{ - TmuxEditPopupWidth: "80%", - TmuxEditPopupHeight: "75%", - TmuxEditDefaultAgent: "codex", - TmuxEditAgents: []TmuxEditAgentCfg{{ - Name: "codex", - DisplayName: "Codex", - DetectPattern: "(?i)codex", - SectionPattern: "section", - PromptPattern: "prompt", - StripPatterns: []string{"x", "y"}, - ClearFirst: sectionBoolPtr(true), - ClearKeys: "C-u", - NewlineKeys: "S-Enter", - SubmitKeys: "Enter", - }}, - }, MCPConfig: MCPConfig{ MCPPromptsDir: ".hexai/prompts", MCPSlashCommandSync: true, diff --git a/internal/appconfig/config_features_test.go b/internal/appconfig/config_features_test.go index 2b8c769..3d94c77 100644 --- a/internal/appconfig/config_features_test.go +++ b/internal/appconfig/config_features_test.go @@ -1,4 +1,4 @@ -// Tests for ignore config, tmux-edit config, and low-level parsing helpers +// Tests for ignore config and low-level parsing helpers // (temperature, model entries, surface entries, resolved model). package appconfig @@ -119,106 +119,6 @@ gitignore = false } } -func TestTmuxEditConfig_FromFile(t *testing.T) { - clearHexaiEnv(t) - dir := t.TempDir() - cfgPath := filepath.Join(dir, "config.toml") - writeFile(t, cfgPath, ` -[tmux_edit] -popup_width = "90%" -popup_height = "85%" -default_agent = "claude" - -[[tmux_edit.agents]] -name = "claude" -display_name = "Claude Code" -detect_pattern = "(?i)(claude|anthropic)" -prompt_pattern = '(?s)>\s*(.+?)$' -clear_first = true -clear_keys = "C-u" -newline_keys = "S-Enter" -submit_keys = "Enter" - -[[tmux_edit.agents]] -name = "cursor" -display_name = "Cursor" -detect_pattern = "(?i)cursor" -prompt_pattern = '(?s)│\s*(.+?)$' -strip_patterns = ["INSERT", "Add a follow-up"] -clear_first = true -clear_keys = "C-u" -newline_keys = "S-Enter" -submit_keys = "Enter" -`) - cfg := LoadWithOptions(context.Background(), newLogger(), LoadOptions{ConfigPath: cfgPath, ProjectRoot: dir}) - if cfg.TmuxEditPopupWidth != "90%" { - t.Errorf("PopupWidth = %q, want 90%%", cfg.TmuxEditPopupWidth) - } - if cfg.TmuxEditPopupHeight != "85%" { - t.Errorf("PopupHeight = %q, want 85%%", cfg.TmuxEditPopupHeight) - } - if cfg.TmuxEditDefaultAgent != "claude" { - t.Errorf("DefaultAgent = %q, want claude", cfg.TmuxEditDefaultAgent) - } - if len(cfg.TmuxEditAgents) != 2 { - t.Fatalf("got %d agents, want 2", len(cfg.TmuxEditAgents)) - } - a := cfg.TmuxEditAgents[0] - if a.Name != "claude" || a.DisplayName != "Claude Code" { - t.Errorf("agent[0] = %q/%q, want claude/Claude Code", a.Name, a.DisplayName) - } - if a.ClearFirst == nil || !*a.ClearFirst { - t.Error("expected ClearFirst = true for claude agent") - } - b := cfg.TmuxEditAgents[1] - if b.Name != "cursor" { - t.Errorf("agent[1].Name = %q, want cursor", b.Name) - } - if len(b.StripPatterns) != 2 { - t.Errorf("agent[1].StripPatterns = %v, want 2 entries", b.StripPatterns) - } -} - -func TestTmuxEditConfig_Merge(t *testing.T) { - clearHexaiEnv(t) - a := newDefaultConfig() - b := App{ - FeatureConfig: FeatureConfig{TmuxEditConfig: TmuxEditConfig{ - TmuxEditPopupWidth: "70%", - TmuxEditDefaultAgent: "amp", - TmuxEditAgents: []TmuxEditAgentCfg{ - {Name: "amp", DisplayName: "Amp"}, - }, - }}, - } - a.mergeWith(&b) - if a.TmuxEditPopupWidth != "70%" { - t.Errorf("PopupWidth = %q, want 70%%", a.TmuxEditPopupWidth) - } - if a.TmuxEditDefaultAgent != "amp" { - t.Errorf("DefaultAgent = %q, want amp", a.TmuxEditDefaultAgent) - } - if len(a.TmuxEditAgents) != 1 || a.TmuxEditAgents[0].Name != "amp" { - t.Errorf("Agents = %v, want single amp", a.TmuxEditAgents) - } -} - -func TestTmuxEditConfig_SkipsEmptyName(t *testing.T) { - clearHexaiEnv(t) - dir := t.TempDir() - cfgPath := filepath.Join(dir, "config.toml") - writeFile(t, cfgPath, ` -[tmux_edit] -[[tmux_edit.agents]] -name = "" -display_name = "Empty" -`) - cfg := LoadWithOptions(context.Background(), newLogger(), LoadOptions{ConfigPath: cfgPath, ProjectRoot: dir}) - if len(cfg.TmuxEditAgents) != 0 { - t.Errorf("got %d agents, want 0 (empty name should be skipped)", len(cfg.TmuxEditAgents)) - } -} - // --- Config Parsing Tests --- func TestParseTemperatureValue(t *testing.T) { diff --git a/internal/appconfig/config_load.go b/internal/appconfig/config_load.go index ccbf49f..052050b 100644 --- a/internal/appconfig/config_load.go +++ b/internal/appconfig/config_load.go @@ -296,7 +296,6 @@ func applyPromptSections(fc *fileConfig, out *App) { func applyFeatureSections(fc *fileConfig, out *App) { applyTmuxSection(fc, out) applyStatsSection(fc, out) - fc.applyTmuxEdit(out) applyMCPSection(fc, out) applyTmuxActionSection(fc, out) } @@ -564,37 +563,6 @@ func setIfNotBlank(dst *string, value string) { } } -// applyTmuxEdit converts the [tmux_edit] section into App fields. -func (fc *fileConfig) applyTmuxEdit(out *App) { - te := fc.TmuxEdit - if strings.TrimSpace(te.PopupWidth) != "" { - out.TmuxEditPopupWidth = strings.TrimSpace(te.PopupWidth) - } - if strings.TrimSpace(te.PopupHeight) != "" { - out.TmuxEditPopupHeight = strings.TrimSpace(te.PopupHeight) - } - if strings.TrimSpace(te.DefaultAgent) != "" { - out.TmuxEditDefaultAgent = strings.TrimSpace(te.DefaultAgent) - } - for _, a := range te.Agents { - if strings.TrimSpace(a.Name) == "" { - continue - } - out.TmuxEditAgents = append(out.TmuxEditAgents, TmuxEditAgentCfg{ - Name: strings.TrimSpace(a.Name), - DisplayName: strings.TrimSpace(a.DisplayName), - DetectPattern: strings.TrimSpace(a.DetectPattern), - SectionPattern: strings.TrimSpace(a.SectionPattern), - PromptPattern: strings.TrimSpace(a.PromptPattern), - StripPatterns: a.StripPatterns, - ClearFirst: a.ClearFirst, - ClearKeys: strings.TrimSpace(a.ClearKeys), - NewlineKeys: strings.TrimSpace(a.NewlineKeys), - SubmitKeys: strings.TrimSpace(a.SubmitKeys), - }) - } -} - func parseSurfaceModels(raw map[string]any, logger *log.Logger) *App { modelsRaw, ok := raw["models"] if !ok { diff --git a/internal/appconfig/config_merge.go b/internal/appconfig/config_merge.go index f3557c1..e5ad6a5 100644 --- a/internal/appconfig/config_merge.go +++ b/internal/appconfig/config_merge.go @@ -10,7 +10,6 @@ func (a *App) mergeWith(other *App) { a.mergeProviderFields(other) a.mergeSurfaceModels(other) a.mergePrompts(other) - a.mergeTmuxEdit(other) a.mergeTmuxAction(other) } @@ -237,19 +236,3 @@ func (a *App) mergeTmuxAction(other *App) { a.TmuxActionMenu = append([]TmuxActionMenuEntry{}, other.TmuxActionMenu...) } } - -// mergeTmuxEdit copies non-empty tmux edit settings from other. -func (a *App) mergeTmuxEdit(other *App) { - if s := strings.TrimSpace(other.TmuxEditPopupWidth); s != "" { - a.TmuxEditPopupWidth = s - } - if s := strings.TrimSpace(other.TmuxEditPopupHeight); s != "" { - a.TmuxEditPopupHeight = s - } - if s := strings.TrimSpace(other.TmuxEditDefaultAgent); s != "" { - a.TmuxEditDefaultAgent = s - } - if len(other.TmuxEditAgents) > 0 { - a.TmuxEditAgents = append([]TmuxEditAgentCfg{}, other.TmuxEditAgents...) - } -} diff --git a/internal/appconfig/config_types.go b/internal/appconfig/config_types.go index 7069b21..8b046e6 100644 --- a/internal/appconfig/config_types.go +++ b/internal/appconfig/config_types.go @@ -48,21 +48,6 @@ type TmuxActionMenuEntry struct { Hotkey string // optional single-character hotkey override } -// TmuxEditAgentCfg describes an AI agent's detection and interaction patterns -// for the tmux popup editor (hexai-tmux-edit). -type TmuxEditAgentCfg struct { - Name string - DisplayName string - DetectPattern string - SectionPattern string - PromptPattern string - StripPatterns []string - ClearFirst *bool - ClearKeys string - NewlineKeys string - SubmitKeys string -} - // LoadOptions tune how configuration is loaded at runtime. type LoadOptions struct { // IgnoreEnv skips applying environment overrides when true. @@ -170,7 +155,6 @@ type fileConfig struct { Tmux sectionTmux `toml:"tmux"` Stats sectionStats `toml:"stats"` Ignore sectionIgnore `toml:"ignore"` - TmuxEdit sectionTmuxEdit `toml:"tmux_edit"` TmuxAction sectionTmuxAction `toml:"tmux_action"` MCP sectionMCP `toml:"mcp"` } @@ -225,28 +209,6 @@ type sectionIgnore struct { LSPNotifyIgnored *bool `toml:"lsp_notify_ignored"` } -// sectionTmuxEdit configures the tmux popup editor feature (hexai-tmux-edit). -type sectionTmuxEdit struct { - PopupWidth string `toml:"popup_width"` - PopupHeight string `toml:"popup_height"` - DefaultAgent string `toml:"default_agent"` - Agents []sectionTmuxEditAgent `toml:"agents"` -} - -// sectionTmuxEditAgent defines detection and interaction patterns for one AI agent. -type sectionTmuxEditAgent struct { - Name string `toml:"name"` - DisplayName string `toml:"display_name"` - DetectPattern string `toml:"detect_pattern"` - SectionPattern string `toml:"section_pattern"` - PromptPattern string `toml:"prompt_pattern"` - StripPatterns []string `toml:"strip_patterns"` - ClearFirst *bool `toml:"clear_first"` - ClearKeys string `toml:"clear_keys"` - NewlineKeys string `toml:"newline_keys"` - SubmitKeys string `toml:"submit_keys"` -} - // sectionMCP configures the MCP server settings. type sectionMCP struct { PromptsDir string `toml:"prompts_dir"` diff --git a/internal/appconfig/feature_sections.go b/internal/appconfig/feature_sections.go index 9216400..b4010b8 100644 --- a/internal/appconfig/feature_sections.go +++ b/internal/appconfig/feature_sections.go @@ -1,9 +1,9 @@ package appconfig // This file defines the cohesive per-subsystem config structs that make up -// FeatureConfig. The old FeatureConfig was a grab-bag that mixed five unrelated -// non-LLM subsystems (ignore filtering, stats, tmux popup editor, tmux action -// menu, MCP server). Splitting them into named structs documents the seams +// FeatureConfig. The old FeatureConfig was a grab-bag that mixed four unrelated +// non-LLM subsystems (ignore filtering, stats, tmux action menu, MCP server). +// Splitting them into named structs documents the seams // between subsystems and lets consumers depend on a single subsystem's config // (via the *Section accessors on App) instead of the whole App God-struct. // @@ -29,14 +29,6 @@ type StatsConfig struct { StatsWindowMinutes int `json:"-"` } -// TmuxEditConfig configures the tmux popup editor feature (hexai-tmux-edit). -type TmuxEditConfig struct { - TmuxEditPopupWidth string `json:"-"` - TmuxEditPopupHeight string `json:"-"` - TmuxEditDefaultAgent string `json:"-"` - TmuxEditAgents []TmuxEditAgentCfg `json:"-"` -} - // TmuxActionConfig configures the main menu for hexai-tmux-action. type TmuxActionConfig struct { TmuxActionMenu []TmuxActionMenuEntry `json:"-"` diff --git a/internal/tmuxedit/agent.go b/internal/tmuxedit/agent.go deleted file mode 100644 index 42213ce..0000000 --- a/internal/tmuxedit/agent.go +++ /dev/null @@ -1,149 +0,0 @@ -// 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 - deps tmuxEditDeps -} - -// 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 := b.deps.sendClearSequence(paneID, b.clearKeys); err != nil { - return err - } - b.deps.sleep() - 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 b.deps.sendLines(paneID, text, b.newlineKeys) -} - -func withAgentDeps(agents []Agent, deps tmuxEditDeps) []Agent { - for _, agent := range agents { - withAgentDep(agent, deps) - } - return agents -} - -func withAgentDep(agent Agent, deps tmuxEditDeps) Agent { - if c, ok := agent.(Configurable); ok { - c.Base().deps = deps - } - return agent -} - -// 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() -} diff --git a/internal/tmuxedit/agent_test.go b/internal/tmuxedit/agent_test.go deleted file mode 100644 index ff782d4..0000000 --- a/internal/tmuxedit/agent_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package tmuxedit - -import ( - "fmt" - "strings" - "testing" -) - -func TestDetectAgent(t *testing.T) { - agents := builtinAgents() - tests := []struct { - name string - content string - want string - }{ - {"cursor box ui", "│ → type here │\n/ commands · @ files", "cursor"}, - // Cursor panes often show Claude model names; cursor's box UI must be detected first - {"cursor not false claude", "Claude 4.5 Sonnet\n│ → test │\n/ commands · @ files", "cursor"}, - {"amp from banner", "Amp by Sourcegraph\n> ", "amp"}, - {"aider from banner", "aider v0.50\n> /help", "aider"}, - {"no match", "some random terminal output\n$ ", "generic"}, - {"empty content", "", "generic"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := detectAgent(tt.content, agents) - if got.Name() != tt.want { - t.Errorf("detectAgent() = %q, want %q", got.Name(), tt.want) - } - }) - } -} - -func TestFindAgentByName(t *testing.T) { - agents := builtinAgents() - tests := []struct { - name string - want string - }{ - {"CURSOR", "cursor"}, - {"amp", "amp"}, - {"nonexistent", "generic"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := findAgentByName(tt.name, agents) - if got.Name() != tt.want { - t.Errorf("findAgentByName(%q) = %q, want %q", tt.name, got.Name(), tt.want) - } - }) - } -} - -func TestDetectAgent_InvalidRegex(t *testing.T) { - agents := []Agent{ - &configAgent{baseAgent{name: "bad", detectPattern: "[invalid"}}, - } - got := detectAgent("anything", agents) - if got.Name() != "generic" { - t.Errorf("expected generic fallback for invalid regex, got %q", got.Name()) - } -} - -func TestGenericAgent(t *testing.T) { - g := genericAgent() - if g.Name() != "generic" { - t.Errorf("Name = %q, want generic", g.Name()) - } -} - -func TestBaseAgent_SendText_Empty(t *testing.T) { - b := &baseAgent{newlineKeys: "S-Enter"} - err := b.SendText("%1", "") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } -} - -func TestBaseAgent_ClearInput_Disabled(t *testing.T) { - b := &baseAgent{clearFirst: false, clearKeys: "C-u"} - err := b.ClearInput("%1") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } -} - -func TestBaseAgent_ClearInput_EmptyKeys(t *testing.T) { - // clearFirst=true but no clearKeys should be a no-op - b := &baseAgent{clearFirst: true, clearKeys: ""} - err := b.ClearInput("%1") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } -} - -func TestBaseAgent_ClearInput_Enabled(t *testing.T) { - var calls []string - deps := noSleepDeps() - deps.sendKeys = func(paneID string, keys ...string) error { - calls = append(calls, fmt.Sprintf("send:%s:%s", paneID, strings.Join(keys, ","))) - return nil - } - - b := &baseAgent{clearFirst: true, clearKeys: "C-u", deps: deps} - err := b.ClearInput("%2") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(calls) != 1 || calls[0] != "send:%2:C-u" { - t.Errorf("expected single C-u send call, got %v", calls) - } -} - -func TestBaseAgent_ClearInput_Error(t *testing.T) { - deps := noSleepDeps() - deps.sendKeys = func(string, ...string) error { - return fmt.Errorf("send failed") - } - - b := &baseAgent{clearFirst: true, clearKeys: "C-u", deps: deps} - err := b.ClearInput("%1") - if err == nil { - t.Fatal("expected error from sendClearSequence failure") - } -} - -func TestBaseAgent_ExtractPrompt_NoPattern(t *testing.T) { - b := &baseAgent{} - got := b.ExtractPrompt("some content") - if got != "" { - t.Errorf("expected empty, got %q", got) - } -} - -func TestBaseAgent_ExtractPrompt_InvalidRegex(t *testing.T) { - b := &baseAgent{promptPat: "[invalid"} - got := b.ExtractPrompt("> test") - if got != "" { - t.Errorf("expected empty for invalid regex, got %q", got) - } -} - -func TestConfigurable_Interface(t *testing.T) { - // Verify that all agent types implement Configurable - agents := builtinAgents() - for _, a := range agents { - c, ok := a.(Configurable) - if !ok { - t.Errorf("agent %q does not implement Configurable", a.Name()) - continue - } - base := c.Base() - if base.name != a.Name() { - t.Errorf("Base().name = %q, want %q", base.name, a.Name()) - } - } -} diff --git a/internal/tmuxedit/agentutil.go b/internal/tmuxedit/agentutil.go deleted file mode 100644 index bf1a723..0000000 --- a/internal/tmuxedit/agentutil.go +++ /dev/null @@ -1,183 +0,0 @@ -// Package tmuxedit implements a tmux popup editor for composing AI agent prompts. -// agentutil.go provides shared helpers for prompt extraction and tmux key sending -// used by individual agent implementations. -package tmuxedit - -import ( - "fmt" - "regexp" - "strconv" - "strings" - "time" -) - -const escapeKeyDelay = 150 * time.Millisecond - -// promptMatch holds a regex match result with its line number in the pane. -type promptMatch struct { - lineNum int - text string // capture group 1 -} - -// matchPromptLines runs the prompt regex against each pane line, returning -// matches with their line numbers for contiguity analysis. -func matchPromp |
