1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
|
package hexaiaction
import (
"context"
"fmt"
"io"
"log"
"strings"
"time"
"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/stats"
"codeberg.org/snonux/hexai/internal/tmux"
)
// Run executes the hexai-tmux-action command flow.
// seams for testability
var (
chooseActionFn = RunTUI
newClientFromApp = llmutils.NewClientFromApp
)
// selectedCustom carries the chosen custom action (if any) from the TUI submenu
// to the executor. Cleared after use.
var selectedCustom *appconfig.CustomAction
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)
if cfg.StatsWindowMinutes > 0 {
stats.SetWindow(time.Duration(cfg.StatsWindowMinutes) * time.Minute)
}
if err := cfg.Validate(); err != nil {
fmt.Fprintf(stderr, logging.AnsiBase+"hexai-tmux-action: %v"+logging.AnsiReset+"\n", err)
return err
}
// Enable custom action submenu with configurable hotkey
if len(cfg.CustomActions) > 0 {
chooseActionFn = func() (ActionKind, error) { return RunTUIWithCustom(cfg.CustomActions, cfg.TmuxCustomMenuHotkey) }
}
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(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")
}
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)
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:
return handleRewriteAction(ctx, parts, cfg, client, stderr)
case ActionDiagnostics:
return handleDiagnosticsAction(ctx, parts, cfg, client)
case ActionDocument:
return handleDocumentAction(ctx, parts, cfg, client)
case ActionGoTest:
return handleGoTestAction(ctx, parts, cfg, client)
case ActionSimplify:
return handleSimplifyAction(ctx, parts, cfg, client)
case ActionCustom:
return handleCustomAction(ctx, parts, cfg, client)
case ActionCustomPrompt:
return handleCustomPromptAction(ctx, parts, cfg, client, stderr)
default:
return parts.Selection, nil
}
}
func handleRewriteAction(ctx context.Context, parts InputParts, cfg appconfig.App, client chatDoer, stderr io.Writer) (string, error) {
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)
return parts.Selection, nil
}
return runWithTimeout(ctx, timeout10s, func(cctx context.Context) (string, error) {
return runRewrite(cctx, cfg, client, instr, cleaned)
})
}
func handleDiagnosticsAction(ctx context.Context, parts InputParts, cfg appconfig.App, client chatDoer) (string, error) {
return runWithTimeout(ctx, timeout10s, func(cctx context.Context) (string, error) {
return runDiagnostics(cctx, cfg, client, parts.Diagnostics, parts.Selection)
})
}
func handleDocumentAction(ctx context.Context, parts InputParts, cfg appconfig.App, client chatDoer) (string, error) {
return runWithTimeout(ctx, timeout10s, func(cctx context.Context) (string, error) {
return runDocument(cctx, cfg, client, parts.Selection)
})
}
func handleGoTestAction(ctx context.Context, parts InputParts, cfg appconfig.App, client chatDoer) (string, error) {
return runWithTimeout(ctx, timeout8s, func(cctx context.Context) (string, error) {
return runGoTest(cctx, cfg, client, parts.Selection)
})
}
func handleSimplifyAction(ctx context.Context, parts InputParts, cfg appconfig.App, client chatDoer) (string, error) {
return runWithTimeout(ctx, timeout10s, func(cctx context.Context) (string, error) {
return runSimplify(cctx, cfg, client, parts.Selection)
})
}
func handleCustomAction(ctx context.Context, parts InputParts, cfg appconfig.App, client chatDoer) (string, error) {
if selectedCustom == nil {
return parts.Selection, nil
}
return runWithTimeout(ctx, timeout10s, func(cctx context.Context) (string, error) {
out, err := runCustom(cctx, cfg, client, *selectedCustom, parts)
selectedCustom = nil
return out, err
})
}
func handleCustomPromptAction(ctx context.Context, parts InputParts, cfg appconfig.App, client chatDoer, stderr io.Writer) (string, error) {
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 runWithTimeout(ctx, timeout10s, func(cctx context.Context) (string, error) {
return runRewrite(cctx, cfg, client, prompt, parts.Selection)
})
}
func runWithTimeout(ctx context.Context, timeout func(context.Context) (context.Context, context.CancelFunc), fn func(context.Context) (string, error)) (string, error) {
innerCtx, cancel := timeout(ctx)
defer cancel()
return fn(innerCtx)
}
// client construction is shared via internal/llmutils
|