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