diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-19 08:36:13 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-19 08:36:13 +0300 |
| commit | 8dbec6969ceff32662c41bd267210c34b90d8810 (patch) | |
| tree | a91c3561a74287a41d60d9f00fe4fe3a6e9e04a8 | |
| parent | 54cf02d5ec926799f1e96c6b21d4cfd41fc0617d (diff) | |
Refactor hexaiaction handlers for nk0
| -rw-r--r-- | internal/hexaiaction/action_custom.go | 25 | ||||
| -rw-r--r-- | internal/hexaiaction/action_custom_prompt.go | 29 | ||||
| -rw-r--r-- | internal/hexaiaction/action_diagnostics.go | 17 | ||||
| -rw-r--r-- | internal/hexaiaction/action_document.go | 17 | ||||
| -rw-r--r-- | internal/hexaiaction/action_fix_typos.go | 17 | ||||
| -rw-r--r-- | internal/hexaiaction/action_gotest.go | 17 | ||||
| -rw-r--r-- | internal/hexaiaction/action_handler.go | 88 | ||||
| -rw-r--r-- | internal/hexaiaction/action_handler_test.go | 87 | ||||
| -rw-r--r-- | internal/hexaiaction/action_rewrite.go | 29 | ||||
| -rw-r--r-- | internal/hexaiaction/action_simplify.go | 17 | ||||
| -rw-r--r-- | internal/hexaiaction/action_skip.go | 11 | ||||
| -rw-r--r-- | internal/hexaiaction/run.go | 298 |
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, }) } |
