summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-19 08:36:13 +0300
committerPaul Buetow <paul@buetow.org>2026-06-19 08:36:13 +0300
commit8dbec6969ceff32662c41bd267210c34b90d8810 (patch)
treea91c3561a74287a41d60d9f00fe4fe3a6e9e04a8
parent54cf02d5ec926799f1e96c6b21d4cfd41fc0617d (diff)
Refactor hexaiaction handlers for nk0
-rw-r--r--internal/hexaiaction/action_custom.go25
-rw-r--r--internal/hexaiaction/action_custom_prompt.go29
-rw-r--r--internal/hexaiaction/action_diagnostics.go17
-rw-r--r--internal/hexaiaction/action_document.go17
-rw-r--r--internal/hexaiaction/action_fix_typos.go17
-rw-r--r--internal/hexaiaction/action_gotest.go17
-rw-r--r--internal/hexaiaction/action_handler.go88
-rw-r--r--internal/hexaiaction/action_handler_test.go87
-rw-r--r--internal/hexaiaction/action_rewrite.go29
-rw-r--r--internal/hexaiaction/action_simplify.go17
-rw-r--r--internal/hexaiaction/action_skip.go11
-rw-r--r--internal/hexaiaction/run.go298
12 files changed, 434 insertions, 218 deletions
diff --git a/internal/hexaiaction/action_custom.go b/internal/hexaiaction/action_custom.go
new file mode 100644
index 0000000..a71c4bd
--- /dev/null
+++ b/internal/hexaiaction/action_custom.go
@@ -0,0 +1,25 @@
+package hexaiaction
+
+import (
+ "context"
+
+ "codeberg.org/snonux/hexai/internal/appconfig"
+)
+
+func init() {
+ registerActionHandler(ActionCustom, actionHandlerFunc(handleCustomActionRequest))
+}
+
+func handleCustomActionRequest(ctx context.Context, req actionRequest) (string, error) {
+ return handleCustomAction(ctx, req.parts, req.cfg, req.client, req.selectedCustom)
+}
+
+func handleCustomAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer, selectedCustom *appconfig.CustomAction) (string, error) {
+ if selectedCustom == nil {
+ return parts.Selection, nil
+ }
+ custom := *selectedCustom
+ return runWithTimeout(ctx, timeout20s, func(cctx context.Context) (string, error) {
+ return runCustom(cctx, cfg, client, custom, parts)
+ })
+}
diff --git a/internal/hexaiaction/action_custom_prompt.go b/internal/hexaiaction/action_custom_prompt.go
new file mode 100644
index 0000000..9e65ca6
--- /dev/null
+++ b/internal/hexaiaction/action_custom_prompt.go
@@ -0,0 +1,29 @@
+package hexaiaction
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "strings"
+
+ "codeberg.org/snonux/hexai/internal/logging"
+)
+
+func init() {
+ registerActionHandler(ActionCustomPrompt, actionHandlerFunc(handleCustomPromptActionRequest))
+}
+
+func handleCustomPromptActionRequest(ctx context.Context, req actionRequest) (string, error) {
+ return handleCustomPromptAction(ctx, req.parts, req.cfg, req.client, req.stderr)
+}
+
+func handleCustomPromptAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer, stderr io.Writer) (string, error) {
+ prompt, err := actionEditorFromContext(ctx)(ctx, nil)
+ if err != nil || strings.TrimSpace(prompt) == "" {
+ _, _ = fmt.Fprintln(stderr, logging.AnsiBase+"hexai-tmux-action: custom prompt canceled or empty; echoing input"+logging.AnsiReset)
+ return parts.Selection, nil
+ }
+ return runWithTimeout(ctx, timeout20s, func(cctx context.Context) (string, error) {
+ return runRewrite(cctx, cfg, client, prompt, parts.Selection)
+ })
+}
diff --git a/internal/hexaiaction/action_diagnostics.go b/internal/hexaiaction/action_diagnostics.go
new file mode 100644
index 0000000..bb2c412
--- /dev/null
+++ b/internal/hexaiaction/action_diagnostics.go
@@ -0,0 +1,17 @@
+package hexaiaction
+
+import "context"
+
+func init() {
+ registerActionHandler(ActionDiagnostics, actionHandlerFunc(handleDiagnosticsActionRequest))
+}
+
+func handleDiagnosticsActionRequest(ctx context.Context, req actionRequest) (string, error) {
+ return handleDiagnosticsAction(ctx, req.parts, req.cfg, req.client)
+}
+
+func handleDiagnosticsAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer) (string, error) {
+ return runWithTimeout(ctx, timeout20s, func(cctx context.Context) (string, error) {
+ return runDiagnostics(cctx, cfg, client, parts.Diagnostics, parts.Selection)
+ })
+}
diff --git a/internal/hexaiaction/action_document.go b/internal/hexaiaction/action_document.go
new file mode 100644
index 0000000..c0d560a
--- /dev/null
+++ b/internal/hexaiaction/action_document.go
@@ -0,0 +1,17 @@
+package hexaiaction
+
+import "context"
+
+func init() {
+ registerActionHandler(ActionDocument, actionHandlerFunc(handleDocumentActionRequest))
+}
+
+func handleDocumentActionRequest(ctx context.Context, req actionRequest) (string, error) {
+ return handleDocumentAction(ctx, req.parts, req.cfg, req.client)
+}
+
+func handleDocumentAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer) (string, error) {
+ return runWithTimeout(ctx, timeout20s, func(cctx context.Context) (string, error) {
+ return runDocument(cctx, cfg, client, parts.Selection)
+ })
+}
diff --git a/internal/hexaiaction/action_fix_typos.go b/internal/hexaiaction/action_fix_typos.go
new file mode 100644
index 0000000..0059500
--- /dev/null
+++ b/internal/hexaiaction/action_fix_typos.go
@@ -0,0 +1,17 @@
+package hexaiaction
+
+import "context"
+
+func init() {
+ registerActionHandler(ActionFixTypos, actionHandlerFunc(handleFixTyposActionRequest))
+}
+
+func handleFixTyposActionRequest(ctx context.Context, req actionRequest) (string, error) {
+ return handleFixTyposAction(ctx, req.parts, req.cfg, req.client)
+}
+
+func handleFixTyposAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer) (string, error) {
+ return runWithTimeout(ctx, timeout20s, func(cctx context.Context) (string, error) {
+ return runFixTypos(cctx, cfg, client, parts.Selection)
+ })
+}
diff --git a/internal/hexaiaction/action_gotest.go b/internal/hexaiaction/action_gotest.go
new file mode 100644
index 0000000..db53541
--- /dev/null
+++ b/internal/hexaiaction/action_gotest.go
@@ -0,0 +1,17 @@
+package hexaiaction
+
+import "context"
+
+func init() {
+ registerActionHandler(ActionGoTest, actionHandlerFunc(handleGoTestActionRequest))
+}
+
+func handleGoTestActionRequest(ctx context.Context, req actionRequest) (string, error) {
+ return handleGoTestAction(ctx, req.parts, req.cfg, req.client)
+}
+
+func handleGoTestAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer) (string, error) {
+ return runWithTimeout(ctx, timeout18s, func(cctx context.Context) (string, error) {
+ return runGoTest(cctx, cfg, client, parts.Selection)
+ })
+}
diff --git a/internal/hexaiaction/action_handler.go b/internal/hexaiaction/action_handler.go
new file mode 100644
index 0000000..8da90ea
--- /dev/null
+++ b/internal/hexaiaction/action_handler.go
@@ -0,0 +1,88 @@
+package hexaiaction
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "sync"
+
+ "codeberg.org/snonux/hexai/internal/appconfig"
+)
+
+type actionRequest struct {
+ parts InputParts
+ cfg actionConfig
+ client chatDoer
+ stderr io.Writer
+ selectedCustom *appconfig.CustomAction
+}
+
+// ActionHandler executes one tmux action kind.
+type ActionHandler interface {
+ Execute(context.Context, actionRequest) (string, error)
+}
+
+// CodeActionHandler is kept as a compatibility name for tmux code-action handlers.
+type CodeActionHandler = ActionHandler
+
+type actionHandlerFunc func(context.Context, actionRequest) (string, error)
+
+func (f actionHandlerFunc) Execute(ctx context.Context, req actionRequest) (string, error) {
+ return f(ctx, req)
+}
+
+type actionHandlerRegistry struct {
+ mu sync.RWMutex
+ handlers map[ActionKind]ActionHandler
+}
+
+func newActionHandlerRegistry() *actionHandlerRegistry {
+ return &actionHandlerRegistry{handlers: make(map[ActionKind]ActionHandler)}
+}
+
+func (r *actionHandlerRegistry) register(kind ActionKind, handler ActionHandler) {
+ if kind == "" {
+ panic("hexaiaction: cannot register empty action kind")
+ }
+ if handler == nil {
+ panic(fmt.Sprintf("hexaiaction: cannot register nil handler for %q", kind))
+ }
+
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ if _, exists := r.handlers[kind]; exists {
+ panic(fmt.Sprintf("hexaiaction: handler already registered for %q", kind))
+ }
+ r.handlers[kind] = handler
+}
+
+func (r *actionHandlerRegistry) lookup(kind ActionKind) (ActionHandler, bool) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ handler, ok := r.handlers[kind]
+ return handler, ok
+}
+
+func (r *actionHandlerRegistry) snapshot() map[ActionKind]ActionHandler {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ handlers := make(map[ActionKind]ActionHandler, len(r.handlers))
+ for kind, handler := range r.handlers {
+ handlers[kind] = handler
+ }
+ return handlers
+}
+
+var actionHandlers = newActionHandlerRegistry()
+
+func registerActionHandler(kind ActionKind, handler ActionHandler) {
+ actionHandlers.register(kind, handler)
+}
+
+func lookupActionHandler(kind ActionKind) (ActionHandler, bool) {
+ return actionHandlers.lookup(kind)
+}
+
+func codeActionHandlers() map[ActionKind]CodeActionHandler {
+ return actionHandlers.snapshot()
+}
diff --git a/internal/hexaiaction/action_handler_test.go b/internal/hexaiaction/action_handler_test.go
new file mode 100644
index 0000000..1d811fe
--- /dev/null
+++ b/internal/hexaiaction/action_handler_test.go
@@ -0,0 +1,87 @@
+package hexaiaction
+
+import (
+ "context"
+ "strings"
+ "testing"
+
+ "codeberg.org/snonux/hexai/internal/appconfig"
+)
+
+func TestActionHandlers_AreSelfRegistered(t *testing.T) {
+ handlers := codeActionHandlers()
+ for _, kind := range []ActionKind{
+ ActionSkip,
+ ActionRewrite,
+ ActionDiagnostics,
+ ActionDocument,
+ ActionGoTest,
+ ActionSimplify,
+ ActionFixTypos,
+ ActionCustom,
+ ActionCustomPrompt,
+ } {
+ if _, ok := handlers[kind]; !ok {
+ t.Fatalf("expected handler for %q", kind)
+ }
+ }
+}
+
+func TestActionHandlers_SnapshotDoesNotMutateRegistry(t *testing.T) {
+ handlers := codeActionHandlers()
+ delete(handlers, ActionSkip)
+ if _, ok := lookupActionHandler(ActionSkip); !ok {
+ t.Fatal("mutating handler snapshot changed registry")
+ }
+}
+
+func TestExecuteAction_UnknownFallsBackToSelection(t *testing.T) {
+ cfg := appconfig.App{}
+ parts := InputParts{Selection: "original"}
+ out, err := executeAction(context.Background(), ActionKind("missing"), parts, &cfg, fakeDoer{"ignored"}, nil, nil)
+ if err != nil {
+ t.Fatalf("executeAction: %v", err)
+ }
+ if out != "original" {
+ t.Fatalf("expected fallback selection, got %q", out)
+ }
+}
+
+func TestActionHandlerRegistryRejectsInvalidRegistrations(t *testing.T) {
+ tests := map[string]func(*actionHandlerRegistry){
+ "empty kind": func(registry *actionHandlerRegistry) {
+ registry.register("", actionHandlerFunc(handleSkipAction))
+ },
+ "nil handler": func(registry *actionHandlerRegistry) {
+ registry.register(ActionKind("nil"), nil)
+ },
+ "duplicate kind": func(registry *actionHandlerRegistry) {
+ registry.register(ActionKind("dup"), actionHandlerFunc(handleSkipAction))
+ registry.register(ActionKind("dup"), actionHandlerFunc(handleSkipAction))
+ },
+ }
+
+ for name, run := range tests {
+ t.Run(name, func(t *testing.T) {
+ defer func() {
+ if recovered := recover(); recovered == nil {
+ t.Fatal("expected panic")
+ }
+ }()
+ run(newActionHandlerRegistry())
+ })
+ }
+}
+
+func TestActionHandlerRegistryNilPanicNamesKind(t *testing.T) {
+ defer func() {
+ recovered := recover()
+ if recovered == nil {
+ t.Fatal("expected panic")
+ }
+ if !strings.Contains(recovered.(string), "custom") {
+ t.Fatalf("expected panic to name kind, got %v", recovered)
+ }
+ }()
+ newActionHandlerRegistry().register(ActionCustom, nil)
+}
diff --git a/internal/hexaiaction/action_rewrite.go b/internal/hexaiaction/action_rewrite.go
new file mode 100644
index 0000000..920eaed
--- /dev/null
+++ b/internal/hexaiaction/action_rewrite.go
@@ -0,0 +1,29 @@
+package hexaiaction
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "strings"
+
+ "codeberg.org/snonux/hexai/internal/logging"
+)
+
+func init() {
+ registerActionHandler(ActionRewrite, actionHandlerFunc(handleRewriteActionRequest))
+}
+
+func handleRewriteActionRequest(ctx context.Context, req actionRequest) (string, error) {
+ return handleRewriteAction(ctx, req.parts, req.cfg, req.client, req.stderr)
+}
+
+func handleRewriteAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer, stderr io.Writer) (string, error) {
+ instr, cleaned := ExtractInstruction(parts.Selection)
+ if strings.TrimSpace(instr) == "" {
+ _, _ = fmt.Fprintln(stderr, logging.AnsiBase+"hexai-tmux-action: no inline instruction found; echoing input"+logging.AnsiReset)
+ return parts.Selection, nil
+ }
+ return runWithTimeout(ctx, timeout20s, func(cctx context.Context) (string, error) {
+ return runRewrite(cctx, cfg, client, instr, cleaned)
+ })
+}
diff --git a/internal/hexaiaction/action_simplify.go b/internal/hexaiaction/action_simplify.go
new file mode 100644
index 0000000..4d7c83b
--- /dev/null
+++ b/internal/hexaiaction/action_simplify.go
@@ -0,0 +1,17 @@
+package hexaiaction
+
+import "context"
+
+func init() {
+ registerActionHandler(ActionSimplify, actionHandlerFunc(handleSimplifyActionRequest))
+}
+
+func handleSimplifyActionRequest(ctx context.Context, req actionRequest) (string, error) {
+ return handleSimplifyAction(ctx, req.parts, req.cfg, req.client)
+}
+
+func handleSimplifyAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer) (string, error) {
+ return runWithTimeout(ctx, timeout20s, func(cctx context.Context) (string, error) {
+ return runSimplify(cctx, cfg, client, parts.Selection)
+ })
+}
diff --git a/internal/hexaiaction/action_skip.go b/internal/hexaiaction/action_skip.go
new file mode 100644
index 0000000..111e2f7
--- /dev/null
+++ b/internal/hexaiaction/action_skip.go
@@ -0,0 +1,11 @@
+package hexaiaction
+
+import "context"
+
+func init() {
+ registerActionHandler(ActionSkip, actionHandlerFunc(handleSkipAction))
+}
+
+func handleSkipAction(_ context.Context, req actionRequest) (string, error) {
+ return req.parts.Selection, nil
+}
diff --git a/internal/hexaiaction/run.go b/internal/hexaiaction/run.go
index 8b78bd0..4f91ab5 100644
--- a/internal/hexaiaction/run.go
+++ b/internal/hexaiaction/run.go
@@ -81,6 +81,14 @@ type Runner struct {
statusSink actionStatusSink
}
+type actionRunDeps struct {
+ chooser actionChooser
+ newClient actionClientFactory
+ loadConfig actionConfigLoader
+ openEditor actionEditorOpener
+ statusSink actionStatusSink
+}
+
// NewRunner builds a Runner with production dependencies.
func NewRunner() *Runner {
return &Runner{
@@ -137,81 +145,95 @@ func actionEditorFromContext(ctx context.Context) actionEditorOpener {
return editor.OpenTempAndEdit
}
-type actionPlan struct {
- fallback string
- run func(context.Context) (string, error)
-}
-
-// CodeActionHandler builds a plan for an action and resolves it.
-type CodeActionHandler interface {
- Build(parts InputParts, cfg actionConfig, client chatDoer, stderr io.Writer) (actionPlan, bool)
- Resolve(ctx context.Context, plan actionPlan) (string, error)
-}
-
-type codeActionHandler struct {
- build func(parts InputParts, cfg actionConfig, client chatDoer, stderr io.Writer) (actionPlan, bool)
+func Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error {
+ return NewRunner().Run(ctx, stdin, stdout, stderr)
}
-func (h codeActionHandler) Build(parts InputParts, cfg actionConfig, client chatDoer, stderr io.Writer) (actionPlan, bool) {
- if h.build == nil {
- return actionPlan{}, false
+func (r *Runner) Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error {
+ deps := r.resolveDeps()
+ logger := log.New(stderr, "hexai-tmux-action ", log.LstdFlags|log.Lmsgprefix)
+ cfg, err := prepareRunConfig(ctx, stderr, logger, deps.loadConfig)
+ if err != nil {
+ return err
}
- return h.build(parts, cfg, client, stderr)
-}
-
-func (h codeActionHandler) Resolve(ctx context.Context, plan actionPlan) (string, error) {
- if plan.run == nil {
- return plan.fallback, nil
+ client, err := prepareActionClient(stderr, cfg, deps.newClient, deps.statusSink)
+ if err != nil {
+ return err
+ }
+ parts, err := ParseInput(stdin)
+ if err != nil {
+ return fmt.Errorf("hexai-tmux-action: failed to read action input from stdin (pipe the selected text or pane contents into hexai-tmux-action): %w", err)
+ }
+ if err := requireInput(parts.Selection); err != nil {
+ return fmt.Errorf("hexai-tmux-action: %w", err)
}
- return plan.run(ctx)
+ choice, err := deps.chooser(cfg)
+ if err != nil {
+ return err
+ }
+ out, err := executeAction(withActionEditor(ctx, deps.openEditor), choice.kind, parts, &cfg, client, stderr, choice.custom)
+ if err != nil {
+ return err
+ }
+ _, _ = io.WriteString(stdout, out)
+ return nil
}
-func Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error {
- return NewRunner().Run(ctx, stdin, stdout, stderr)
+func (r *Runner) resolveDeps() actionRunDeps {
+ deps := actionRunDeps{
+ chooser: chooseActionFromConfig,
+ newClient: defaultActionClientFactory,
+ loadConfig: loadActionConfig,
+ openEditor: actionEditorOpener(editor.OpenTempAndEdit),
+ statusSink: actionStatusSink(tmuxActionStatusSink{}),
+ }
+ if r == nil {
+ return deps
+ }
+ return r.applyOverrides(deps)
}
-func (r *Runner) Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error {
- chooser := chooseActionFromConfig
- newClient := defaultActionClientFactory
- loadConfig := loadActionConfig
- openEditor := actionEditorOpener(editor.OpenTempAndEdit)
- statusSink := actionStatusSink(tmuxActionStatusSink{})
- if r != nil {
- if r.chooseAction != nil {
- chooser = r.chooseAction
- }
- if r.newClient != nil {
- newClient = r.newClient
- }
- if r.loadConfig != nil {
- loadConfig = r.loadConfig
- }
- if r.openEditor != nil {
- openEditor = r.openEditor
- }
- if r.statusSink != nil {
- statusSink = r.statusSink
- }
+func (r *Runner) applyOverrides(deps actionRunDeps) actionRunDeps {
+ if r.chooseAction != nil {
+ deps.chooser = r.chooseAction
+ }
+ if r.newClient != nil {
+ deps.newClient = r.newClient
+ }
+ if r.loadConfig != nil {
+ deps.loadConfig = r.loadConfig
+ }
+ if r.openEditor != nil {
+ deps.openEditor = r.openEditor
+ }
+ if r.statusSink != nil {
+ deps.statusSink = r.statusSink
}
+ return deps
+}
- logger := log.New(stderr, "hexai-tmux-action ", log.LstdFlags|log.Lmsgprefix)
+func prepareRunConfig(ctx context.Context, stderr io.Writer, logger *log.Logger, loadConfig actionConfigLoader) (appconfig.App, error) {
cfg := loadConfig(ctx, logger)
if cfg.StatsWindowMinutes > 0 {
stats.SetWindow(time.Duration(cfg.StatsWindowMinutes) * time.Minute)
}
if err := cfg.Validate(); err != nil {
_, _ = fmt.Fprintf(stderr, logging.AnsiBase+"hexai-tmux-action: %v"+logging.AnsiReset+"\n", err)
- return err
+ return cfg, err
}
if len(cfg.CodeActionConfigs) > 0 {
if provider := strings.TrimSpace(cfg.CodeActionConfigs[0].Provider); provider != "" {
cfg.Provider = provider
}
}
+ return cfg, nil
+}
+
+func prepareActionClient(stderr io.Writer, cfg appconfig.App, newClient actionClientFactory, statusSink actionStatusSink) (chatDoer, error) {
cli, err := newClient(cfg)
if err != nil {
_, _ = fmt.Fprintf(stderr, logging.AnsiBase+"hexai-tmux-action: LLM disabled: %v"+logging.AnsiReset+"\n", err)
- return err
+ return nil, err
}
primaryModel := strings.TrimSpace(reqOptsFrom(&cfg).model)
if primaryModel == "" {
@@ -220,24 +242,7 @@ func (r *Runner) Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Wri
if statusSink != nil {
_ = statusSink.SetLLMStart(cli.Name(), primaryModel)
}
- var client chatDoer = cli
- parts, err := ParseInput(stdin)
- if err != nil {
- return fmt.Errorf("hexai-tmux-action: failed to read action input from stdin (pipe the selected text or pane contents into hexai-tmux-action): %w", err)
- }
- if err := requireInput(parts.Selection); err != nil {
- return fmt.Errorf("hexai-tmux-action: %w", err)
- }
- choice, err := chooser(cfg)
- if err != nil {
- return err
- }
- out, err := executeAction(withActionEditor(ctx, openEditor), choice.kind, parts, &cfg, client, stderr, choice.custom)
- if err != nil {
- return err
- }
- _, _ = io.WriteString(stdout, out)
- return nil
+ return cli, nil
}
// WithConfigPath attaches a config path override to the context for Run/RunCommand.
@@ -259,159 +264,16 @@ func configPathFromContext(ctx context.Context) string {
}
func executeAction(ctx context.Context, kind ActionKind, parts InputParts, cfg actionConfig, client chatDoer, stderr io.Writer, selectedCustom *appconfig.CustomAction) (string, error) {
- if kind == ActionCustom {
- return handleCustomAction(ctx, parts, cfg, client, selectedCustom)
- }
- handler, ok := codeActionHandlers()[kind]
+ handler, ok := lookupActionHandler(kind)
if !ok {
return parts.Selection, nil
}
- plan, ok := handler.Build(parts, cfg, client, stderr)
- if !ok {
- return parts.Selection, nil
- }
- return handler.Resolve(ctx, plan)
-}
-
-func codeActionHandlers() map[ActionKind]CodeActionHandler {
- return map[ActionKind]CodeActionHandler{
- ActionSkip: codeActionHandler{build: buildSkipPlan},
- ActionRewrite: codeActionHandler{build: buildRewritePlan},
- ActionDiagnostics: codeActionHandler{build: buildDiagnosticsPlan},
- ActionDocument: codeActionHandler{build: buildDocumentPlan},
- ActionGoTest: codeActionHandler{build: buildGoTestPlan},
- ActionSimplify: codeActionHandler{build: buildSimplifyPlan},
- ActionFixTypos: codeActionHandler{build: buildFixTyposPlan},
- ActionCustomPrompt: codeActionHandler{build: buildCustomPromptPlan},
- }
-}
-
-func buildSkipPlan(parts InputParts, _ actionConfig, _ chatDoer, _ io.Writer) (actionPlan, bool) {
- return actionPlan{fallback: parts.Selection}, true
-}
-
-func buildRewritePlan(parts InputParts, cfg actionConfig, client chatDoer, stderr io.Writer) (actionPlan, bool) {
- return actionPlan{
- fallback: parts.Selection,
- run: func(ctx context.Context) (string, error) {
- return handleRewriteAction(ctx, parts, cfg, client, stderr)
- },
- }, true
-}
-
-func buildDiagnosticsPlan(parts InputParts, cfg actionConfig, client chatDoer, _ io.Writer) (actionPlan, bool) {
- return actionPlan{
- fallback: parts.Selection,
- run: func(ctx context.Context) (string, error) {
- return handleDiagnosticsAction(ctx, parts, cfg, client)
- },
- }, true
-}
-
-func buildDocumentPlan(parts InputParts, cfg actionConfig, client chatDoer, _ io.Writer) (actionPlan, bool) {
- return actionPlan{
- fallback: parts.Selection,
- run: func(ctx context.Context) (string, error) {
- return handleDocumentAction(ctx, parts, cfg, client)
- },
- }, true
-}
-
-func buildGoTestPlan(parts InputParts, cfg actionConfig, client chatDoer, _ io.Writer) (actionPlan, bool) {
- return actionPlan{
- fallback: parts.Selection,
- run: func(ctx context.Context) (string, error) {
- return handleGoTestAction(ctx, parts, cfg, client)
- },
- }, true
-}
-
-func buildSimplifyPlan(parts InputParts, cfg actionConfig, client chatDoer, _ io.Writer) (actionPlan, bool) {
- return actionPlan{
- fallback: parts.Selection,
- run: func(ctx context.Context) (string, error) {
- return handleSimplifyAction(ctx, parts, cfg, client)
- },
- }, true
-}
-
-func buildFixTyposPlan(parts InputParts, cfg actionConfig, client chatDoer, _ io.Writer) (actionPlan, bool) {
- return actionPlan{
- fallback: parts.Selection,
- run: func(ctx context.Context) (string, error) {
- return handleFixTyposAction(ctx, parts, cfg, client)
- },
- }, true
-}
-
-func buildCustomPromptPlan(parts InputParts, cfg actionConfig, client chatDoer, stderr io.Writer) (actionPlan, bool) {
- return actionPlan{
- fallback: parts.Selection,
- run: func(ctx context.Context) (string, error) {
- return handleCustomPromptAction(ctx, parts, cfg, client, stderr)
- },
- }, true
-}
-
-func handleRewriteAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer, stderr io.Writer) (string, error) {
- instr, cleaned := ExtractInstruction(parts.Selection)
- if strings.TrimSpace(instr) == "" {
- _, _ = fmt.Fprintln(stderr, logging.AnsiBase+"hexai-tmux-action: no inline instruction found; echoing input"+logging.AnsiReset)
- return parts.Selection, nil
- }
- return runWithTimeout(ctx, timeout20s, func(cctx context.Context) (string, error) {
- return runRewrite(cctx, cfg, client, instr, cleaned)
- })
-}
-
-func handleDiagnosticsAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer) (string, error) {
- return runWithTimeout(ctx, timeout20s, func(cctx context.Context) (string, error) {
- return runDiagnostics(cctx, cfg, client, parts.Diagnostics, parts.Selection)
- })
-}
-
-func handleDocumentAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer) (string, error) {
- return runWithTimeout(ctx, timeout20s, func(cctx context.Context) (string, error) {
- return runDocument(cctx, cfg, client, parts.Selection)
- })
-}
-
-func handleGoTestAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer) (string, error) {
- return runWithTimeout(ctx, timeout18s, func(cctx context.Context) (string, error) {
- return runGoTest(cctx, cfg, client, parts.Selection)
- })
-}
-
-func handleSimplifyAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer) (string, error) {
- return runWithTimeout(ctx, timeout20s, func(cctx context.Context) (string, error) {
- return runSimplify(cctx, cfg, client, parts.Selection)
- })
-}
-
-func handleFixTyposAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer) (string, error) {
- return runWithTimeout(ctx, timeout20s, func(cctx context.Context) (string, error) {
- return runFixTypos(cctx, cfg, client, parts.Selection)
- })
-}
-
-func handleCustomAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer, selectedCustom *appconfig.CustomAction) (string, error) {
- if selectedCustom == nil {
- return parts.Selection, nil
- }
- custom := *selectedCustom
- return runWithTimeout(ctx, timeout20s, func(cctx context.Context) (string, error) {
- return runCustom(cctx, cfg, client, custom, parts)
- })
-}
-
-func handleCustomPromptAction(ctx context.Context, parts InputParts, cfg actionConfig, client chatDoer, stderr io.Writer) (string, error) {
- prompt, err := actionEditorFromContext(ctx)(ctx, nil)
- if err != nil || strings.TrimSpace(prompt) == "" {
- _, _ = fmt.Fprintln(stderr, logging.AnsiBase+"hexai-tmux-action: custom prompt canceled or empty; echoing input"+logging.AnsiReset)
- return parts.Selection, nil
- }
- return runWithTimeout(ctx, timeout20s, func(cctx context.Context) (string, error) {
- return runRewrite(cctx, cfg, client, prompt, parts.Selection)
+ return handler.Execute(ctx, actionRequest{
+ parts: parts,
+ cfg: cfg,
+ client: client,
+ stderr: stderr,
+ selectedCustom: selectedCustom,
})
}