diff options
Diffstat (limited to 'internal/hexaiaction')
| -rw-r--r-- | internal/hexaiaction/prompts.go | 107 | ||||
| -rw-r--r-- | internal/hexaiaction/run.go | 131 |
2 files changed, 139 insertions, 99 deletions
diff --git a/internal/hexaiaction/prompts.go b/internal/hexaiaction/prompts.go index 3c33f8a..e9d7fc6 100644 --- a/internal/hexaiaction/prompts.go +++ b/internal/hexaiaction/prompts.go @@ -1,13 +1,14 @@ package hexaiaction import ( - "context" - "strings" - "time" + "context" + "strings" + "time" - "codeberg.org/snonux/hexai/internal/appconfig" - "codeberg.org/snonux/hexai/internal/llm" - "codeberg.org/snonux/hexai/internal/textutil" + "codeberg.org/snonux/hexai/internal/appconfig" + "codeberg.org/snonux/hexai/internal/llm" + "codeberg.org/snonux/hexai/internal/textutil" + "codeberg.org/snonux/hexai/internal/tmux" ) // Render performs simple {{var}} replacement like LSP. @@ -18,12 +19,22 @@ func StripFences(s string) string { return textutil.StripCodeFences(s) } type chatDoer interface { Chat(ctx context.Context, msgs []llm.Message, opts ...llm.RequestOption) (string, error) + DefaultModel() string +} + +type providerNamer interface{ Name() string } + +func providerOf(c any) string { + if n, ok := c.(providerNamer); ok { + return n.Name() + } + return "llm" } func runRewrite(ctx context.Context, cfg appconfig.App, client chatDoer, instruction, selection string) (string, error) { - sys := cfg.PromptCodeActionRewriteSystem - user := Render(cfg.PromptCodeActionRewriteUser, map[string]string{"instruction": instruction, "selection": selection}) - return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) + sys := cfg.PromptCodeActionRewriteSystem + user := Render(cfg.PromptCodeActionRewriteUser, map[string]string{"instruction": instruction, "selection": selection}) + return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) } func runDiagnostics(ctx context.Context, cfg appconfig.App, client chatDoer, diags []string, selection string) (string, error) { @@ -39,52 +50,80 @@ func runDiagnostics(ctx context.Context, cfg appconfig.App, client chatDoer, dia } sys := cfg.PromptCodeActionDiagnosticsSystem user := Render(cfg.PromptCodeActionDiagnosticsUser, map[string]string{"diagnostics": b.String(), "selection": selection}) - return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) + return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) } func runDocument(ctx context.Context, cfg appconfig.App, client chatDoer, selection string) (string, error) { - sys := cfg.PromptCodeActionDocumentSystem - user := Render(cfg.PromptCodeActionDocumentUser, map[string]string{"selection": selection}) - return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) + sys := cfg.PromptCodeActionDocumentSystem + user := Render(cfg.PromptCodeActionDocumentUser, map[string]string{"selection": selection}) + return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) } func runSimplify(ctx context.Context, cfg appconfig.App, client chatDoer, selection string) (string, error) { - sys := cfg.PromptCodeActionSimplifySystem - user := Render(cfg.PromptCodeActionSimplifyUser, map[string]string{"selection": selection}) - return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) + sys := cfg.PromptCodeActionSimplifySystem + user := Render(cfg.PromptCodeActionSimplifyUser, map[string]string{"selection": selection}) + return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) } func runGoTest(ctx context.Context, cfg appconfig.App, client chatDoer, funcCode string) (string, error) { sys := cfg.PromptCodeActionGoTestSystem user := Render(cfg.PromptCodeActionGoTestUser, map[string]string{"function": funcCode}) - return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) + return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) } func runOnce(ctx context.Context, client chatDoer, sys, user string) (string, error) { - msgs := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} - txt, err := client.Chat(ctx, msgs) - if err != nil { - return "", err - } - return strings.TrimSpace(StripFences(txt)), nil + msgs := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} + start := time.Now() + txt, err := client.Chat(ctx, msgs) + if err != nil { + return "", err + } + out := strings.TrimSpace(StripFences(txt)) + // Update tmux heartbeat with simple one-request stats + sent := 0 + for _, m := range msgs { + sent += len(m.Content) + } + recv := len(out) + mins := time.Since(start).Minutes() + if mins <= 0 { + mins = 0.001 + } + rpm := float64(1) / mins + _ = tmux.SetStatus(tmux.FormatLLMStatsStatusColored(providerOf(client), client.DefaultModel(), 1, rpm, int64(sent), int64(recv))) + return out, nil } func runOnceWithOpts(ctx context.Context, client chatDoer, sys, user string, opts []llm.RequestOption) (string, error) { - msgs := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} - txt, err := client.Chat(ctx, msgs, opts...) - if err != nil { - return "", err - } - return strings.TrimSpace(StripFences(txt)), nil + msgs := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} + start := time.Now() + txt, err := client.Chat(ctx, msgs, opts...) + if err != nil { + return "", err + } + out := strings.TrimSpace(StripFences(txt)) + // Update tmux heartbeat with simple one-request stats + sent := 0 + for _, m := range msgs { + sent += len(m.Content) + } + recv := len(out) + mins := time.Since(start).Minutes() + if mins <= 0 { + mins = 0.001 + } + rpm := float64(1) / mins + _ = tmux.SetStatus(tmux.FormatLLMStatsStatusColored(providerOf(client), client.DefaultModel(), 1, rpm, int64(sent), int64(recv))) + return out, nil } // reqOptsFrom builds LLM request options similar to LSP behavior. func reqOptsFrom(cfg appconfig.App) []llm.RequestOption { - opts := []llm.RequestOption{llm.WithMaxTokens(cfg.MaxTokens)} - if cfg.CodingTemperature != nil { - opts = append(opts, llm.WithTemperature(*cfg.CodingTemperature)) - } - return opts + opts := []llm.RequestOption{llm.WithMaxTokens(cfg.MaxTokens)} + if cfg.CodingTemperature != nil { + opts = append(opts, llm.WithTemperature(*cfg.CodingTemperature)) + } + return opts } // Timeout helpers to mirror LSP behavior. diff --git a/internal/hexaiaction/run.go b/internal/hexaiaction/run.go index c8cfcac..4958642 100644 --- a/internal/hexaiaction/run.go +++ b/internal/hexaiaction/run.go @@ -1,63 +1,64 @@ package hexaiaction import ( - "context" - "fmt" - "io" - "log" - "strings" + "context" + "fmt" + "io" + "log" + "strings" - "codeberg.org/snonux/hexai/internal/appconfig" - "codeberg.org/snonux/hexai/internal/editor" - "codeberg.org/snonux/hexai/internal/logging" - "codeberg.org/snonux/hexai/internal/llmutils" - "codeberg.org/snonux/hexai/internal/tmux" + "codeberg.org/snonux/hexai/internal/appconfig" + "codeberg.org/snonux/hexai/internal/editor" + "codeberg.org/snonux/hexai/internal/llmutils" + "codeberg.org/snonux/hexai/internal/logging" + "codeberg.org/snonux/hexai/internal/tmux" ) // Run executes the hexai-tmux-action command flow. // seams for testability -var chooseActionFn = RunTUI -var newClientFromApp = llmutils.NewClientFromApp +var ( + chooseActionFn = RunTUI + 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) - cli, err := newClientFromApp(cfg) - if err != nil { - fmt.Fprintf(stderr, logging.AnsiBase+"hexai-tmux-action: LLM disabled: %v"+logging.AnsiReset+"\n", err) - return err - } - _ = tmux.SetStatus("hexai action ready " + cli.DefaultModel()) - var client chatDoer = cli - parts, err := ParseInput(stdin) + logger := log.New(stderr, "hexai-tmux-action ", log.LstdFlags|log.Lmsgprefix) + cfg := appconfig.Load(logger) + cli, err := newClientFromApp(cfg) if err != nil { - fmt.Fprintln(stderr, logging.AnsiBase+"hexai-tmux-action: failed to read input"+logging.AnsiReset) + fmt.Fprintf(stderr, logging.AnsiBase+"hexai-tmux-action: LLM disabled: %v"+logging.AnsiReset+"\n", err) + return err + } + _ = tmux.SetStatus(tmux.FormatLLMStartStatus(cli.Name(), 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 } if strings.TrimSpace(parts.Selection) == "" { - return fmt.Errorf("hexai-tmux-action: no input provided on stdin") + return fmt.Errorf("hexai-tmux-action: no input provided on stdin") + } + kind, err := chooseActionFn() + if err != nil { + return err + } + out, err := executeAction(ctx, kind, parts, cfg, client, stderr) + if err != nil { + return err } - kind, err := chooseActionFn() - if err != nil { - return err - } - out, err := executeAction(ctx, kind, parts, cfg, client, stderr) - if err != nil { - return err - } - io.WriteString(stdout, out) - _ = tmux.SetStatus("✅ " + cli.DefaultModel()) - return nil + io.WriteString(stdout, out) + return nil } func executeAction(ctx context.Context, kind ActionKind, parts InputParts, cfg appconfig.App, client chatDoer, stderr io.Writer) (string, error) { - switch kind { - case ActionSkip: - return parts.Selection, nil - case ActionRewrite: + switch kind { + case ActionSkip: + return parts.Selection, nil + case ActionRewrite: 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) + fmt.Fprintln(stderr, logging.AnsiBase+"hexai-tmux-action: no inline instruction found; echoing input"+logging.AnsiReset) return parts.Selection, nil } cctx, cancel := timeout10s(ctx) @@ -67,31 +68,31 @@ func executeAction(ctx context.Context, kind ActionKind, parts InputParts, cfg a cctx, cancel := timeout10s(ctx) defer cancel() return runDiagnostics(cctx, cfg, client, parts.Diagnostics, parts.Selection) - case ActionDocument: - cctx, cancel := timeout10s(ctx) - defer cancel() - return runDocument(cctx, cfg, client, parts.Selection) - case ActionGoTest: - cctx, cancel := timeout8s(ctx) - defer cancel() - return runGoTest(cctx, cfg, client, parts.Selection) - case ActionSimplify: - cctx, cancel := timeout10s(ctx) - defer cancel() - return runSimplify(cctx, cfg, client, parts.Selection) - case ActionCustom: - cctx, cancel := timeout10s(ctx) - defer cancel() - // Open editor for free-form instruction - prompt, err := editor.OpenTempAndEdit(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 runRewrite(cctx, cfg, client, prompt, parts.Selection) - default: - return parts.Selection, nil - } + case ActionDocument: + cctx, cancel := timeout10s(ctx) + defer cancel() + return runDocument(cctx, cfg, client, parts.Selection) + case ActionGoTest: + cctx, cancel := timeout8s(ctx) + defer cancel() + return runGoTest(cctx, cfg, client, parts.Selection) + case ActionSimplify: + cctx, cancel := timeout10s(ctx) + defer cancel() + return runSimplify(cctx, cfg, client, parts.Selection) + case ActionCustom: + cctx, cancel := timeout10s(ctx) + defer cancel() + // Open editor for free-form instruction + prompt, err := editor.OpenTempAndEdit(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 runRewrite(cctx, cfg, client, prompt, parts.Selection) + default: + return parts.Selection, nil + } } // client construction is shared via internal/llmutils |
