From 04f290dbeeee8a6fcbc70fed253a968336bcb2ab Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sat, 6 Sep 2025 13:19:01 +0300 Subject: more tests --- internal/hexaiaction/parse.go | 69 +++++++++++++++++ internal/hexaiaction/parse_test.go | 121 ++++++++++++++++++++++++++++++ internal/hexaiaction/prompts.go | 91 ++++++++++++++++++++++ internal/hexaiaction/run.go | 74 ++++++++++++++++++ internal/hexaiaction/run_test.go | 51 +++++++++++++ internal/hexaiaction/tui.go | 118 +++++++++++++++++++++++++++++ internal/hexaiaction/tui_delegate.go | 35 +++++++++ internal/hexaiaction/tui_delegate_test.go | 32 ++++++++ internal/hexaiaction/tui_test.go | 36 +++++++++ internal/hexaiaction/types.go | 19 +++++ 10 files changed, 646 insertions(+) create mode 100644 internal/hexaiaction/parse.go create mode 100644 internal/hexaiaction/parse_test.go create mode 100644 internal/hexaiaction/prompts.go create mode 100644 internal/hexaiaction/run.go create mode 100644 internal/hexaiaction/run_test.go create mode 100644 internal/hexaiaction/tui.go create mode 100644 internal/hexaiaction/tui_delegate.go create mode 100644 internal/hexaiaction/tui_delegate_test.go create mode 100644 internal/hexaiaction/tui_test.go create mode 100644 internal/hexaiaction/types.go (limited to 'internal/hexaiaction') diff --git a/internal/hexaiaction/parse.go b/internal/hexaiaction/parse.go new file mode 100644 index 0000000..99e2b24 --- /dev/null +++ b/internal/hexaiaction/parse.go @@ -0,0 +1,69 @@ +package hexaiaction + +import ( + "bufio" + "io" + "strings" + + "codeberg.org/snonux/hexai/internal/textutil" +) + +// ParseInput splits raw stdin into optional diagnostics and selection/code. +// Format: +// +// Diagnostics:\n +// \n +// (optional)\n +// +// +// If the header is absent, the entire input is treated as selection. +func ParseInput(r io.Reader) (InputParts, error) { + b, err := io.ReadAll(bufio.NewReader(r)) + if err != nil { + return InputParts{}, err + } + raw := strings.TrimSpace(string(b)) + if raw == "" { + return InputParts{Selection: ""}, nil + } + lines := strings.Split(raw, "\n") + // find a case-insensitive line equal to "diagnostics:" + diagsIdx := -1 + for i, ln := range lines { + t := strings.TrimSpace(strings.ToLower(ln)) + if t == "diagnostics:" { + diagsIdx = i + break + } + } + if diagsIdx < 0 { + return InputParts{Selection: raw}, nil + } + // collect diagnostics until a blank line or EOF + diags := []string{} + i := diagsIdx + 1 + for ; i < len(lines); i++ { + t := strings.TrimSpace(lines[i]) + if t == "" { + i++ + break + } + diags = append(diags, t) + } + sel := strings.Join(lines[i:], "\n") + sel = strings.TrimSpace(sel) + return InputParts{Selection: sel, Diagnostics: diags}, nil +} + +// ExtractInstruction mirrors the LSP instructionFromSelection behavior (subset), +// scanning the first line for an instruction marker and removing it from the selection. +func ExtractInstruction(sel string) (string, string) { return textutil.InstructionFromSelection(sel) } + +// findFirstInstructionInLine follows the same precedence as LSP: +// - ;text; (strict) +// - /* text */ (single-line) +// - (single-line) +// - // text +// - # text +// - -- text +// helpers moved to textutil diff --git a/internal/hexaiaction/parse_test.go b/internal/hexaiaction/parse_test.go new file mode 100644 index 0000000..f81ab54 --- /dev/null +++ b/internal/hexaiaction/parse_test.go @@ -0,0 +1,121 @@ +package hexaiaction + +import ( + "context" + "strings" + "testing" + + "codeberg.org/snonux/hexai/internal/appconfig" + "codeberg.org/snonux/hexai/internal/llm" +) + +func TestParseInput_NoDiagnostics(t *testing.T) { + in := "some code here" + parts, err := ParseInput(strings.NewReader(in)) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if parts.Selection != in || len(parts.Diagnostics) != 0 { + t.Fatalf("unexpected parse: %#v", parts) + } +} + +func TestParseInput_WithDiagnostics(t *testing.T) { + in := "Diagnostics:\nmissing return\nuse of undefined: foo\n\nfunc a() {}" + parts, err := ParseInput(strings.NewReader(in)) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if parts.Selection != "func a() {}" { + t.Fatalf("selection wrong: %q", parts.Selection) + } + if len(parts.Diagnostics) != 2 || parts.Diagnostics[0] != "missing return" { + t.Fatalf("diags wrong: %#v", parts.Diagnostics) + } +} + +func TestExtractInstruction_Variants(t *testing.T) { + cases := []struct{ in, wantInstr string }{ + {";rewrite to X;\ncode", "rewrite to X"}, + {"/* fix it */\ncode", "fix it"}, + {"\ncode", "doc me"}, + {"// change it\ncode", "change it"}, + {"# tweak\ncode", "tweak"}, + {"-- fix\ncode", "fix"}, + } + for _, c := range cases { + got, cleaned := ExtractInstruction(c.in) + if got != c.wantInstr { + t.Fatalf("instr mismatch: %q != %q", got, c.wantInstr) + } + if strings.Contains(cleaned, c.wantInstr) && strings.Contains(c.in, c.wantInstr) { + t.Fatalf("expected instruction removed from selection: %q", cleaned) + } + } +} + +func TestRenderAndStrip(t *testing.T) { + tpl := "Hello, {{name}}" + out := Render(tpl, map[string]string{"name": "Hex"}) + if out != "Hello, Hex" { + t.Fatalf("unexpected render: %q", out) + } + fenced := "```go\npackage x\n```" + if StripFences(fenced) != "package x" { + t.Fatalf("unexpected strip") + } +} + +type fakeClient struct { + last []llm.Message + out string + err error +} + +func (f *fakeClient) Chat(_ context.Context, msgs []llm.Message, _ ...llm.RequestOption) (string, error) { + f.last = msgs + return f.out, f.err +} + +func TestRuners_Prompts(t *testing.T) { + cfg := appconfig.App{ + PromptCodeActionRewriteSystem: "SYS-R", + PromptCodeActionRewriteUser: "R {{instruction}} :: {{selection}}", + PromptCodeActionDiagnosticsSystem: "SYS-D", + PromptCodeActionDiagnosticsUser: "D {{diagnostics}} :: {{selection}}", + PromptCodeActionDocumentSystem: "SYS-C", + PromptCodeActionDocumentUser: "C {{selection}}", + PromptCodeActionGoTestSystem: "SYS-T", + PromptCodeActionGoTestUser: "T {{function}}", + } + f := &fakeClient{out: "```\nDONE\n```"} + ctx := context.Background() + // rewrite + if out, err := runRewrite(ctx, cfg, f, "instr", "sel"); err != nil || out != "DONE" { + t.Fatalf("rewrite failed: %q %v", out, err) + } + if len(f.last) != 2 || f.last[0].Content != "SYS-R" || !strings.Contains(f.last[1].Content, "instr") { + t.Fatalf("rewrite prompts wrong: %#v", f.last) + } + // diagnostics + if out, err := runDiagnostics(ctx, cfg, f, []string{"a", "b"}, "sel"); err != nil || out != "DONE" { + t.Fatalf("diagnostics failed: %q %v", out, err) + } + if f.last[0].Content != "SYS-D" || !strings.Contains(f.last[1].Content, "a\nb") { + t.Fatalf("diagnostics prompts wrong: %#v", f.last) + } + // document + if out, err := runDocument(ctx, cfg, f, "sel"); err != nil || out != "DONE" { + t.Fatalf("document failed: %q %v", out, err) + } + if f.last[0].Content != "SYS-C" || !strings.Contains(f.last[1].Content, "sel") { + t.Fatalf("document prompts wrong: %#v", f.last) + } + // gotest + if out, err := runGoTest(ctx, cfg, f, "func A(){}"); err != nil || out != "DONE" { + t.Fatalf("gotest failed: %q %v", out, err) + } + if f.last[0].Content != "SYS-T" || !strings.Contains(f.last[1].Content, "func A(){") { + t.Fatalf("gotest prompts wrong: %#v", f.last) + } +} diff --git a/internal/hexaiaction/prompts.go b/internal/hexaiaction/prompts.go new file mode 100644 index 0000000..2e0e4e2 --- /dev/null +++ b/internal/hexaiaction/prompts.go @@ -0,0 +1,91 @@ +package hexaiaction + +import ( + "context" + "strings" + "time" + + "codeberg.org/snonux/hexai/internal/appconfig" + "codeberg.org/snonux/hexai/internal/llm" + "codeberg.org/snonux/hexai/internal/textutil" +) + +// Render performs simple {{var}} replacement like LSP. +func Render(t string, vars map[string]string) string { return textutil.RenderTemplate(t, vars) } + +// StripFences removes surrounding markdown code fences. +func StripFences(s string) string { return textutil.StripCodeFences(s) } + +type chatDoer interface { + Chat(ctx context.Context, msgs []llm.Message, opts ...llm.RequestOption) (string, error) +} + +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)) +} + +func runDiagnostics(ctx context.Context, cfg appconfig.App, client chatDoer, diags []string, selection string) (string, error) { + var b strings.Builder + for i, d := range diags { + if strings.TrimSpace(d) == "" { + continue + } + b.WriteString(strings.TrimSpace(d)) + if i < len(diags)-1 { + b.WriteString("\n") + } + } + sys := cfg.PromptCodeActionDiagnosticsSystem + user := Render(cfg.PromptCodeActionDiagnosticsUser, map[string]string{"diagnostics": b.String(), "selection": selection}) + 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)) +} + +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)) +} + +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 +} + +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 +} + +// 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 +} + +// Timeout helpers to mirror LSP behavior. +func timeout10s(parent context.Context) (context.Context, context.CancelFunc) { + return context.WithTimeout(parent, 10*time.Second) +} + +func timeout8s(parent context.Context) (context.Context, context.CancelFunc) { + return context.WithTimeout(parent, 8*time.Second) +} diff --git a/internal/hexaiaction/run.go b/internal/hexaiaction/run.go new file mode 100644 index 0000000..2a67a58 --- /dev/null +++ b/internal/hexaiaction/run.go @@ -0,0 +1,74 @@ +package hexaiaction + +import ( + "context" + "fmt" + "io" + "log" + "strings" + + "codeberg.org/snonux/hexai/internal/appconfig" + "codeberg.org/snonux/hexai/internal/logging" + "codeberg.org/snonux/hexai/internal/llmutils" +) + +// Run executes the hexai-action command flow. +func Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error { + logger := log.New(stderr, "hexai-action ", log.LstdFlags|log.Lmsgprefix) + cfg := appconfig.Load(logger) + client, err := llmutils.NewClientFromApp(cfg) + if err != nil { + fmt.Fprintf(stderr, logging.AnsiBase+"hexai-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) + return err + } + if strings.TrimSpace(parts.Selection) == "" { + return fmt.Errorf("hexai-action: no input provided on stdin") + } + kind, err := RunTUI() + 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: + instr, cleaned := ExtractInstruction(parts.Selection) + if strings.TrimSpace(instr) == "" { + fmt.Fprintln(stderr, logging.AnsiBase+"hexai-action: no inline instruction found; echoing input"+logging.AnsiReset) + return parts.Selection, nil + } + cctx, cancel := timeout10s(ctx) + defer cancel() + return runRewrite(cctx, cfg, client, instr, cleaned) + case ActionDiagnostics: + 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) + default: + return parts.Selection, nil + } +} + +// client construction is shared via internal/llmutils diff --git a/internal/hexaiaction/run_test.go b/internal/hexaiaction/run_test.go new file mode 100644 index 0000000..87fbfa8 --- /dev/null +++ b/internal/hexaiaction/run_test.go @@ -0,0 +1,51 @@ +package hexaiaction + +import ( + "context" + "strings" + "testing" + + "codeberg.org/snonux/hexai/internal/appconfig" + "codeberg.org/snonux/hexai/internal/llm" +) + +type fakeDoer struct{ out string } + +func (f fakeDoer) Chat(_ context.Context, _ []llm.Message, _ ...llm.RequestOption) (string, error) { + return f.out, nil +} + +func TestExecuteAction_Skip(t *testing.T) { + cfg := appconfig.App{} + parts := InputParts{Selection: "data"} + out, err := executeAction(context.Background(), ActionSkip, parts, cfg, fakeDoer{"IGN"}, nil) + if err != nil || out != "data" { + t.Fatalf("skip failed: %q %v", out, err) + } +} + +func TestExecuteAction_Rewrite_Document_GoTest(t *testing.T) { + cfg := appconfig.Load(nil) // defaults + // Use fenced output to exercise StripFences + client := fakeDoer{"```\nDONE\n```"} + + // rewrite with inline instruction + sel := ";change;\ncode" + out, err := executeAction(context.Background(), ActionRewrite, InputParts{Selection: sel}, cfg, client, nil) + if err != nil || strings.TrimSpace(out) != "DONE" { + t.Fatalf("rewrite failed: %q %v", out, err) + } + + // document + out, err = executeAction(context.Background(), ActionDocument, InputParts{Selection: "code"}, cfg, client, nil) + if err != nil || strings.TrimSpace(out) != "DONE" { + t.Fatalf("document failed: %q %v", out, err) + } + + // go test + out, err = executeAction(context.Background(), ActionGoTest, InputParts{Selection: "func A(){}"}, cfg, client, nil) + if err != nil || strings.TrimSpace(out) != "DONE" { + t.Fatalf("gotest failed: %q %v", out, err) + } +} + diff --git a/internal/hexaiaction/tui.go b/internal/hexaiaction/tui.go new file mode 100644 index 0000000..16988c0 --- /dev/null +++ b/internal/hexaiaction/tui.go @@ -0,0 +1,118 @@ +package hexaiaction + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" +) + +// item implements list.Item +type item struct { + title, desc string + kind ActionKind + hotkey rune +} + +func (i item) Title() string { return i.title } +func (i item) Description() string { return i.desc } +func (i item) FilterValue() string { return i.title } + +type model struct { + list list.Model + chosen ActionKind + done bool +} + +func newModel() model { + items := []list.Item{ + item{title: "Rewrite selection", desc: "", kind: ActionRewrite, hotkey: 'r'}, + item{title: "Document code", desc: "", kind: ActionDocument, hotkey: 'c'}, + item{title: "Generate Go unit test(s)", desc: "", kind: ActionGoTest, hotkey: 't'}, + item{title: "Skip", desc: "", kind: ActionSkip, hotkey: 's'}, + } + l := list.New(items, oneLineDelegate{}, 0, 0) + l.Title = "Select Hexai Action" + l.SetShowHelp(false) + l.SetShowStatusBar(false) + l.SetFilteringEnabled(false) + return model{list: l} +} + +func (m model) Init() tea.Cmd { return nil } + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + return handleKey(m, msg) + case tea.WindowSizeMsg: + m.list.SetSize(msg.Width, msg.Height) + } + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd +} + +func handleKey(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) { + raw := msg.String() + low := strings.ToLower(raw) + switch low { + case "esc", "q": + // Treat ESC and q as Skip/quit + m.chosen = ActionSkip + m.done = true + return m, tea.Quit + case "enter": + if it, ok := m.list.SelectedItem().(item); ok { + m.chosen = it.kind + m.done = true + return m, tea.Quit + } + case "j", "down": + m.list.CursorDown() + case "k", "up": + m.list.CursorUp() + case "g", "home": + m.list.Select(0) + case "end": + if n := len(m.list.Items()); n > 0 { m.list.Select(n - 1) } + case "s", "r", "c", "t": + items := m.list.Items() + for i := 0; i < len(items); i++ { + if it, ok := items[i].(item); ok && strings.ToLower(string(it.hotkey)) == low { + m.list.Select(i) + m.chosen = it.kind + m.done = true + return m, tea.Quit + } + } + } + if raw == "G" { // Shift+G jumps to end + if n := len(m.list.Items()); n > 0 { m.list.Select(n - 1) } + } + return m, nil +} + +func (m model) View() string { + if m.done { + return "" + } + return m.list.View() +} + +// RunTUI returns the chosen ActionKind. +func RunTUI() (ActionKind, error) { + p := tea.NewProgram(newModel()) + md, err := p.Run() + if err != nil { + return ActionSkip, err + } + if m, ok := md.(model); ok { + if m.chosen == "" { + return ActionSkip, nil + } + return m.chosen, nil + } + return ActionSkip, fmt.Errorf("unexpected model type") +} diff --git a/internal/hexaiaction/tui_delegate.go b/internal/hexaiaction/tui_delegate.go new file mode 100644 index 0000000..0e5a68c --- /dev/null +++ b/internal/hexaiaction/tui_delegate.go @@ -0,0 +1,35 @@ +package hexaiaction + +import ( + "fmt" + "io" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// oneLineDelegate renders a single compact line per item, no spacing. +type oneLineDelegate struct{} + +var ( + hotStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205")) + cursorStyle = lipgloss.NewStyle().Bold(true) +) + +func (oneLineDelegate) Height() int { return 1 } +func (oneLineDelegate) Spacing() int { return 0 } +func (oneLineDelegate) Update(tea.Msg, *list.Model) tea.Cmd { return nil } +func (oneLineDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + title := listItem.FilterValue() + hk := '?' + if it, ok := listItem.(item); ok { + hk = it.hotkey + } + hot := hotStyle.Render(fmt.Sprintf(" (%c)", hk)) + cursor := " " + if index == m.Index() { + cursor = cursorStyle.Render("> ") + } + fmt.Fprintf(w, "%s%s%s", cursor, title, hot) +} diff --git a/internal/hexaiaction/tui_delegate_test.go b/internal/hexaiaction/tui_delegate_test.go new file mode 100644 index 0000000..27881e4 --- /dev/null +++ b/internal/hexaiaction/tui_delegate_test.go @@ -0,0 +1,32 @@ +package hexaiaction + +import ( + "bytes" + "regexp" + "testing" + + "github.com/charmbracelet/bubbles/list" +) + +func stripANSI(s string) string { + re := regexp.MustCompile(`\x1b\[[0-9;]*m`) + return re.ReplaceAllString(s, "") +} + +func TestOneLineDelegate_Render(t *testing.T) { + items := []list.Item{item{title: "Rewrite selection", kind: ActionRewrite, hotkey: 'r'}} + m := list.New(items, oneLineDelegate{}, 0, 0) + m.Select(0) + var b bytes.Buffer + oneLineDelegate{}.Render(&b, m, 0, items[0]) + out := stripANSI(b.String()) + if !regexp.MustCompile(`> \w`).MatchString(out) { + t.Fatalf("expected cursor prefix in %q", out) + } + if !regexp.MustCompile(`Rewrite selection`).MatchString(out) { + t.Fatalf("expected title in %q", out) + } + if !regexp.MustCompile(`\(r\)`).MatchString(out) { + t.Fatalf("expected hotkey in %q", out) + } +} diff --git a/internal/hexaiaction/tui_test.go b/internal/hexaiaction/tui_test.go new file mode 100644 index 0000000..0f7d091 --- /dev/null +++ b/internal/hexaiaction/tui_test.go @@ -0,0 +1,36 @@ +package hexaiaction + +import ( + "testing" + + tea "github.com/charmbracelet/bubbletea" +) + +func TestHandleKey_EscSkips(t *testing.T) { + m := newModel() + nm, _ := handleKey(m, tea.KeyMsg{Type: tea.KeyEsc}) + got, ok := nm.(model) + if !ok || !got.done || got.chosen != ActionSkip { + t.Fatalf("esc should skip: ok=%v done=%v chosen=%v", ok, got.done, got.chosen) + } +} + +func TestHandleKey_QuickHotkey(t *testing.T) { + m := newModel() + nm, _ := handleKey(m, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'r'}}) + got := nm.(model) + if !got.done || got.chosen != ActionRewrite { + t.Fatalf("r should choose rewrite: done=%v chosen=%v", got.done, got.chosen) + } +} + +func TestHandleKey_JumpEndWithG(t *testing.T) { + m := newModel() + // raw 'G' rune should jump to end (special cased) + nm, _ := handleKey(m, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'G'}}) + got := nm.(model) + if idx := got.list.Index(); idx != len(got.list.Items())-1 { + t.Fatalf("G should jump to end, index=%d", idx) + } +} + diff --git a/internal/hexaiaction/types.go b/internal/hexaiaction/types.go new file mode 100644 index 0000000..5e01cfc --- /dev/null +++ b/internal/hexaiaction/types.go @@ -0,0 +1,19 @@ +package hexaiaction + +// Summary: Core types and constants for hexai-action. + +type ActionKind string + +const ( + ActionSkip ActionKind = "skip" + ActionRewrite ActionKind = "rewrite" + ActionDiagnostics ActionKind = "diagnostics" + ActionDocument ActionKind = "document" + ActionGoTest ActionKind = "gotest" +) + +// InputParts represents parsed stdin input for actions. +type InputParts struct { + Selection string + Diagnostics []string +} -- cgit v1.2.3