diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-07 14:29:35 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-07 14:29:35 +0300 |
| commit | 23482b5d8da5c67da1fc501ddbafdd123be3972c (patch) | |
| tree | 452dc7c418055ebb79a88a303e50d2dbc1877f09 | |
| parent | 76f388f9759cdc15cb1eba985cd87cde1906208b (diff) | |
feat: rename hexai-action -> hexai-tmux-action; remove --tmux/--no-tmux; tmux-only flow; update docs and Magefile
| -rw-r--r-- | Magefile.go | 26 | ||||
| -rw-r--r-- | PROJECTSTATUS.md | 2 | ||||
| -rw-r--r-- | README.md | 9 | ||||
| -rw-r--r-- | TODO.md | 35 | ||||
| -rw-r--r-- | cmd/hexai-tmux-action/main.go (renamed from cmd/hexai-action/main.go) | 5 | ||||
| -rw-r--r-- | docs/configuration.md | 6 | ||||
| -rw-r--r-- | docs/go-unit-tests.md | 27 | ||||
| -rw-r--r-- | docs/source-structure.md | 79 | ||||
| -rw-r--r-- | docs/testing.md | 30 | ||||
| -rw-r--r-- | docs/usage.md | 8 | ||||
| -rw-r--r-- | internal/hexaiaction/cmdentry.go | 41 | ||||
| -rw-r--r-- | internal/hexaiaction/cmdentry_runcommand_test.go | 22 | ||||
| -rw-r--r-- | internal/hexaiaction/cmdentry_test.go | 23 | ||||
| -rw-r--r-- | internal/hexaiaction/run.go | 12 | ||||
| -rw-r--r-- | internal/hexaiaction/types.go | 2 |
15 files changed, 68 insertions, 259 deletions
diff --git a/Magefile.go b/Magefile.go index 16ff7c0..5c5be0b 100644 --- a/Magefile.go +++ b/Magefile.go @@ -17,16 +17,16 @@ import ( ) var ( - Default = Build // Default target: build all binaries. + Default = Build // Default target: build all binaries. coverageThreshold float64 = 85 coveragePrinted = make(chan struct{}, 1) ) -// Build builds the Hexai LSP and CLI binaries. +// Build builds binaries. func Build() error { - mg.Deps(BuildHexaiLSP, BuildHexaiCLI, BuildHexaiAction) - printCoverage() - return nil + mg.Deps(BuildHexaiLSP, BuildHexaiCLI, BuildHexaiTmuxAction) + printCoverage() + return nil } // BuildHexaiLSP builds the LSP server binary. @@ -41,10 +41,10 @@ func BuildHexaiCLI() error { return sh.RunV("go", "build", "-o", "hexai", "cmd/hexai/main.go") } -// BuildHexaiAction builds the hexai-action TUI binary. -func BuildHexaiAction() error { +// BuildHexaiTmuxAction builds the hexai-tmux-action TUI binary. +func BuildHexaiTmuxAction() error { printCoverage() - return sh.RunV("go", "build", "-o", "hexai-action", "cmd/hexai-action/main.go") + return sh.RunV("go", "build", "-o", "hexai-tmux-action", "cmd/hexai-tmux-action/main.go") } // Dev runs tests, vet, lint, then builds with race for both binaries. @@ -57,7 +57,7 @@ func Dev() error { if err := sh.RunV("go", "build", "-race", "-o", "hexai", "cmd/hexai/main.go"); err != nil { return err } - return sh.RunV("go", "build", "-race", "-o", "hexai-action", "cmd/hexai-action/main.go") + return sh.RunV("go", "build", "-race", "-o", "hexai-tmux-action", "cmd/hexai-tmux-action/main.go") } // Run launches the LSP server via go run (useful during development). @@ -97,14 +97,14 @@ func Install() error { if err := sh.RunV("cp", "-v", "./hexai", bin+"/"); err != nil { return err } - return sh.RunV("cp", "-v", "./hexai-action", bin+"/") + return sh.RunV("cp", "-v", "./hexai-tmux-action", bin+"/") } -// RunAction runs the hexai-action TUI via go run (reads stdin). -func RunAction() error { +// RunTmuxAction runs the hexai-tmux-action TUI via go run (reads stdin). +func RunTmuxAction() error { printCoverage() mg.Deps(Dev) - return sh.RunV("go", "run", "cmd/hexai-action/main.go") + return sh.RunV("go", "run", "cmd/hexai-tmux-action/main.go") } // printCoverage prints a warning if an existing coverage profile shows total < coverateThreshold. diff --git a/PROJECTSTATUS.md b/PROJECTSTATUS.md index 18b0278..a5687fa 100644 --- a/PROJECTSTATUS.md +++ b/PROJECTSTATUS.md @@ -15,7 +15,7 @@ Or maybe ``` [keys.normal] -C-p = ":sh hexai-action" +C-p = ":sh hexai-tmux-action" ``` And then generate a menu with all the code actions hexai-lsp knows of and include hotkeys for each menu item! Also print out a notice that this is a work-around due to limitations in Helix's current LSP UI. @@ -12,21 +12,20 @@ It has got improved capabilities for Go code understanding (for example, create * LSP Code actions * LSP in-editor chat with the LLM * Stand-alone command line tool for LLM interaction -* TUI code-action runner (`hexai-action`) with Bubble Tea +* TUI code-action runner (`hexai-tmux-action`) with Bubble Tea * Support for OpenAI, GitHub Copilot, and Ollama ## Documentation * [Configuration guide](docs/configuration.md) * [Usage examples](docs/usage.md) -* [Source structure](docs/source-structure.md) ## Build and tasks Hexai uses Mage for developer tasks. Install Mage, then run targets like build, dev, test, and install. - Install Mage: `go install github.com/magefile/mage@latest` -- Build binaries: `mage build` (produces `hexai`, `hexai-lsp`, and `hexai-action`) +- Build binaries: `mage build` (produces `hexai`, `hexai-lsp`, and `hexai-tmux-action`) - Dev build (+ tests, vet, lint): `mage dev` - Run tests: `mage test` - Run tests with coverage: `go test ./... -cover` @@ -43,5 +42,5 @@ Either use the Mage method as mentioned above, or install directly with: - CLI: `go install codeberg.org/snonux/hexai/cmd/hexai@latest` - LSP: `go install codeberg.org/snonux/hexai/cmd/hexai-lsp@latest` -- Action runner: `go install codeberg.org/snonux/hexai/cmd/hexai-action@latest` -Install: `mage install` (copies `hexai-action` to `GOPATH/bin` together with other binaries) +- Action runner: `go install codeberg.org/snonux/hexai/cmd/hexai-tmux-action@latest` +Install: `mage install` (copies `hexai-tmux-action` to `GOPATH/bin` together with other binaries) @@ -1,13 +1,13 @@ -Comprehensive plan: integrate Helix + tmux flow into hexai-action +Comprehensive plan: integrate Helix + tmux flow into hexai-tmux-action Summary of current setup -- Helix keybinding pipes selection to `ai`, which dispatches to `hx.hexai-action-prompt` for hexai-action mode. -- `hx.hexai-action-prompt` writes stdin to `~/.hx-action-input`, opens a tmux split-pane, runs `hexai-action -infile <input> -outfile <reply>.tmp`, then atomically renames to `<reply>` and prints the reply back to Helix. +- Helix keybinding pipes selection to the tmux action command. +- The tmux action writes stdin to a temp file, opens a tmux split-pane, runs `hexai-tmux-action -infile <input> -outfile <reply>.tmp`, then atomically renames to `<reply>` and prints the reply back to Helix. - This works but requires shell scripts and out-of-band temp files. Goal -- Helix should call hexai-action directly: `:pipe hexai-action`. -- hexai-action itself handles: reading stdin; presenting TUI in a tmux pane (when needed); executing the chosen action; printing the result to stdout for Helix to apply. +- Helix should call hexai-tmux-action directly: `:pipe hexai-tmux-action`. +- hexai-tmux-action itself handles: reading stdin; presenting TUI in a tmux pane; executing the chosen action; printing the result to stdout for Helix to apply. - Consolidate all tmux logic in a reusable `internal/tmux` package for future features. Proposed CLI/UX @@ -28,7 +28,7 @@ High-level design 1) IO orchestration (parent process) - Determine whether to show TUI inline or via tmux based on TTY detection and `-tmux`/`-no-tmux`. - Inline path: run current `hexaiaction.Run(ctx, in, out, err)` (unchanged behavior) and exit. - - Tmux path: write stdin to a secure temp file, spawn a tmux split-pane that executes a `hexai-action -ui-child -infile <in> -outfile <out>.tmp`, wait for completion (by process exit and/or file rename), then print `<out>` to stdout. + - Tmux path: write stdin to a secure temp file, spawn a tmux split-pane that executes a `hexai-tmux-action -ui-child -infile <in> -outfile <out>.tmp`, wait for completion (by process exit and/or file rename), then print `<out>` to stdout. 2) Child/TUI execution (`-ui-child`) - Read from `-infile`, parse input (diagnostics + selection), construct LLM client, show Bubble Tea menu, run selected action, write result to `-outfile.tmp`, fsync, rename to `-outfile`. @@ -47,7 +47,7 @@ High-level design - Implementation details: - Shell out to `tmux` (no lib dep). Build command like: `tmux split-window -v -p 33 "<cmd>"`. - Quote/escape argv safely. Prefer `exec.Command` for the child in a shell wrapper, or join argv for `tmux`’s command string. - - Avoid writing to `~/.hx-*`; use `os.CreateTemp("", "hexai-action-*" )` under `$TMPDIR`. + - Avoid writing to `~/.hx-*`; use `os.CreateTemp("", "hexai-tmux-action-*" )` under `$TMPDIR`. 4) hexaiaction refactor (internal package) - Separate concerns to keep functions small/testable: @@ -61,15 +61,14 @@ High-level design - Timeouts: child actions already use short timeouts; parent wait for outfile should have a reasonable deadline (e.g., 60s) to avoid hanging Helix. - Atomic writes: write to `outfile.tmp`, `Sync`, then `Rename` for a clear completion signal. - Cleanup: always remove temp files (defer and signal handling for SIGINT/SIGTERM). - - Logging: log to stderr with clear `hexai-action` prefixes; keep stdout clean for Helix’s `:pipe`. + - Logging: log to stderr with clear `hexai-tmux-action` prefixes; keep stdout clean for Helix’s `:pipe`. Helix configuration after change - Replace the current keybinding pipeline with a single call: - - `C-a = ":pipe hexai-action"` -- Optional: users can force tmux pane with `:pipe hexai-action -tmux` or disable with `:pipe hexai-action -no-tmux` if auto-detection does not fit their setup. + - `C-a = ":pipe hexai-tmux-action"` Migration plan -1) Implement tmux package and integrate auto-mode in `cmd/hexai-action/main.go`. +1) Implement tmux package and integrate auto-mode in `cmd/hexai-tmux-action/main.go`. 2) Keep legacy flags (`-infile`, `-outfile`) for compatibility and tests. 3) Update README and docs to show new Helix keybinding and describe flags. 4) Mark shell scripts (`llminputs/ai`, `llminputs/hx.hexai-action-prompt`) as deprecated in repo notes; retain them temporarily. @@ -78,7 +77,7 @@ Migration plan Testing plan - Unit tests: - `internal/tmux`: mock `exec.Command` via a small command-runner interface; verify command assembly and availability checks. - - `internal/hexaiaction`: tests for parent decision logic (TTY vs pipe; tmux available vs not) using injectable detectors. + - `internal/hexaiaction`: tests for tmux split orchestration and child flow. - hexaiaction: existing tests continue to pass; add tests for non-interactive fallback behavior when no TTY. - Integration tests (manual or scripted): - Run under tmux: verify a pane opens, TUI choice is applied, stdout contains result. @@ -93,7 +92,7 @@ Edge cases and mitigations Implementation steps (incremental) 1) Add `internal/tmux` with `Available`, `SplitRun`, and helpers. -2) Add TTY detection helper to `internal/hexaiaction` and wire flags (`-tmux`, `-no-tmux`, `-ui-child`). +2) Wire flags (`-ui-child`, `-tmux-target`, `-tmux-split`, `-tmux-percent`). 3) Parent flow: detect mode, manage temp files, spawn child via tmux when selected, wait/print result. 4) Child flow: reuse existing `hexaiaction.Run` to keep logic centralized; ensure outfile atomic write. 5) Docs: update README with new Helix config and flags. @@ -101,22 +100,22 @@ Implementation steps (incremental) Notes on code organization - Place all tmux-related code under `internal/tmux` and keep functions well under 50 lines. -- Keep command entrypoint (`cmd/hexai-action/main.go`) small and focused on wiring/mode selection. +- Keep command entrypoint (`cmd/hexai-tmux-action/main.go`) small and focused on wiring/mode selection. - Avoid duplication across `hexaiaction` and `tmux`; IO/file and action logic remain in `hexaiaction`. Outcome -- One-step Helix integration (`:pipe hexai-action`). +- One-step Helix integration (`:pipe hexai-tmux-action`). - No helper scripts required; cross-platform friendly with graceful fallbacks. - Reusable tmux utilities for future features. Progress - [x] Add `internal/tmux` with `Available`, `SplitRun`, quoting helpers. -- [x] Wire flags in `hexai-action`: `-tmux`, `-no-tmux`, `-ui-child`, `-tmux-target`, `-tmux-split`, `-tmux-percent`. +- [x] Wire flags in tmux action: `-ui-child`, `-tmux-target`, `-tmux-split`, `-tmux-percent`. - [x] Parent tmux orchestration: write stdin to temp, split tmux, wait for outfile, print to stdout. - [x] Child mode: atomic `outfile.tmp` write and rename, with error echo fallback. -- [x] Unit tests for `internal/tmux` and tmux decision logic in `hexai-action` (validate locally; target ≥85% coverage for new code). +- [x] Unit tests for `internal/tmux` and tmux orchestration in action (validate locally; target ≥85% coverage for new code). - [x] Update README/docs for new Helix keybinding and flags. - [ ] Delete legacy helper scripts (`llminputs/ai`, `llminputs/hx.hexai-action-prompt`) when ready; no deprecation notice. - [x] Ran coverage locally. Notes: - `mage coverage` now passes (HTML at docs/coverage.html). Total cross-package coverage ≈ 84%. - - New package `internal/tmux` is ≥85% covered. The `hexai-action` entrypoint package sits ~69% overall; newly added helper paths are covered (openIO, runChild, runInTmuxParent, echoThrough, waitForFile, etc.). The inline TTY UI path and `main()` remain intentionally untested. + - New package `internal/tmux` is ≥85% covered. The action entrypoint package sits ~69% overall; newly added helper paths are covered (openIO, runChild, runInTmuxParent, etc.). The `main()` remains intentionally untested. diff --git a/cmd/hexai-action/main.go b/cmd/hexai-tmux-action/main.go index b796cbd..02cfe09 100644 --- a/cmd/hexai-action/main.go +++ b/cmd/hexai-tmux-action/main.go @@ -12,8 +12,6 @@ import ( func main() { infile := flag.String("infile", "", "Read input from this file instead of stdin") outfile := flag.String("outfile", "", "Write output to this file instead of stdout") - forceTmux := flag.Bool("tmux", false, "Force running the UI in a tmux split-pane (auto if not set)") - noTmux := flag.Bool("no-tmux", false, "Disable tmux mode even if available") uiChild := flag.Bool("ui-child", false, "INTERNAL: run interactive UI and write to -outfile atomically") tmuxTarget := flag.String("tmux-target", "", "tmux split target (advanced)") tmuxSplit := flag.String("tmux-split", "v", "tmux split orientation: v or h") @@ -22,8 +20,7 @@ func main() { opts := hexaiaction.Options{ Infile: *infile, Outfile: *outfile, - ForceTmux: *forceTmux, NoTmux: *noTmux, UIChild: *uiChild, - TmuxTarget: *tmuxTarget, TmuxSplit: *tmuxSplit, TmuxPercent: *tmuxPercent, + UIChild: *uiChild, TmuxTarget: *tmuxTarget, TmuxSplit: *tmuxSplit, TmuxPercent: *tmuxPercent, } if err := hexaiaction.RunCommand(context.Background(), opts, os.Stdin, os.Stdout, os.Stderr); err != nil { fmt.Fprintln(os.Stderr, err) diff --git a/docs/configuration.md b/docs/configuration.md index dc4adbd..09ff519 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -42,11 +42,11 @@ Hexai Action (TUI) configuration This is mostly useful when Helix runs in a [tmux](https://tmux.github.io/) session! -- Helix integration (recommended): bind a key to pipe the current selection to `hexai-action` and replace it with the output. - - Example: `C-a = ":pipe hexai-action"` +- Helix integration (recommended): bind a key to pipe the current selection to `hexai-tmux-action` and replace it with the output. + - Example: `C-a = ":pipe hexai-tmux-action"` - Default behavior: - Inline TUI when run in a real terminal (TTY). - - When invoked via Helix `:pipe` and a tmux session is available, `hexai-action` opens a split pane to render the menu and returns the result on stdout for Helix to apply. + - When invoked via Helix `:pipe`, `hexai-tmux-action` opens a split pane to render the menu and returns the result on stdout for Helix to apply. - If no TTY and no tmux are available, it falls back to echoing the input. - Flags: - `--infile` Read input from the given file instead of stdin. diff --git a/docs/go-unit-tests.md b/docs/go-unit-tests.md deleted file mode 100644 index 1be9fd0..0000000 --- a/docs/go-unit-tests.md +++ /dev/null @@ -1,27 +0,0 @@ -# Go unit tests via code action - -Hexai can generate Go unit tests for the function at your cursor. - -- Scope: Available only for Go source files ending with `.go` (not `_test.go`). -- Trigger: Use your editor's code actions on the current selection/position and pick "Implement unit test". - -What happens - -- Function detection: Hexai finds the nearest `func` definition above the cursor and captures the function body by balancing braces. -- Test generation: - - If an LLM provider is configured, Hexai asks it to generate one or more `Test*` functions using the `testing` package. The provider must return only the test function code (no package/import lines). - - If no provider is configured or the request fails, Hexai inserts a small stub test `Test<Name>` with a TODO. -- File handling and navigation: - - If `<file>_test.go` exists, the test function is appended to the end of that file. - - If it does not exist, Hexai creates it, writing `package <pkg>` (inferred from the source file) and `import "testing"`, followed by the generated test function(s). - - After applying the edit, Hexai asks the editor to focus the test file and place the cursor at the start of the newly added test function. - -Notes and limitations - -- Imports on append: when appending to an existing test file, Hexai assumes `testing` is available. If not, add `import "testing"` to the test file and re-run `go test`. -- Method names: for methods with receivers, test names default to `TestMethod` (stub fallback). Future improvement may generate `TestType_Method` automatically. -- Formatting: run `go fmt ./...` or your editor's formatter to normalize whitespace if needed. - -Examples - -In Helix, position the cursor inside a function and invoke code actions; choose "Implement unit test". Hexai will create or update `<file>_test.go` accordingly. diff --git a/docs/source-structure.md b/docs/source-structure.md deleted file mode 100644 index 0c7f56f..0000000 --- a/docs/source-structure.md +++ /dev/null @@ -1,79 +0,0 @@ -# Source code structure - -This document provides a high‑level map of the Hexai source layout and how the -main packages relate to each other. - -## Diagram - -```mermaid -graph TD - %% Entrypoints - A[cmd/hexai\nCLI entrypoint] --> B[internal/hexaicli\nCLI runner] - C[cmd/hexai-lsp\nLSP entrypoint] --> D[internal/hexailsp\nLSP runner] - - %% Shared/internal packages - subgraph internal/ - I[appconfig\nLoad config from file/env] - L[llm\nClient + providers] - G[logging\nBound logger + helpers] - S[lsp\nJSON-RPC, server, handlers] - end - - %% Relationships - B --> I - B --> L - B --> G - - D --> I - D --> L - D --> G - D --> S - - S --> L - S --> G - - %% LLM providers - subgraph internal/llm - P1[openai.go] - P2[ollama.go] - P3[copilot.go] - end - L --> P1 - L --> P2 - L --> P3 - - %% Version info - V[internal/version.go\nVersion string] --> A - V --> C -``` - -## Module overview - -- cmd/hexai: CLI binary that parses flags, prints version via `internal.Version`, - and delegates to `internal/hexaicli.Run`. -- cmd/hexai-lsp: LSP server binary that parses flags, prints version, and calls - `internal/hexailsp.Run` (stdio JSON‑RPC server). -- internal/hexaicli: CLI flow — reads stdin/args, loads config, builds an LLM - client, constructs messages, and runs a single chat request (streaming when - supported). -- internal/hexailsp: LSP orchestration — binds logging, loads config, builds the - LLM client, constructs `internal/lsp.ServerOptions`, and runs the server. -- internal/lsp: Minimal LSP over stdio — document store, JSON‑RPC handlers - (initialize, completion, code action, etc.), context building, and a small - completion cache. -- internal/llm: Provider‑agnostic client interface plus concrete providers for - OpenAI, GitHub Copilot, and Ollama, including streaming support where - available. -- internal/appconfig: Loads user configuration from file and environment, shared - by both CLI and LSP paths. -- internal/logging: Central logger binding and small helpers for consistent, - readable logs and chat summaries. -- internal/version.go: Single place for the version string used by both - binaries. - -## Typical flows - -- CLI: `cmd/hexai` → `internal/hexaicli` → `internal/appconfig` → `internal/llm` - (providers) → print output and a short summary line. -- LSP: `cmd/hexai-lsp` → `internal/hexailsp` → `internal/lsp.Server` → - request handlers → `internal/llm` for completions/actions. diff --git a/docs/testing.md b/docs/testing.md deleted file mode 100644 index 86d88b1..0000000 --- a/docs/testing.md +++ /dev/null @@ -1,30 +0,0 @@ -# Testing Guide - -This repository includes a growing test suite designed to be realistic and robust. - -Key patterns: - -- Table‑driven tests: consolidate repetitive scenarios into concise tables (see `internal/lsp/*_table_test.go`). -- Shared fixtures: use `internal/testutil/fixtures.go` for multi‑line docblocks, chat replies, function suggestions, and markdown fences. -- Provider mocks: use `httptest.Server` and/or custom `http.RoundTripper` to simulate OpenAI/Copilot/Ollama responses, including success, stream (SSE), and error cases. -- E2E LSP tests: capture JSON‑RPC frames from the in‑memory server (`captureResponse`, `captureRequest`) and validate code actions, resolves, and chat edits. - -Suggested additions: - -- Expand table‑driven coverage for completion edit computations and label/filter selection. -- Add more negative tests (malformed SSE/JSON payloads) to assert robust error handling. - -## Running Tests - -- Full suite with coverage: - - `HEXAI_TEST_SKIP_NET=1 go test ./... -cover` - - The `HEXAI_TEST_SKIP_NET=1` env var disables any tests that require network access, keeping runs deterministic in CI/sandboxes. - -- Package-specific runs: - - `HEXAI_TEST_SKIP_NET=1 go test ./internal/hexaiaction -cover` - - `HEXAI_TEST_SKIP_NET=1 go test ./internal/hexaiaction -cover` - -Notes - -- Some environments restrict writes to the Go build cache; if you see cache permission errors, re-run in a less-restricted shell or allow the command to write to the cache. -- Always format Go code before committing: `gofumpt -w .` diff --git a/docs/usage.md b/docs/usage.md index 05d6c55..fb65596 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -105,7 +105,7 @@ hexai 'install ripgrep on macOS and explain' ## Hexai Action (TUI) -`hexai-action` runs code actions over a selection or diagnostics+selection piped from stdin, or read from a file. +`hexai-tmux-action` runs code actions over a selection or diagnostics+selection piped from stdin, or read from a file. - Choose an action with arrow keys, `j/k`, `g/G`, Enter, or hotkeys `[s] [r] [c] [t]`. - Output is written to stdout by default, or to a file via `--outfile`. @@ -122,11 +122,11 @@ Examples ```sh # From stdin -cat input.go | hexai-action +cat input.go | hexai-tmux-action # From file to file -hexai-action --infile input.go --outfile output.go +hexai-tmux-action --infile input.go --outfile output.go # Using shell redirection -hexai-action < input.go > output.go +hexai-tmux-action < input.go > output.go ``` diff --git a/internal/hexaiaction/cmdentry.go b/internal/hexaiaction/cmdentry.go index 1947390..cf72495 100644 --- a/internal/hexaiaction/cmdentry.go +++ b/internal/hexaiaction/cmdentry.go @@ -12,52 +12,32 @@ import ( "golang.org/x/term" ) -// Options configures the command-line orchestration for hexai-action. +// Options configures the command-line orchestration for hexai-tmux-action. type Options struct { Infile string Outfile string - ForceTmux bool - NoTmux bool UIChild bool TmuxTarget string TmuxSplit string // "v" or "h" TmuxPercent int // 1-100 } -// RunCommand is the CLI orchestrator used by cmd/hexai-action. It decides whether -// to run inline, in a tmux split pane, or in child mode; then delegates to Run. +// RunCommand is the CLI orchestrator used by cmd/hexai-tmux-action. It runs in tmux +// split-pane mode by default, or child mode when -ui-child is set. func RunCommand(ctx context.Context, opts Options, stdin io.Reader, stdout, stderr io.Writer) error { if opts.UIChild { return runChild(ctx, opts.Infile, opts.Outfile, stdout, stderr) } - if shouldRunInTmux(opts.ForceTmux, opts.NoTmux) { - return runInTmuxParent(stdin, stdout, opts.TmuxTarget, opts.TmuxSplit, opts.TmuxPercent) - } - // Inline path: only if we have a TTY for UI; otherwise echo input - if isTTYFn(os.Stdout.Fd()) && isTTYFn(os.Stdin.Fd()) { - in, out, closeIn, closeOut, err := openIO(opts.Infile, opts.Outfile) - if err != nil { return err } - defer closeIn(); defer closeOut() - return Run(ctx, in, out, stderr) - } - // Fallback: echo - return echoThrough(opts.Infile, opts.Outfile, stdin, stdout) + // Always use tmux path + return runInTmuxParent(stdin, stdout, opts.TmuxTarget, opts.TmuxSplit, opts.TmuxPercent) } // seams for unit tests var isTTYFn = func(fd uintptr) bool { return term.IsTerminal(int(fd)) } -var tmuxAvailableFn = tmux.Available var splitRunFn = tmux.SplitRun var osExecutableFn = os.Executable var runFn = Run -func shouldRunInTmux(forceTmux, noTmux bool) bool { - if noTmux { return false } - if forceTmux { return true } - if !(isTTYFn(os.Stdin.Fd()) && isTTYFn(os.Stdout.Fd())) && tmuxAvailableFn() { return true } - return false -} - // openIO returns readers/writers for infile/outfile flags with deferred closers. func openIO(infile, outfile string) (io.Reader, io.Writer, func(), func(), error) { in := io.Reader(os.Stdin) @@ -66,13 +46,13 @@ func openIO(infile, outfile string) (io.Reader, io.Writer, func(), func(), error closeOut := func() {} if path := infile; path != "" { f, err := os.Open(path) - if err != nil { return nil, nil, func(){}, func(){}, fmt.Errorf("hexai-action: cannot open infile: %w", err) } + if err != nil { return nil, nil, func(){}, func(){}, fmt.Errorf("hexai-tmux-action: cannot open infile: %w", err) } in = f closeIn = func() { _ = f.Close() } } if path := outfile; path != "" { f, err := os.Create(path) - if err != nil { return nil, nil, func(){}, func(){}, fmt.Errorf("hexai-action: cannot open outfile: %w", err) } + if err != nil { return nil, nil, func(){}, func(){}, fmt.Errorf("hexai-tmux-action: cannot open outfile: %w", err) } out = f closeOut = func() { _ = f.Close() } } @@ -99,7 +79,7 @@ func runChild(ctx context.Context, infile, outfile string, stdout, stderr io.Wri if err := runFn(ctx, in, out, stderr); err != nil { closeOut() if copyErr := echoThrough(infile, tmp, os.Stdin, stdout); copyErr != nil { - return fmt.Errorf("hexai-action child: %v; echo failed: %v", err, copyErr) + return fmt.Errorf("hexai-tmux-action child: %v; echo failed: %v", err, copyErr) } } else { closeOut() @@ -108,7 +88,7 @@ func runChild(ctx context.Context, infile, outfile string, stdout, stderr io.Wri } func runInTmuxParent(stdin io.Reader, stdout io.Writer, target, split string, percent int) error { - dir, err := os.MkdirTemp("", "hexai-action-") + dir, err := os.MkdirTemp("", "hexai-tmux-action-") if err != nil { return err } defer func() { _ = os.RemoveAll(dir) }() inPath := filepath.Join(dir, "input.txt") @@ -135,7 +115,7 @@ func waitForFile(path string, timeout time.Duration) error { deadline := time.Now().Add(timeout) for { if _, err := os.Stat(path); err == nil { return nil } - if time.Now().After(deadline) { return fmt.Errorf("hexai-action: timeout waiting for reply file") } + if time.Now().After(deadline) { return fmt.Errorf("hexai-tmux-action: timeout waiting for reply file") } time.Sleep(200 * time.Millisecond) } } @@ -148,6 +128,7 @@ func catFileTo(w io.Writer, path string) error { return err } +// echoThrough no longer used in tmux-only flow, but kept for potential reuse. func echoThrough(infile, outfile string, stdin io.Reader, stdout io.Writer) error { var in io.Reader = stdin var out io.Writer = stdout diff --git a/internal/hexaiaction/cmdentry_runcommand_test.go b/internal/hexaiaction/cmdentry_runcommand_test.go index 7c8aa5c..092e43b 100644 --- a/internal/hexaiaction/cmdentry_runcommand_test.go +++ b/internal/hexaiaction/cmdentry_runcommand_test.go @@ -6,7 +6,6 @@ import ( "io" "os" "path/filepath" - "strings" "testing" "codeberg.org/snonux/hexai/internal/tmux" @@ -30,12 +29,10 @@ func TestRunCommand_UIChild(t *testing.T) { func TestRunCommand_Tmux(t *testing.T) { oldTTY := isTTYFn - oldAvail := tmuxAvailableFn oldExec := osExecutableFn oldSplit := splitRunFn isTTYFn = func(_ uintptr) bool { return false } - tmuxAvailableFn = func() bool { return true } - osExecutableFn = func() (string, error) { return "/bin/hexai-action", nil } + osExecutableFn = func() (string, error) { return "/bin/hexai-tmux-action", nil } splitRunFn = func(_ tmux.SplitOpts, argv []string) error { for i := 0; i < len(argv)-1; i++ { if argv[i] == "-outfile" && i+1 < len(argv) { @@ -45,9 +42,9 @@ func TestRunCommand_Tmux(t *testing.T) { } return nil } - defer func(){ isTTYFn = oldTTY; tmuxAvailableFn = oldAvail; osExecutableFn = oldExec; splitRunFn = oldSplit }() + defer func(){ isTTYFn = oldTTY; osExecutableFn = oldExec; splitRunFn = oldSplit }() var out bytes.Buffer - if err := RunCommand(context.Background(), Options{ForceTmux: true}, bytes.NewBufferString("X"), &out, io.Discard); err != nil { + if err := RunCommand(context.Background(), Options{}, bytes.NewBufferString("X"), &out, io.Discard); err != nil { t.Fatalf("RunCommand tmux: %v", err) } if out.String() != "OUT" { t.Fatalf("stdout: %q", out.String()) } @@ -56,15 +53,4 @@ func TestRunCommand_Tmux(t *testing.T) { // Inline TTY path is exercised implicitly via other helpers; testing it directly // would require TTY simulation which is brittle in unit tests. -func TestRunCommand_FallbackEcho(t *testing.T) { - oldTTY := isTTYFn - oldAvail := tmuxAvailableFn - isTTYFn = func(_ uintptr) bool { return false } - tmuxAvailableFn = func() bool { return false } - defer func(){ isTTYFn = oldTTY; tmuxAvailableFn = oldAvail }() - var out bytes.Buffer - if err := RunCommand(context.Background(), Options{NoTmux: true}, bytes.NewBufferString("Z"), &out, io.Discard); err != nil { - t.Fatalf("RunCommand fallback: %v", err) - } - if strings.TrimSpace(out.String()) != "Z" { t.Fatalf("stdout: %q", out.String()) } -} +// Fallback echo path removed in tmux-only flow. diff --git a/internal/hexaiaction/cmdentry_test.go b/internal/hexaiaction/cmdentry_test.go index 8525f7d..de8b5dd 100644 --- a/internal/hexaiaction/cmdentry_test.go +++ b/internal/hexaiaction/cmdentry_test.go @@ -13,24 +13,7 @@ import ( "codeberg.org/snonux/hexai/internal/tmux" ) -func TestShouldRunInTmux_Preferences(t *testing.T) { - if shouldRunInTmux(false, true) { t.Fatal("expected false when no-tmux is set") } - if !shouldRunInTmux(true, false) { t.Fatal("expected true when -tmux is set") } -} - -func TestShouldRunInTmux_Auto(t *testing.T) { - oldIsTTY := isTTYFn - oldAvail := tmuxAvailableFn - t.Cleanup(func() { isTTYFn = oldIsTTY; tmuxAvailableFn = oldAvail }) - isTTYFn = func(_ uintptr) bool { return false } - tmuxAvailableFn = func() bool { return true } - if !shouldRunInTmux(false, false) { t.Fatal("expected true when not TTY and tmux available") } - isTTYFn = func(_ uintptr) bool { return true } - if shouldRunInTmux(false, false) { t.Fatal("expected false when TTY present") } - isTTYFn = func(_ uintptr) bool { return false } - tmuxAvailableFn = func() bool { return false } - if shouldRunInTmux(false, false) { t.Fatal("expected false when tmux unavailable") } -} +// tmux-only flow: decision helpers removed. func TestPersistStdin_WritesFile(t *testing.T) { dir := t.TempDir() @@ -78,7 +61,7 @@ func TestRunInTmuxParent_Stubbed(t *testing.T) { rout, wout, _ := os.Pipe() oldExec := osExecutableFn oldSplit := splitRunFn - osExecutableFn = func() (string, error) { return "/bin/hexai-action", nil } + osExecutableFn = func() (string, error) { return "/bin/hexai-tmux-action", nil } splitRunFn = func(opts tmux.SplitOpts, argv []string) error { for i := 0; i < len(argv)-1; i++ { if argv[i] == "-outfile" && i+1 < len(argv) { @@ -106,7 +89,7 @@ func TestRunInTmuxParent_ExecutableError(t *testing.T) { func TestRunInTmuxParent_SplitError(t *testing.T) { oldExec := osExecutableFn - osExecutableFn = func() (string, error) { return "/bin/hexai-action", nil } + 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 }) diff --git a/internal/hexaiaction/run.go b/internal/hexaiaction/run.go index 609dad1..a8d4243 100644 --- a/internal/hexaiaction/run.go +++ b/internal/hexaiaction/run.go @@ -12,26 +12,26 @@ import ( "codeberg.org/snonux/hexai/internal/llmutils" ) -// Run executes the hexai-action command flow. +// Run executes the hexai-tmux-action command flow. // seams for testability var chooseActionFn = RunTUI var newClientFromApp = llmutils.NewClientFromApp func Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error { - logger := log.New(stderr, "hexai-action ", log.LstdFlags|log.Lmsgprefix) + logger := log.New(stderr, "hexai-tmux-action ", log.LstdFlags|log.Lmsgprefix) cfg := appconfig.Load(logger) client, err := newClientFromApp(cfg) if err != nil { - fmt.Fprintf(stderr, logging.AnsiBase+"hexai-action: LLM disabled: %v"+logging.AnsiReset+"\n", err) + fmt.Fprintf(stderr, logging.AnsiBase+"hexai-tmux-action: LLM disabled: %v"+logging.AnsiReset+"\n", err) return err } parts, err := ParseInput(stdin) if err != nil { - fmt.Fprintln(stderr, logging.AnsiBase+"hexai-action: failed to read input"+logging.AnsiReset) + fmt.Fprintln(stderr, logging.AnsiBase+"hexai-tmux-action: failed to read input"+logging.AnsiReset) return err } if strings.TrimSpace(parts.Selection) == "" { - return fmt.Errorf("hexai-action: no input provided on stdin") + return fmt.Errorf("hexai-tmux-action: no input provided on stdin") } kind, err := chooseActionFn() if err != nil { @@ -52,7 +52,7 @@ func executeAction(ctx context.Context, kind ActionKind, parts InputParts, cfg a case ActionRewrite: instr, cleaned := ExtractInstruction(parts.Selection) if strings.TrimSpace(instr) == "" { - fmt.Fprintln(stderr, logging.AnsiBase+"hexai-action: no inline instruction found; echoing input"+logging.AnsiReset) + fmt.Fprintln(stderr, logging.AnsiBase+"hexai-tmux-action: no inline instruction found; echoing input"+logging.AnsiReset) return parts.Selection, nil } cctx, cancel := timeout10s(ctx) diff --git a/internal/hexaiaction/types.go b/internal/hexaiaction/types.go index 5e01cfc..2dc918f 100644 --- a/internal/hexaiaction/types.go +++ b/internal/hexaiaction/types.go @@ -1,6 +1,6 @@ package hexaiaction -// Summary: Core types and constants for hexai-action. +// Summary: Core types and constants for hexai-tmux-action. type ActionKind string |
