From 23c16bb19723e4429330c7449e220507e298e7b1 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 2 Mar 2026 14:31:35 +0300 Subject: [PATCH] feat: add tool mgmt, model mgmt, options, event bus, LLM completion, steer mode, and 10 example extensions Phase 2+3 extension API additions: - Tool management: GetAllTools, SetActiveTools (plan-mode support) - Model management: SetModel, GetAvailableModels, ModelChangedEvent - Extension options: RegisterOption, GetOption, SetOption (env/config/default) - Inter-extension event bus: OnCustomEvent, EmitCustomEvent - Direct LLM completion: ctx.Complete with streaming/blocking modes - Steer delivery mode: CancelAndSend for interrupt-and-redirect New example extensions (10): - plan-mode.go: read-only exploration with /plan toggle - summarize.go: conversation summarization via ctx.Complete - bookmark.go: persistent bookmarks via AppendEntry/GetEntries - auto-commit.go: auto-commit on exit using last assistant message - permission-gate.go: confirm dangerous bash commands - protected-paths.go: block writes to .env, .git/, secrets/ - notify.go: desktop notifications on agent completion - inline-bash.go: !{cmd} expansion in prompts - pirate.go: system prompt persona injection - project-rules.go: load .kit/rules/*.md into system prompt Always-wrap tools through runner for SetActiveTools disabled-tool checking. Removed phase1/phase2 test extensions from examples. --- cmd/root.go | 49 ++++- examples/extensions/auto-commit.go | 72 +++++++ examples/extensions/bookmark.go | 79 ++++++++ examples/extensions/inline-bash.go | 52 +++++ examples/extensions/notify.go | 35 ++++ examples/extensions/permission-gate.go | 64 +++++++ examples/extensions/pirate.go | 28 +++ examples/extensions/plan-mode.go | 88 +++++++++ examples/extensions/project-rules.go | 71 +++++++ examples/extensions/protected-paths.go | 114 +++++++++++ examples/extensions/summarize.go | 93 +++++++++ internal/agent/agent.go | 70 +++++++ internal/app/app.go | 39 ++++ internal/app/events.go | 10 + internal/extensions/api.go | 254 +++++++++++++++++++++++++ internal/extensions/loader.go | 9 + internal/extensions/runner.go | 180 ++++++++++++++++-- internal/extensions/symbols.go | 13 ++ internal/extensions/wrapper.go | 16 +- internal/extensions/wrapper_test.go | 9 +- internal/ui/model.go | 6 + pkg/kit/kit.go | 230 ++++++++++++++++++++++ 22 files changed, 1552 insertions(+), 29 deletions(-) create mode 100644 examples/extensions/auto-commit.go create mode 100644 examples/extensions/bookmark.go create mode 100644 examples/extensions/inline-bash.go create mode 100644 examples/extensions/notify.go create mode 100644 examples/extensions/permission-gate.go create mode 100644 examples/extensions/pirate.go create mode 100644 examples/extensions/plan-mode.go create mode 100644 examples/extensions/project-rules.go create mode 100644 examples/extensions/protected-paths.go create mode 100644 examples/extensions/summarize.go diff --git a/cmd/root.go b/cmd/root.go index ddc6e68c..9a27cbb2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -14,6 +14,7 @@ import ( "github.com/mark3labs/kit/internal/app" "github.com/mark3labs/kit/internal/config" "github.com/mark3labs/kit/internal/extensions" + "github.com/mark3labs/kit/internal/models" "github.com/mark3labs/kit/internal/ui" kit "github.com/mark3labs/kit/pkg/kit" "github.com/spf13/cobra" @@ -606,14 +607,15 @@ func runNormalMode(ctx context.Context) error { if kitInstance.HasExtensions() { cwd, _ := os.Getwd() kitInstance.SetExtensionContext(extensions.Context{ - CWD: cwd, - Model: modelName, - Interactive: promptFlag == "", - Print: func(text string) { appInstance.PrintFromExtension("", text) }, - PrintInfo: func(text string) { appInstance.PrintFromExtension("info", text) }, - PrintError: func(text string) { appInstance.PrintFromExtension("error", text) }, - PrintBlock: appInstance.PrintBlockFromExtension, - SendMessage: func(text string) { appInstance.Run(text) }, + CWD: cwd, + Model: modelName, + Interactive: promptFlag == "", + Print: func(text string) { appInstance.PrintFromExtension("", text) }, + PrintInfo: func(text string) { appInstance.PrintFromExtension("info", text) }, + PrintError: func(text string) { appInstance.PrintFromExtension("error", text) }, + PrintBlock: appInstance.PrintBlockFromExtension, + SendMessage: func(text string) { appInstance.Run(text) }, + CancelAndSend: func(text string) { appInstance.Steer(text) }, SetWidget: func(config extensions.WidgetConfig) { kitInstance.SetExtensionWidget(config) appInstance.NotifyWidgetUpdate() @@ -737,6 +739,37 @@ func runNormalMode(ctx context.Context) error { kitInstance.RemoveExtensionStatus(key) appInstance.NotifyWidgetUpdate() }, + GetOption: func(name string) string { + return kitInstance.GetExtensionOption(name) + }, + SetOption: func(name string, value string) { + kitInstance.SetExtensionOption(name, value) + }, + SetModel: func(modelString string) error { + err := kitInstance.SetModel(context.Background(), modelString) + if err != nil { + return err + } + // Notify TUI so it updates model in status bar. + p, m, _ := models.ParseModelString(modelString) + appInstance.NotifyModelChanged(p, m) + return nil + }, + GetAvailableModels: func() []extensions.ModelInfoEntry { + return kitInstance.GetAvailableModels() + }, + EmitCustomEvent: func(name string, data string) { + kitInstance.EmitExtensionCustomEvent(name, data) + }, + Complete: func(req extensions.CompleteRequest) (extensions.CompleteResponse, error) { + return kitInstance.ExecuteCompletion(context.Background(), req) + }, + GetAllTools: func() []extensions.ToolInfo { + return kitInstance.GetExtensionToolInfos() + }, + SetActiveTools: func(names []string) { + kitInstance.SetExtensionActiveTools(names) + }, ShowOverlay: func(config extensions.OverlayConfig) extensions.OverlayResult { ch := make(chan app.OverlayResponse, 1) appInstance.SendOverlayRequest(app.OverlayRequestEvent{ diff --git a/examples/extensions/auto-commit.go b/examples/extensions/auto-commit.go new file mode 100644 index 00000000..ccab929f --- /dev/null +++ b/examples/extensions/auto-commit.go @@ -0,0 +1,72 @@ +//go:build ignore + +package main + +import ( + "os/exec" + "strings" + + "kit/ext" +) + +// Init automatically commits staged changes when the session shuts down, +// using the last assistant message as the commit message. Inspired by +// Pi's auto-commit-on-exit.ts. +// +// Only commits if: +// - There are staged changes (git diff --cached is non-empty) +// - There is at least one assistant message to use as commit message +// +// The commit message is derived from the last assistant response, trimmed +// to the first paragraph (max 72 chars for the subject line). +// +// Usage: kit -e examples/extensions/auto-commit.go +func Init(api ext.API) { + api.OnSessionShutdown(func(_ ext.SessionShutdownEvent, ctx ext.Context) { + // Check for staged changes. + diff, err := exec.Command("git", "diff", "--cached", "--quiet").CombinedOutput() + _ = diff + if err == nil { + return // exit code 0 means no staged changes + } + + // Get the last assistant message. + msgs := ctx.GetMessages() + var lastAssistant string + for i := len(msgs) - 1; i >= 0; i-- { + if msgs[i].Role == "assistant" { + lastAssistant = msgs[i].Content + break + } + } + if lastAssistant == "" { + return + } + + // Build commit message: first paragraph, subject line max 72 chars. + subject := firstParagraph(lastAssistant) + if len(subject) > 72 { + subject = subject[:69] + "..." + } + + // Commit. + cmd := exec.Command("git", "commit", "-m", subject) + output, err := cmd.CombinedOutput() + if err != nil { + ctx.PrintError("Auto-commit failed: " + string(output)) + return + } + ctx.PrintInfo("Auto-committed: " + subject) + }) +} + +// firstParagraph returns the first non-empty paragraph of text. +func firstParagraph(text string) string { + text = strings.TrimSpace(text) + // Split on double newlines (paragraph breaks). + parts := strings.SplitN(text, "\n\n", 2) + line := strings.TrimSpace(parts[0]) + // Collapse to single line. + line = strings.ReplaceAll(line, "\n", " ") + return line +} diff --git a/examples/extensions/bookmark.go b/examples/extensions/bookmark.go new file mode 100644 index 00000000..75fd6ef2 --- /dev/null +++ b/examples/extensions/bookmark.go @@ -0,0 +1,79 @@ +//go:build ignore + +package main + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "kit/ext" +) + +// Init adds bookmark commands for marking and recalling important points in +// a conversation. Bookmarks are persisted in the session tree and survive +// restarts. Inspired by Pi's bookmark.ts. +// +// Commands: +// +// /bookmark