From a1031b761ff0c5cd79f3f0906e5c3b33fa849f37 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sat, 25 Apr 2026 16:12:23 +0300 Subject: feat: configurable hexai-tmux-action menu via [[tmux_action.menu]] When [[tmux_action.menu]] is defined in config it fully replaces the built-in menu. Each entry takes a kind (built-in or "custom"), optional title/hotkey overrides, and optional custom_id for embedding custom actions directly in the main menu. Hotkey dispatch is now dynamic so any single-character hotkey works without code changes. Co-Authored-By: Claude Sonnet 4.6 --- internal/appconfig/app_sections.go | 4 ++++ internal/appconfig/config_load.go | 18 ++++++++++++++++++ internal/appconfig/config_merge.go | 8 ++++++++ internal/appconfig/config_types.go | 25 +++++++++++++++++++++++++ 4 files changed, 55 insertions(+) (limited to 'internal/appconfig') diff --git a/internal/appconfig/app_sections.go b/internal/appconfig/app_sections.go index 5426f2a..6dd60c6 100644 --- a/internal/appconfig/app_sections.go +++ b/internal/appconfig/app_sections.go @@ -114,6 +114,8 @@ type FeatureConfig struct { TmuxEditPopupHeight string `json:"-"` TmuxEditDefaultAgent string `json:"-"` TmuxEditAgents []TmuxEditAgentCfg `json:"-"` + // TmuxAction: configurable main menu for hexai-tmux-action + TmuxActionMenu []TmuxActionMenuEntry `json:"-"` // MCP: Model Context Protocol server settings MCPPromptsDir string `json:"-"` // Directory for prompt storage MCPSlashCommandSync bool `json:"-"` // Enable slash command sync @@ -205,6 +207,7 @@ 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 } @@ -214,4 +217,5 @@ 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/config_load.go b/internal/appconfig/config_load.go index 88750fd..aa169bc 100644 --- a/internal/appconfig/config_load.go +++ b/internal/appconfig/config_load.go @@ -284,6 +284,24 @@ func applyFeatureSections(fc *fileConfig, out *App) { applyStatsSection(fc, out) fc.applyTmuxEdit(out) applyMCPSection(fc, out) + applyTmuxActionSection(fc, out) +} + +// applyTmuxActionSection converts [[tmux_action.menu]] entries into App fields. +func applyTmuxActionSection(fc *fileConfig, out *App) { + if len(fc.TmuxAction.Menu) == 0 { + return + } + entries := make([]TmuxActionMenuEntry, 0, len(fc.TmuxAction.Menu)) + for _, e := range fc.TmuxAction.Menu { + entries = append(entries, TmuxActionMenuEntry{ + Kind: strings.TrimSpace(e.Kind), + CustomID: strings.TrimSpace(e.CustomID), + Title: strings.TrimSpace(e.Title), + Hotkey: strings.TrimSpace(e.Hotkey), + }) + } + out.TmuxActionMenu = entries } func applyGeneralSection(fc *fileConfig, out *App) { diff --git a/internal/appconfig/config_merge.go b/internal/appconfig/config_merge.go index e31e0dc..7a99c94 100644 --- a/internal/appconfig/config_merge.go +++ b/internal/appconfig/config_merge.go @@ -11,6 +11,7 @@ func (a *App) mergeWith(other *App) { a.mergeSurfaceModels(other) a.mergePrompts(other) a.mergeTmuxEdit(other) + a.mergeTmuxAction(other) } // mergeBasics merges general (non-provider) fields. @@ -227,6 +228,13 @@ func mergeStringField(dst *string, src string) { } } +// mergeTmuxAction replaces the action menu when the incoming config defines one. +func (a *App) mergeTmuxAction(other *App) { + if len(other.TmuxActionMenu) > 0 { + 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 != "" { diff --git a/internal/appconfig/config_types.go b/internal/appconfig/config_types.go index 54f9bcf..5c6cf0c 100644 --- a/internal/appconfig/config_types.go +++ b/internal/appconfig/config_types.go @@ -35,6 +35,19 @@ type CustomAction struct { User string // optional; if set, render with available vars } +// TmuxActionMenuEntry configures a single entry in the hexai-tmux-action menu. +// Set Kind to a built-in action kind (rewrite, simplify, document, gotest, +// fix_typos, custom_prompt, skip) or to "custom" to embed a custom action +// directly in the main menu (requires CustomID referencing a +// [[prompts.code_action.custom]] entry). Title and Hotkey are optional +// overrides; built-in defaults are used when left empty. +type TmuxActionMenuEntry struct { + Kind string // built-in kind or "custom" + CustomID string // used when Kind == "custom", references a custom action by id + Title string // optional title override + 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 { @@ -155,6 +168,7 @@ type fileConfig struct { Stats sectionStats `toml:"stats"` Ignore sectionIgnore `toml:"ignore"` TmuxEdit sectionTmuxEdit `toml:"tmux_edit"` + TmuxAction sectionTmuxAction `toml:"tmux_action"` MCP sectionMCP `toml:"mcp"` } @@ -352,3 +366,14 @@ type sectionCustomAction struct { type sectionTmux struct { CustomMenuHotkey string `toml:"custom_menu_hotkey"` } + +type sectionTmuxAction struct { + Menu []sectionTmuxActionMenuEntry `toml:"menu"` +} + +type sectionTmuxActionMenuEntry struct { + Kind string `toml:"kind"` + CustomID string `toml:"custom_id"` + Title string `toml:"title"` + Hotkey string `toml:"hotkey"` +} -- cgit v1.2.3