summaryrefslogtreecommitdiff
path: root/internal/hexaiaction
diff options
context:
space:
mode:
Diffstat (limited to 'internal/hexaiaction')
-rw-r--r--internal/hexaiaction/prompts.go107
-rw-r--r--internal/hexaiaction/run.go131
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