summaryrefslogtreecommitdiff
path: root/internal/hexaiaction
diff options
context:
space:
mode:
Diffstat (limited to 'internal/hexaiaction')
-rw-r--r--internal/hexaiaction/cmdentry.go26
-rw-r--r--internal/hexaiaction/cmdentry_runcommand_test.go8
-rw-r--r--internal/hexaiaction/cmdentry_test.go22
-rw-r--r--internal/hexaiaction/prompts.go7
-rw-r--r--internal/hexaiaction/run.go16
-rw-r--r--internal/hexaiaction/tui.go3
-rw-r--r--internal/hexaiaction/types.go1
7 files changed, 52 insertions, 31 deletions
diff --git a/internal/hexaiaction/cmdentry.go b/internal/hexaiaction/cmdentry.go
index 78c315b..d60f172 100644
--- a/internal/hexaiaction/cmdentry.go
+++ b/internal/hexaiaction/cmdentry.go
@@ -10,17 +10,16 @@ import (
"codeberg.org/snonux/hexai/internal/llm"
"codeberg.org/snonux/hexai/internal/tmux"
- "golang.org/x/term"
)
// Options configures the command-line orchestration for hexai-tmux-action.
type Options struct {
- Infile string
- Outfile string
- UIChild bool
- TmuxTarget string
- TmuxSplit string // "v" or "h"
- TmuxPercent int // 1-100
+ Infile string
+ Outfile string
+ UIChild bool
+ TmuxTarget string
+ TmuxPopupWidth string // popup width, e.g. "80%" or "120"
+ TmuxPopupHeight string // popup height, e.g. "80%" or "40"
}
// RunCommand is the CLI orchestrator used by cmd/hexai-tmux-action. It runs in tmux
@@ -30,14 +29,13 @@ func RunCommand(ctx context.Context, opts Options, stdin io.Reader, stdout, stde
if opts.UIChild {
return runChild(ctx, opts.Infile, opts.Outfile, stdout, stderr)
}
- // Always use tmux path
- return runInTmuxParent(ctx, stdin, stdout, opts.TmuxTarget, opts.TmuxSplit, opts.TmuxPercent)
+ // Always use tmux popup path
+ return runInTmuxParent(ctx, stdin, stdout, opts.TmuxTarget, opts.TmuxPopupWidth, opts.TmuxPopupHeight)
}
// seams for unit tests
var (
- isTTYFn = func(fd uintptr) bool { return term.IsTerminal(int(fd)) }
- splitRunFn = tmux.SplitRun
+ popupRunFn = tmux.PopupRun
osExecutableFn = os.Executable
runFn = Run
)
@@ -99,7 +97,7 @@ func runChild(ctx context.Context, infile, outfile string, stdout, stderr io.Wri
return os.Rename(tmp, outfile)
}
-func runInTmuxParent(ctx context.Context, stdin io.Reader, stdout io.Writer, target, split string, percent int) error {
+func runInTmuxParent(ctx context.Context, stdin io.Reader, stdout io.Writer, target, popupWidth, popupHeight string) error {
dir, err := os.MkdirTemp("", "hexai-tmux-action-")
if err != nil {
return err
@@ -115,8 +113,8 @@ func runInTmuxParent(ctx context.Context, stdin io.Reader, stdout io.Writer, tar
return err
}
argv := []string{exe, "-ui-child", "-infile", inPath, "-outfile", outPath}
- opts := tmux.SplitOpts{Target: target, Vertical: split != "h", Percent: percent}
- if err := splitRunFn(opts, argv); err != nil {
+ opts := tmux.PopupOpts{Target: target, Width: popupWidth, Height: popupHeight}
+ if err := popupRunFn(opts, argv); err != nil {
return err
}
if err := waitForFile(ctx, outPath, 60*time.Second); err != nil {
diff --git a/internal/hexaiaction/cmdentry_runcommand_test.go b/internal/hexaiaction/cmdentry_runcommand_test.go
index b139bb3..ac6106d 100644
--- a/internal/hexaiaction/cmdentry_runcommand_test.go
+++ b/internal/hexaiaction/cmdentry_runcommand_test.go
@@ -33,12 +33,10 @@ func TestRunCommand_UIChild(t *testing.T) {
}
func TestRunCommand_Tmux(t *testing.T) {
- oldTTY := isTTYFn
oldExec := osExecutableFn
- oldSplit := splitRunFn
- isTTYFn = func(_ uintptr) bool { return false }
+ oldPopup := popupRunFn
osExecutableFn = func() (string, error) { return "/bin/hexai-tmux-action", nil }
- splitRunFn = func(_ tmux.SplitOpts, argv []string) error {
+ popupRunFn = func(_ tmux.PopupOpts, argv []string) error {
for i := 0; i < len(argv)-1; i++ {
if argv[i] == "-outfile" && i+1 < len(argv) {
_ = os.WriteFile(argv[i+1], []byte("OUT"), 0o600)
@@ -47,7 +45,7 @@ func TestRunCommand_Tmux(t *testing.T) {
}
return nil
}
- defer func() { isTTYFn = oldTTY; osExecutableFn = oldExec; splitRunFn = oldSplit }()
+ defer func() { osExecutableFn = oldExec; popupRunFn = oldPopup }()
var out bytes.Buffer
if err := RunCommand(context.Background(), Options{}, bytes.NewBufferString("X"), &out, io.Discard); err != nil {
t.Fatalf("RunCommand tmux: %v", err)
diff --git a/internal/hexaiaction/cmdentry_test.go b/internal/hexaiaction/cmdentry_test.go
index 054c78c..b9d5e9b 100644
--- a/internal/hexaiaction/cmdentry_test.go
+++ b/internal/hexaiaction/cmdentry_test.go
@@ -78,9 +78,9 @@ func TestRunInTmuxParent_Stubbed(t *testing.T) {
// capture stdout
rout, wout, _ := os.Pipe()
oldExec := osExecutableFn
- oldSplit := splitRunFn
+ oldPopup := popupRunFn
osExecutableFn = func() (string, error) { return "/bin/hexai-tmux-action", nil }
- splitRunFn = func(opts tmux.SplitOpts, argv []string) error {
+ popupRunFn = func(opts tmux.PopupOpts, argv []string) error {
for i := 0; i < len(argv)-1; i++ {
if argv[i] == "-outfile" && i+1 < len(argv) {
_ = os.WriteFile(argv[i+1], []byte("OUT:"+strings.Join(argv, ",")), 0o600)
@@ -89,8 +89,8 @@ func TestRunInTmuxParent_Stubbed(t *testing.T) {
}
return nil
}
- t.Cleanup(func() { osExecutableFn = oldExec; splitRunFn = oldSplit })
- if err := runInTmuxParent(context.Background(), r, wout, "", "v", 33); err != nil {
+ t.Cleanup(func() { osExecutableFn = oldExec; popupRunFn = oldPopup })
+ if err := runInTmuxParent(context.Background(), r, wout, "", "", ""); err != nil {
t.Fatalf("runInTmuxParent: %v", err)
}
_ = wout.Close()
@@ -108,22 +108,22 @@ func TestRunInTmuxParent_ExecutableError(t *testing.T) {
r, w, _ := os.Pipe()
_, _ = w.Write([]byte("x"))
_ = w.Close()
- if err := runInTmuxParent(context.Background(), r, io.Discard, "", "v", 33); err == nil {
+ if err := runInTmuxParent(context.Background(), r, io.Discard, "", "", ""); err == nil {
t.Fatal("expected error from missing executable")
}
}
-func TestRunInTmuxParent_SplitError(t *testing.T) {
+func TestRunInTmuxParent_PopupError(t *testing.T) {
oldExec := osExecutableFn
osExecutableFn = func() (string, error) { return "/bin/hexai-tmux-action", nil }
- oldSplit := splitRunFn
- splitRunFn = func(_ tmux.SplitOpts, _ []string) error { return fmt.Errorf("split failed") }
- t.Cleanup(func() { osExecutableFn = oldExec; splitRunFn = oldSplit })
+ oldPopup := popupRunFn
+ popupRunFn = func(_ tmux.PopupOpts, _ []string) error { return fmt.Errorf("popup failed") }
+ t.Cleanup(func() { osExecutableFn = oldExec; popupRunFn = oldPopup })
r, w, _ := os.Pipe()
_, _ = w.Write([]byte("x"))
_ = w.Close()
- if err := runInTmuxParent(context.Background(), r, io.Discard, "", "v", 33); err == nil {
- t.Fatal("expected split error")
+ if err := runInTmuxParent(context.Background(), r, io.Discard, "", "", ""); err == nil {
+ t.Fatal("expected popup error")
}
}
diff --git a/internal/hexaiaction/prompts.go b/internal/hexaiaction/prompts.go
index 03a9441..4b2d8be 100644
--- a/internal/hexaiaction/prompts.go
+++ b/internal/hexaiaction/prompts.go
@@ -82,6 +82,13 @@ func runSimplify(ctx context.Context, cfg actionConfig, client chatDoer, selecti
return runOnce(ctx, client, sys, user, reqOptsFrom(cfg))
}
+func runFixTypos(ctx context.Context, cfg actionConfig, client chatDoer, selection string) (string, error) {
+ prompts := cfg.PromptSection()
+ sys := prompts.PromptCodeActionFixTyposSystem
+ user := Render(prompts.PromptCodeActionFixTyposUser, map[string]string{"selection": selection})
+ return runOnce(ctx, client, sys, user, reqOptsFrom(cfg))
+}
+
func runGoTest(ctx context.Context, cfg actionConfig, client chatDoer, funcCode string) (string, error) {
prompts := cfg.PromptSection()
sys := prompts.PromptCodeActionGoTestSystem
diff --git a/internal/hexaiaction/run.go b/internal/hexaiaction/run.go
index 2a3ade1..0330354 100644
--- a/internal/hexaiaction/run.go
+++ b/internal/hexaiaction/run.go
@@ -251,6 +251,7 @@ func codeActionHandlers() map[ActionKind]CodeActionHandler {
ActionDocument: codeActionHandler{build: buildDocumentPlan},
ActionGoTest: codeActionHandler{build: buildGoTestPlan},
ActionSimplify: codeActionHandler{build: buildSimplifyPlan},
+ ActionFixTypos: codeActionHandler{build: buildFixTyposPlan},
ActionCustomPrompt: codeActionHandler{build: buildCustomPromptPlan},
}
}
@@ -304,6 +305,15 @@ func buildSimplifyPlan(parts InputParts, cfg actionConfig, client chatDoer, _ io
}, 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,
@@ -348,6 +358,12 @@ func handleSimplifyAction(ctx context.Context, parts InputParts, cfg actionConfi
})
}
+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
diff --git a/internal/hexaiaction/tui.go b/internal/hexaiaction/tui.go
index 549a6ab..749b30c 100644
--- a/internal/hexaiaction/tui.go
+++ b/internal/hexaiaction/tui.go
@@ -31,6 +31,7 @@ func newModel() model {
item{title: "Simplify and improve", desc: "", kind: ActionSimplify, hotkey: 'i'},
item{title: "Document code", desc: "", kind: ActionDocument, hotkey: 'c'},
item{title: "Generate Go unit test(s)", desc: "", kind: ActionGoTest, hotkey: 't'},
+ item{title: "Fix typos and improve grammar and clarity", desc: "", kind: ActionFixTypos, hotkey: 'f'},
item{title: "Custom prompt", desc: "", kind: ActionCustomPrompt, hotkey: 'p'},
item{title: "Skip", desc: "", kind: ActionSkip, hotkey: 's'},
}
@@ -81,7 +82,7 @@ func handleKey(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if n := len(m.list.Items()); n > 0 {
m.list.Select(n - 1)
}
- case "s", "r", "c", "t", "i", "p":
+ case "s", "r", "c", "t", "i", "f", "p":
items := m.list.Items()
for i := 0; i < len(items); i++ {
if it, ok := items[i].(item); ok && strings.ToLower(string(it.hotkey)) == low {
diff --git a/internal/hexaiaction/types.go b/internal/hexaiaction/types.go
index 8b4ba28..eb1f3f4 100644
--- a/internal/hexaiaction/types.go
+++ b/internal/hexaiaction/types.go
@@ -10,6 +10,7 @@ const (
ActionDocument ActionKind = "document"
ActionGoTest ActionKind = "gotest"
ActionSimplify ActionKind = "simplify"
+ ActionFixTypos ActionKind = "fix_typos"
// ActionCustom represents a configured custom action from the submenu.
ActionCustom ActionKind = "custom"
// ActionCustomPrompt is the free-form prompt opened in the editor (hotkey 'p').