From 75cf6abd55bfb60324fc47cf91eac08dbb8b87b4 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 8 Sep 2025 12:02:40 +0300 Subject: docs: move tmux documentation to its own file --- Magefile.go | 22 +- PROJECTSTATUS.md | 8 +- README.md | 26 +- cmd/hexai-lsp/main_test.go | 31 +- cmd/hexai-tmux-action/main.go | 41 +- cmd/hexai/main_test.go | 32 +- docs/configuration.md | 19 +- docs/coverage.html | 89 +- docs/coverage.out | 10618 +++++++++++---------- docs/helix-tmux-quickstart.md | 68 - docs/tmux.md | 109 + internal/appconfig/config.go | 84 +- internal/appconfig/config_test.go | 38 +- internal/editor/editor.go | 102 +- internal/editor/editor_test.go | 63 +- internal/hexaiaction/cmdentry.go | 252 +- internal/hexaiaction/cmdentry_runcommand_test.go | 87 +- internal/hexaiaction/cmdentry_test.go | 250 +- internal/hexaiaction/custom_action_test.go | 59 +- internal/hexaiaction/parse.go | 8 +- internal/hexaiaction/parse_test.go | 2 + internal/hexaiaction/prompts_more_test.go | 23 +- internal/hexaiaction/run_more_test.go | 35 +- internal/hexaiaction/run_seam_test.go | 56 +- internal/hexaiaction/run_test.go | 70 +- internal/hexaiaction/tui.go | 114 +- internal/hexaiaction/tui_delegate.go | 36 +- internal/hexaiaction/tui_delegate_test.go | 42 +- internal/hexaiaction/tui_test.go | 74 +- internal/hexaiaction/types.go | 14 +- internal/hexaicli/editor_integration_test.go | 83 +- internal/hexaicli/run_more_test.go | 51 +- internal/hexailsp/run_more_test.go | 63 +- internal/llm/provider_more2_test.go | 13 +- internal/llmutils/client.go | 51 +- internal/llmutils/client_test.go | 33 +- internal/logging/logging_test.go | 33 +- internal/lsp/codeaction_gotest_int_test.go | 37 +- internal/lsp/coverage_add_test.go | 156 +- internal/lsp/handlers.go | 112 +- internal/lsp/handlers_codeaction.go | 110 +- internal/lsp/handlers_completion.go | 9 + internal/lsp/handlers_document.go | 2 +- internal/lsp/handlers_init_more_test.go | 15 +- internal/lsp/server.go | 28 +- internal/testutil/fixtures_test.go | 9 +- internal/textutil/textutil.go | 192 +- internal/textutil/textutil_test.go | 130 +- internal/tmux/tmux.go | 18 +- internal/tmux/tmux_test.go | 135 +- 50 files changed, 7048 insertions(+), 6704 deletions(-) delete mode 100644 docs/helix-tmux-quickstart.md create mode 100644 docs/tmux.md diff --git a/Magefile.go b/Magefile.go index 5c5be0b..b297a9d 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 binaries. func Build() error { - mg.Deps(BuildHexaiLSP, BuildHexaiCLI, BuildHexaiTmuxAction) - printCoverage() - return nil + mg.Deps(BuildHexaiLSP, BuildHexaiCLI, BuildHexaiTmuxAction) + printCoverage() + return nil } // BuildHexaiLSP builds the LSP server binary. @@ -43,8 +43,8 @@ func BuildHexaiCLI() error { // BuildHexaiTmuxAction builds the hexai-tmux-action TUI binary. func BuildHexaiTmuxAction() error { - printCoverage() - return sh.RunV("go", "build", "-o", "hexai-tmux-action", "cmd/hexai-tmux-action/main.go") + printCoverage() + 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-tmux-action", "cmd/hexai-tmux-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-tmux-action", bin+"/") + return sh.RunV("cp", "-v", "./hexai-tmux-action", bin+"/") } // 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-tmux-action/main.go") + printCoverage() + mg.Deps(Dev) + 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 41a634a..9991645 100644 --- a/PROJECTSTATUS.md +++ b/PROJECTSTATUS.md @@ -1,19 +1,13 @@ # Project status -This documents shows future items and in progress items. Already completed ones are removed from this document as we go. +This document shows future items and items in progress. Already completed ones are deleted from this document as updates occur. ## Features -* [ ] tmux or helix status line updates with LLM progress/stats? -* [/] EDITOR support for custom action in hexai-tmux-action - * Verify documentation is correct -* [/] EDITOR support for hexai when no args given - * Verify documentation is correct * [ ] In-editor chat triggers should be context aware of the current file, buffer and function! * [ ] Kagi FastGPT for in-editor search - Think about an in-editor chat trigger, maybe with S> for search! * [ ] Test whethe GitHub Copilot support actually works now, and if not, fix it! -* [ ] Fish and zsh and bash shell integration for command completion? Have access to the current shell history (N last commands), current directory content, and current directory name. * [ ] Be able to re-configure the temperature in-editor * [ ] Be able to switch LLMs. diff --git a/README.md b/README.md index 32ce84f..a2b1836 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ It has got improved capabilities for Go code understanding (for example, create * [Configuration guide](docs/configuration.md) * [Usage examples](docs/usage.md) -* [Helix + tmux quickstart](docs/helix-tmux-quickstart.md) +* [Helix + tmux quickstart](docs/tmux.md) ## Build and tasks @@ -53,26 +53,4 @@ Either use the Mage method as mentioned above, or install directly with: ## Tmux Status Line -Hexai can surface live progress in tmux's status line via a user option. Add this to your `~/.tmux.conf`: - -``` -set -g status-right '#{@hexai_status} #[fg=colour8]| %H:%M' -``` - -- Note: `colour8` is typically “bright black” (a dim grey) in many themes. - If it’s low-contrast on your background, change it (e.g., `colour7` or `white`). - -- CLI updates `@hexai_status` at start (⏳ provider:model) and on completion with compact stats (↑sent, ↓recv, rpm, reqs). -- LSP emits an initial heartbeat on client initialize and periodic compact stats (provider, model, rpm, reqs, bytes). -- The TUI action runner sets a ready heartbeat and a completion heartbeat with stats. -- Toggle with `HEXAI_TMUX_STATUS=0` to disable (enabled by default). - -The status segment supports simple theming: - -- Preset themes: - - `HEXAI_TMUX_STATUS_THEME=white-on-purple` (white fg on purple/magenta bg) - - `HEXAI_TMUX_STATUS_THEME=black-on-yellow` (black fg on yellow bg) -- Explicit colors: set any tmux color names or 256-color codes - - `HEXAI_TMUX_STATUS_FG=white` - - `HEXAI_TMUX_STATUS_BG=magenta` (or `colour5`, etc.) -- If the segment is truncated, widen it: `set -g status-right-length 120` +See the [tmux integration guide](docs/tmux.md) for details on configuring the status line. diff --git a/cmd/hexai-lsp/main_test.go b/cmd/hexai-lsp/main_test.go index 987bfb4..387b640 100644 --- a/cmd/hexai-lsp/main_test.go +++ b/cmd/hexai-lsp/main_test.go @@ -1,23 +1,22 @@ package main import ( - "bytes" - "log" - "os" - "testing" + "bytes" + "log" + "os" + "testing" ) func TestMain_Version(t *testing.T) { - oldArgs := os.Args - defer func() { os.Args = oldArgs }() - os.Args = []string{"hexai-lsp", "-version"} - var buf bytes.Buffer - old := log.Writer() - log.SetOutput(&buf) - defer log.SetOutput(old) - main() - if buf.Len() == 0 { - t.Fatalf("expected version log") - } + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + os.Args = []string{"hexai-lsp", "-version"} + var buf bytes.Buffer + old := log.Writer() + log.SetOutput(&buf) + defer log.SetOutput(old) + main() + if buf.Len() == 0 { + t.Fatalf("expected version log") + } } - diff --git a/cmd/hexai-tmux-action/main.go b/cmd/hexai-tmux-action/main.go index 02cfe09..2d8793b 100644 --- a/cmd/hexai-tmux-action/main.go +++ b/cmd/hexai-tmux-action/main.go @@ -1,30 +1,29 @@ package main import ( - "context" - "flag" - "fmt" - "os" + "context" + "flag" + "fmt" + "os" - "codeberg.org/snonux/hexai/internal/hexaiaction" + "codeberg.org/snonux/hexai/internal/hexaiaction" ) 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") - 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") - tmuxPercent := flag.Int("tmux-percent", 33, "tmux split size percentage (1-100)") - flag.Parse() + infile := flag.String("infile", "", "Read input from this file instead of stdin") + outfile := flag.String("outfile", "", "Write output to this file instead of stdout") + 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") + tmuxPercent := flag.Int("tmux-percent", 33, "tmux split size percentage (1-100)") + flag.Parse() - opts := hexaiaction.Options{ - Infile: *infile, Outfile: *outfile, - 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) - os.Exit(1) - } + opts := hexaiaction.Options{ + Infile: *infile, Outfile: *outfile, + 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) + os.Exit(1) + } } - diff --git a/cmd/hexai/main_test.go b/cmd/hexai/main_test.go index beb684b..70c844f 100644 --- a/cmd/hexai/main_test.go +++ b/cmd/hexai/main_test.go @@ -1,23 +1,23 @@ package main import ( - "io" - "os" - "testing" + "io" + "os" + "testing" ) func TestMain_Version(t *testing.T) { - oldArgs := os.Args - defer func() { os.Args = oldArgs }() - os.Args = []string{"hexai", "-version"} - r, w, _ := os.Pipe() - old := os.Stdout - os.Stdout = w - defer func() { os.Stdout = old }() - main() - w.Close() - b, _ := io.ReadAll(r) - if len(b) == 0 { - t.Fatalf("expected version output") - } + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + os.Args = []string{"hexai", "-version"} + r, w, _ := os.Pipe() + old := os.Stdout + os.Stdout = w + defer func() { os.Stdout = old }() + main() + w.Close() + b, _ := io.ReadAll(r) + if len(b) == 0 { + t.Fatalf("expected version output") + } } diff --git a/docs/configuration.md b/docs/configuration.md index 95b65de..0b9e204 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -64,24 +64,7 @@ Editor integration Tmux status line -- Hexai can update a tmux user option during LLM activity. Add this to your `~/.tmux.conf` to display it: - - - `set -g status-right '#{@hexai_status} #[fg=colour8]| %H:%M'` - -- Status content is updated best‑effort at key moments: - - CLI: start (⏳ provider:model) and completion with compact stats (↑sent, ↓recv, rpm, reqs) - - LSP: initial heartbeat on client initialize, and periodic compact stats (provider, model, rpm, reqs, bytes) - - TUI action runner: ready (provider:model) and completion with compact stats - -- Toggle via environment: - - Enable (default): unset or `HEXAI_TMUX_STATUS=1` - - Disable: `HEXAI_TMUX_STATUS=0` - -- Theme and colors: - - Preset: `HEXAI_TMUX_STATUS_THEME=white-on-purple` (white fg on purple/magenta bg) - - Explicit: `HEXAI_TMUX_STATUS_FG=`, `HEXAI_TMUX_STATUS_BG=` (e.g., `white`, `magenta`, `colour5`) - - Colors use tmux’s `fg`/`bg` names; both methods wrap the entire `@hexai_status` segment. - - If truncated, increase width: `set -g status-right-length 120` +See the [tmux integration guide](docs/tmux.md) for details on configuring the status line. Code action prompts diff --git a/docs/coverage.html b/docs/coverage.html index 8560a58..059834b 100644 --- a/docs/coverage.html +++ b/docs/coverage.html @@ -71,13 +71,13 @@ - + - + @@ -113,7 +113,7 @@ - + @@ -123,7 +123,9 @@ - + + + @@ -1485,6 +1487,7 @@ import ( "codeberg.org/snonux/hexai/internal/editor" "codeberg.org/snonux/hexai/internal/logging" "codeberg.org/snonux/hexai/internal/llmutils" + "codeberg.org/snonux/hexai/internal/tmux" ) // Run executes the hexai-tmux-action command flow. @@ -1495,12 +1498,14 @@ var newClientFromApp = llmutils.NewClientFromApp func Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error { logger := log.New(stderr, "hexai-tmux-action ", log.LstdFlags|log.Lmsgprefix) cfg := appconfig.Load(logger) - client, err := newClientFromApp(cfg) + cli, err := newClientFromApp(cfg) if err != nil { fmt.Fprintf(stderr, logging.AnsiBase+"hexai-tmux-action: LLM disabled: %v"+logging.AnsiReset+"\n", err) return err } - parts, err := ParseInput(stdin) + _ = tmux.SetStatus("hexai action ready " + cli.DefaultModel()) + var client chatDoer = cli + parts, err := ParseInput(stdin) if err != nil { fmt.Fprintln(stderr, logging.AnsiBase+"hexai-tmux-action: failed to read input"+logging.AnsiReset) return err @@ -1512,12 +1517,13 @@ func Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error < if err != nil { return err } - out, err := executeAction(ctx, kind, parts, cfg, client, stderr) - if err != nil { - return err - } - io.WriteString(stdout, out) - return nil + out, err := executeAction(ctx, kind, parts, cfg, client, stderr) + if err != nil { + return err + } + io.WriteString(stdout, out) + _ = tmux.SetStatus("✅ " + cli.DefaultModel()) + return nil } func executeAction(ctx context.Context, kind ActionKind, parts InputParts, cfg appconfig.App, client chatDoer, stderr io.Writer) (string, error) { @@ -1745,6 +1751,7 @@ import ( "codeberg.org/snonux/hexai/internal/logging" "codeberg.org/snonux/hexai/internal/llm" "codeberg.org/snonux/hexai/internal/llmutils" + "codeberg.org/snonux/hexai/internal/tmux" ) // Run executes the Hexai CLI behavior given arguments and I/O streams. @@ -1851,8 +1858,10 @@ func buildMessagesFromConfig(cfg appconfig.App, input string) []llm.Message { - start := time.Now() - var output string + start := time.Now() + // Best-effort tmux status update + _ = tmux.SetStatus("⏳ " + client.Name() + ":" + client.DefaultModel()) + var output string if s, ok := client.(llm.Streamer); ok { var b strings.Builder if err := s.ChatStream(ctx, msgs, func(chunk string) { @@ -1870,10 +1879,11 @@ func runChat(ctx context.Context, client llm.Client, msgs []llm.Message, input s output = txt fmt.Fprint(out, output) } - dur := time.Since(start) - fmt.Fprintf(errw, "\n"+logging.AnsiBase+"done provider=%s model=%s time=%s in_bytes=%d out_bytes=%d"+logging.AnsiReset+"\n", - client.Name(), client.DefaultModel(), dur.Round(time.Millisecond), len(input), len(output)) - return nil + dur := time.Since(start) + fmt.Fprintf(errw, "\n"+logging.AnsiBase+"done provider=%s model=%s time=%s in_bytes=%d out_bytes=%d"+logging.AnsiReset+"\n", + client.Name(), client.DefaultModel(), dur.Round(time.Millisecond), len(input), len(output)) + _ = tmux.SetStatus("✅ " + client.DefaultModel() + " " + dur.Round(time.Millisecond).String()) + return nil } // printProviderInfo writes the provider/model line to stderr. @@ -5260,6 +5270,7 @@ import ( "codeberg.org/snonux/hexai/internal/llm" "codeberg.org/snonux/hexai/internal/logging" "codeberg.org/snonux/hexai/internal/textutil" + tmx "codeberg.org/snonux/hexai/internal/tmux" ) // Configurable inline trigger characters (default to '>') used by free helpers below. @@ -5312,7 +5323,11 @@ func (s *Server) logLLMStats() { rpm := float64(reqs) / mins sentPerMin := float64(sentTot) / mins recvPerMin := float64(recvTot) / mins - logging.Logf("lsp ", "llm stats reqs=%d avg_sent=%d avg_recv=%d sent_total=%d recv_total=%d rpm=%.2f sent_per_min=%.0f recv_per_min=%.0f", reqs, avgSent, avgRecv, sentTot, recvTot, rpm, sentPerMin, recvPerMin) + logging.Logf("lsp ", "llm stats reqs=%d avg_sent=%d avg_recv=%d sent_total=%d recv_total=%d rpm=%.2f sent_per_min=%.0f recv_per_min=%.0f", reqs, avgSent, avgRecv, sentTot, recvTot, rpm, sentPerMin, recvPerMin) + // Best-effort tmux status update + if s.llmClient != nil { + _ = tmx.SetStatus("LLM:" + s.llmClient.DefaultModel()) + } } // Completion prompt builders and filters @@ -6194,6 +6209,36 @@ func FindStrictInlineTag(line string) (text string, left, right int, ok bool)