diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-25 08:49:02 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-25 08:49:02 +0300 |
| commit | e33cd60277bc717ad7a8c73cf01c7bedad0d1837 (patch) | |
| tree | cf3e72b6e16223a207883c7049ae200ac57aee2d /internal | |
| parent | caa7876ac8bdb5cc23e12db4d4cb65cf86861151 (diff) | |
Release v0.35.0: fix-typos action and tmux popup for hexai-tmux-action
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/appconfig/app_sections.go | 2 | ||||
| -rw-r--r-- | internal/appconfig/config_load.go | 4 | ||||
| -rw-r--r-- | internal/appconfig/config_merge.go | 2 | ||||
| -rw-r--r-- | internal/appconfig/config_types.go | 4 | ||||
| -rw-r--r-- | internal/hexaiaction/cmdentry.go | 26 | ||||
| -rw-r--r-- | internal/hexaiaction/cmdentry_runcommand_test.go | 8 | ||||
| -rw-r--r-- | internal/hexaiaction/cmdentry_test.go | 22 | ||||
| -rw-r--r-- | internal/hexaiaction/prompts.go | 7 | ||||
| -rw-r--r-- | internal/hexaiaction/run.go | 16 | ||||
| -rw-r--r-- | internal/hexaiaction/tui.go | 3 | ||||
| -rw-r--r-- | internal/hexaiaction/types.go | 1 | ||||
| -rw-r--r-- | internal/tmux/tmux.go | 32 | ||||
| -rw-r--r-- | internal/version.go | 2 |
13 files changed, 97 insertions, 32 deletions
diff --git a/internal/appconfig/app_sections.go b/internal/appconfig/app_sections.go index 430ebce..5426f2a 100644 --- a/internal/appconfig/app_sections.go +++ b/internal/appconfig/app_sections.go @@ -90,6 +90,8 @@ type PromptConfig struct { PromptCodeActionGoTestUser string `json:"-"` PromptCodeActionSimplifySystem string `json:"-"` PromptCodeActionSimplifyUser string `json:"-"` + PromptCodeActionFixTyposSystem string `json:"-"` + PromptCodeActionFixTyposUser string `json:"-"` // CLI PromptCLIDefaultSystem string `json:"-"` PromptCLIExplainSystem string `json:"-"` diff --git a/internal/appconfig/config_load.go b/internal/appconfig/config_load.go index 4c6214c..88750fd 100644 --- a/internal/appconfig/config_load.go +++ b/internal/appconfig/config_load.go @@ -440,6 +440,8 @@ func applyPromptCodeAction(fc *fileConfig, out *App) { strings.TrimSpace(ca.GoTestUser) == "" && strings.TrimSpace(ca.SimplifySystem) == "" && strings.TrimSpace(ca.SimplifyUser) == "" && + strings.TrimSpace(ca.FixTyposSystem) == "" && + strings.TrimSpace(ca.FixTyposUser) == "" && len(ca.Custom) == 0 { return } @@ -453,6 +455,8 @@ func applyPromptCodeAction(fc *fileConfig, out *App) { setIfNotBlank(&out.PromptCodeActionGoTestUser, ca.GoTestUser) setIfNotBlank(&out.PromptCodeActionSimplifySystem, ca.SimplifySystem) setIfNotBlank(&out.PromptCodeActionSimplifyUser, ca.SimplifyUser) + setIfNotBlank(&out.PromptCodeActionFixTyposSystem, ca.FixTyposSystem) + setIfNotBlank(&out.PromptCodeActionFixTyposUser, ca.FixTyposUser) if len(ca.Custom) > 0 { out.CustomActions = append(out.CustomActions, toCustomActions(ca.Custom)...) } diff --git a/internal/appconfig/config_merge.go b/internal/appconfig/config_merge.go index d474fb7..e31e0dc 100644 --- a/internal/appconfig/config_merge.go +++ b/internal/appconfig/config_merge.go @@ -205,6 +205,8 @@ func mergeCodeActionPrompts(dst, src *App) { mergeStringField(&dst.PromptCodeActionGoTestUser, src.PromptCodeActionGoTestUser) mergeStringField(&dst.PromptCodeActionSimplifySystem, src.PromptCodeActionSimplifySystem) mergeStringField(&dst.PromptCodeActionSimplifyUser, src.PromptCodeActionSimplifyUser) + mergeStringField(&dst.PromptCodeActionFixTyposSystem, src.PromptCodeActionFixTyposSystem) + mergeStringField(&dst.PromptCodeActionFixTyposUser, src.PromptCodeActionFixTyposUser) } func mergeCLIPrompts(dst, src *App) { diff --git a/internal/appconfig/config_types.go b/internal/appconfig/config_types.go index 05befb2..54f9bcf 100644 --- a/internal/appconfig/config_types.go +++ b/internal/appconfig/config_types.go @@ -125,6 +125,8 @@ func defaultPromptConfig() PromptConfig { PromptCodeActionGoTestUser: "Function under test:\n{{function}}", PromptCodeActionSimplifySystem: "You are a precise code improvement engine. Simplify and improve the given code while preserving behavior. Return only the improved code with no prose or backticks.", PromptCodeActionSimplifyUser: "Improve this code:\n{{selection}}", + PromptCodeActionFixTyposSystem: "You are a precise proofreader. Fix all typos, spelling errors, and grammatical mistakes in the given text. Improve clarity and readability while preserving the original meaning, tone, and structure. Return only the corrected text with no prose or backticks.", + PromptCodeActionFixTyposUser: "Fix typos and improve grammar and clarity:\n{{selection}}", PromptCLIDefaultSystem: "You are Hexai CLI. Default to very short, concise answers. If the user asks for commands, output only the commands (one per line) with no commentary or explanation. Only when the word 'explain' appears in the prompt, produce a verbose explanation.", PromptCLIExplainSystem: "You are Hexai CLI. The user requested an explanation. Provide a clear, verbose explanation with reasoning and details. If commands are needed, include them with brief context.", @@ -322,6 +324,8 @@ type sectionPromptsCodeAction struct { GoTestUser string `toml:"go_test_user"` SimplifySystem string `toml:"simplify_system"` SimplifyUser string `toml:"simplify_user"` + FixTyposSystem string `toml:"fix_typos_system"` + FixTyposUser string `toml:"fix_typos_user"` Custom []sectionCustomAction `toml:"custom"` } 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'). diff --git a/internal/tmux/tmux.go b/internal/tmux/tmux.go index 6d75a44..9a4f5ee 100644 --- a/internal/tmux/tmux.go +++ b/internal/tmux/tmux.go @@ -53,6 +53,38 @@ func SplitRun(opts SplitOpts, argv []string) error { return c.Run() } +// PopupOpts controls how a tmux display-popup is created for running a command. +type PopupOpts struct { + Target string // optional pane target, e.g. ":." + Width string // popup width, e.g. "80%" or "120"; empty means tmux default + Height string // popup height, e.g. "80%" or "40"; empty means tmux default +} + +// PopupRun opens a tmux display-popup and runs argv inside it. +// The -E flag makes the popup close automatically when the command exits. +// It returns once the popup has closed (blocking call). +func PopupRun(opts PopupOpts, argv []string) error { + if len(argv) == 0 { + return nil + } + args := []string{"display-popup", "-E"} + if cwd, err := os.Getwd(); err == nil && cwd != "" { + args = append(args, "-d", cwd) + } + if strings.TrimSpace(opts.Target) != "" { + args = append(args, "-t", opts.Target) + } + if strings.TrimSpace(opts.Width) != "" { + args = append(args, "-w", strings.TrimSpace(opts.Width)) + } + if strings.TrimSpace(opts.Height) != "" { + args = append(args, "-h", strings.TrimSpace(opts.Height)) + } + cmdStr := shellJoin(argv) + args = append(args, cmdStr) + return command("tmux", args...).Run() +} + // shellJoin quotes argv elements for safe use in a single shell command string. // It avoids interpretation by wrapping in single quotes and escaping embedded single quotes. func shellJoin(argv []string) string { diff --git a/internal/version.go b/internal/version.go index 5e5bbba..f8f1502 100644 --- a/internal/version.go +++ b/internal/version.go @@ -1,4 +1,4 @@ // Package internal provides the Hexai semantic version identifier used by CLI and LSP binaries. package internal -const Version = "0.34.0" +const Version = "0.35.0" |
