mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
feat(ui): list loaded extensions in startup banner
- Add ExtensionInfo type and Loaded() method to the public ExtensionAPI so SDK consumers can inspect which extensions are active. - Introduce ui.ExtensionItem and thread ExtensionItems/GetExtensionItems through AppModelOptions, mirroring the existing SkillItem pattern. - Render an [Extensions] row in AddStartupMessageToScrollList showing the filename of each loaded extension (with a (N tools) suffix when extensions register tools). Falls back to tool count only when items are unavailable, and is omitted entirely when no extensions load. - Refresh the list on /reload-ext via a new refreshExtensionItems hook so the banner stays accurate across hot-reloads. - Add buildExtensionItems helper in cmd/root.go that strips .go and resolves subdirectory extensions to their parent dir name, tagging each as project or user scope based on cwd.
This commit is contained in:
+48
-5
@@ -434,6 +434,36 @@ func extensionCommandsForUI(k *kit.Kit) []commands.ExtensionCommand {
|
||||
return cmds
|
||||
}
|
||||
|
||||
// buildExtensionItems converts the loaded extensions into ui.ExtensionItem
|
||||
// values for the startup [Extensions] section. The display name is the file
|
||||
// basename without the .go suffix; for subdirectory extensions the parent
|
||||
// directory name is used (matching `kit extensions list`).
|
||||
func buildExtensionItems(k *kit.Kit, cwd string) []ui.ExtensionItem {
|
||||
infos := k.Extensions().Loaded()
|
||||
if len(infos) == 0 {
|
||||
return nil
|
||||
}
|
||||
items := make([]ui.ExtensionItem, 0, len(infos))
|
||||
for _, info := range infos {
|
||||
name := filepath.Base(info.Path)
|
||||
if name == "main.go" {
|
||||
// Subdirectory extension: use the parent directory's name.
|
||||
name = filepath.Base(filepath.Dir(info.Path))
|
||||
}
|
||||
name = strings.TrimSuffix(name, ".go")
|
||||
source := "user"
|
||||
if cwd != "" && strings.HasPrefix(info.Path, cwd) {
|
||||
source = "project"
|
||||
}
|
||||
items = append(items, ui.ExtensionItem{
|
||||
Name: name,
|
||||
Path: info.Path,
|
||||
Source: source,
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// widgetProviderForUI returns a function that converts extension widgets to
|
||||
// ui.WidgetData for the given placement. Returns nil if extensions are
|
||||
// disabled, which is safe — the UI treats a nil GetWidgets as "no widgets".
|
||||
@@ -942,6 +972,10 @@ func runNormalMode(ctx context.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
// Build extension items from the loaded extensions for the [Extensions]
|
||||
// startup section. Each entry is a single .go file (or a subdir's main.go).
|
||||
extensionItems := buildExtensionItems(kitInstance, cwd)
|
||||
|
||||
// Build prompt template and skill item provider callbacks for hot-reload.
|
||||
// These are called by the TUI when ContentReloadEvent fires.
|
||||
getPromptTemplates := func() []*prompts.PromptTemplate {
|
||||
@@ -986,6 +1020,13 @@ func runNormalMode(ctx context.Context) error {
|
||||
return items
|
||||
}
|
||||
|
||||
// getExtensionItems re-collects the loaded extension list, used by the
|
||||
// TUI after an extension hot-reload to refresh the [Extensions] row.
|
||||
getExtensionItems := func() []ui.ExtensionItem {
|
||||
cwd, _ := os.Getwd()
|
||||
return buildExtensionItems(kitInstance, cwd)
|
||||
}
|
||||
|
||||
// Build extension UI providers once (shared between both modes).
|
||||
getWidgets := widgetProviderForUI(kitInstance)
|
||||
getHeader := headerProviderForUI(kitInstance)
|
||||
@@ -1222,7 +1263,7 @@ func runNormalMode(ctx context.Context) error {
|
||||
|
||||
// Check if running in non-interactive mode
|
||||
if positionalPrompt != "" {
|
||||
return runNonInteractiveModeApp(ctx, appInstance, cli, positionalPrompt, quietFlag, jsonFlag, noExitFlag, modelName, parsedProvider, kitInstance.GetLoadingMessage(), serverNames, toolNames, mcpToolCount, extensionToolCount, usageTracker, extCommands, promptTemplates, contextPaths, skillItems, getPromptTemplates, getSkillItems, getToolNames, getMCPToolCount, mcpPrompts, getMCPPrompts, expandMCPPrompt, getWidgets, getHeader, getFooter, getToolRenderer, getEditorInterceptor, getUIVisibility, getStatusBarEntries, emitBeforeFork, emitBeforeSessionSwitch, getGlobalShortcuts, getExtensionCommands, setModelForUI, emitModelChangeForUI, kitInstance.IsReasoningModel(), kitInstance.GetThinkingLevel(), setThinkingLevelForUI, switchSessionForUI, reloadExtensionsForUI)
|
||||
return runNonInteractiveModeApp(ctx, appInstance, cli, positionalPrompt, quietFlag, jsonFlag, noExitFlag, modelName, parsedProvider, kitInstance.GetLoadingMessage(), serverNames, toolNames, mcpToolCount, extensionToolCount, usageTracker, extCommands, promptTemplates, contextPaths, skillItems, extensionItems, getPromptTemplates, getSkillItems, getExtensionItems, getToolNames, getMCPToolCount, mcpPrompts, getMCPPrompts, expandMCPPrompt, getWidgets, getHeader, getFooter, getToolRenderer, getEditorInterceptor, getUIVisibility, getStatusBarEntries, emitBeforeFork, emitBeforeSessionSwitch, getGlobalShortcuts, getExtensionCommands, setModelForUI, emitModelChangeForUI, kitInstance.IsReasoningModel(), kitInstance.GetThinkingLevel(), setThinkingLevelForUI, switchSessionForUI, reloadExtensionsForUI)
|
||||
}
|
||||
|
||||
// Quiet mode is not allowed in interactive mode
|
||||
@@ -1230,7 +1271,7 @@ func runNormalMode(ctx context.Context) error {
|
||||
return fmt.Errorf("--quiet requires a prompt")
|
||||
}
|
||||
|
||||
return runInteractiveModeBubbleTea(ctx, appInstance, modelName, parsedProvider, kitInstance.GetLoadingMessage(), serverNames, toolNames, mcpToolCount, extensionToolCount, usageTracker, extCommands, promptTemplates, contextPaths, skillItems, getPromptTemplates, getSkillItems, getToolNames, getMCPToolCount, mcpPrompts, getMCPPrompts, expandMCPPrompt, getWidgets, getHeader, getFooter, getToolRenderer, getEditorInterceptor, getUIVisibility, getStatusBarEntries, emitBeforeFork, emitBeforeSessionSwitch, getGlobalShortcuts, getExtensionCommands, setModelForUI, emitModelChangeForUI, kitInstance.IsReasoningModel(), kitInstance.GetThinkingLevel(), setThinkingLevelForUI, switchSessionForUI, reloadExtensionsForUI, startupExtensionMessages)
|
||||
return runInteractiveModeBubbleTea(ctx, appInstance, modelName, parsedProvider, kitInstance.GetLoadingMessage(), serverNames, toolNames, mcpToolCount, extensionToolCount, usageTracker, extCommands, promptTemplates, contextPaths, skillItems, extensionItems, getPromptTemplates, getSkillItems, getExtensionItems, getToolNames, getMCPToolCount, mcpPrompts, getMCPPrompts, expandMCPPrompt, getWidgets, getHeader, getFooter, getToolRenderer, getEditorInterceptor, getUIVisibility, getStatusBarEntries, emitBeforeFork, emitBeforeSessionSwitch, getGlobalShortcuts, getExtensionCommands, setModelForUI, emitModelChangeForUI, kitInstance.IsReasoningModel(), kitInstance.GetThinkingLevel(), setThinkingLevelForUI, switchSessionForUI, reloadExtensionsForUI, startupExtensionMessages)
|
||||
}
|
||||
|
||||
// runNonInteractiveModeApp executes a single prompt via the app layer and exits,
|
||||
@@ -1243,7 +1284,7 @@ func runNormalMode(ctx context.Context) error {
|
||||
//
|
||||
// When --no-exit is set, after the prompt completes the interactive BubbleTea
|
||||
// TUI is started so the user can continue the conversation.
|
||||
func runNonInteractiveModeApp(ctx context.Context, appInstance *app.App, cli *ui.CLI, prompt string, quiet, jsonOutput, noExit bool, modelName, providerName, loadingMessage string, serverNames, toolNames []string, mcpToolCount, extensionToolCount int, usageTracker *ui.UsageTracker, extCommands []commands.ExtensionCommand, promptTemplates []*prompts.PromptTemplate, contextPaths []string, skillItems []ui.SkillItem, getPromptTemplates func() []*prompts.PromptTemplate, getSkillItems func() []ui.SkillItem, getToolNames func() []string, getMCPToolCount func() int, mcpPrompts []ui.MCPPromptInfo, getMCPPrompts func() []ui.MCPPromptInfo, expandMCPPrompt func(string, string, map[string]string) (*ui.MCPPromptExpandResult, error), getWidgets func(string) []ui.WidgetData, getHeader, getFooter func() *ui.WidgetData, getToolRenderer func(string) *ui.ToolRendererData, getEditorInterceptor func() *ui.EditorInterceptor, getUIVisibility func() *ui.UIVisibility, getStatusBarEntries func() []ui.StatusBarEntryData, emitBeforeFork func(string, bool, string) (bool, string), emitBeforeSessionSwitch func(string) (bool, string), getGlobalShortcuts func() map[string]func(), getExtensionCommands func() []commands.ExtensionCommand, setModel func(string) error, emitModelChange func(string, string, string), isReasoningModel bool, thinkingLevel string, setThinkingLevel func(string) error, switchSession func(string) error, reloadExtensions func() error) error {
|
||||
func runNonInteractiveModeApp(ctx context.Context, appInstance *app.App, cli *ui.CLI, prompt string, quiet, jsonOutput, noExit bool, modelName, providerName, loadingMessage string, serverNames, toolNames []string, mcpToolCount, extensionToolCount int, usageTracker *ui.UsageTracker, extCommands []commands.ExtensionCommand, promptTemplates []*prompts.PromptTemplate, contextPaths []string, skillItems []ui.SkillItem, extensionItems []ui.ExtensionItem, getPromptTemplates func() []*prompts.PromptTemplate, getSkillItems func() []ui.SkillItem, getExtensionItems func() []ui.ExtensionItem, getToolNames func() []string, getMCPToolCount func() int, mcpPrompts []ui.MCPPromptInfo, getMCPPrompts func() []ui.MCPPromptInfo, expandMCPPrompt func(string, string, map[string]string) (*ui.MCPPromptExpandResult, error), getWidgets func(string) []ui.WidgetData, getHeader, getFooter func() *ui.WidgetData, getToolRenderer func(string) *ui.ToolRendererData, getEditorInterceptor func() *ui.EditorInterceptor, getUIVisibility func() *ui.UIVisibility, getStatusBarEntries func() []ui.StatusBarEntryData, emitBeforeFork func(string, bool, string) (bool, string), emitBeforeSessionSwitch func(string) (bool, string), getGlobalShortcuts func() map[string]func(), getExtensionCommands func() []commands.ExtensionCommand, setModel func(string) error, emitModelChange func(string, string, string), isReasoningModel bool, thinkingLevel string, setThinkingLevel func(string) error, switchSession func(string) error, reloadExtensions func() error) error {
|
||||
// Expand @file references in the prompt before sending to the agent.
|
||||
// Text files are XML-inlined; binary files are extracted as multimodal parts.
|
||||
var fileParts []kit.LLMFilePart
|
||||
@@ -1304,7 +1345,7 @@ func runNonInteractiveModeApp(ctx context.Context, appInstance *app.App, cli *ui
|
||||
|
||||
// If --no-exit was requested, hand off to the interactive TUI.
|
||||
if noExit {
|
||||
return runInteractiveModeBubbleTea(ctx, appInstance, modelName, providerName, loadingMessage, serverNames, toolNames, mcpToolCount, extensionToolCount, usageTracker, extCommands, promptTemplates, contextPaths, skillItems, getPromptTemplates, getSkillItems, getToolNames, getMCPToolCount, mcpPrompts, getMCPPrompts, expandMCPPrompt, getWidgets, getHeader, getFooter, getToolRenderer, getEditorInterceptor, getUIVisibility, getStatusBarEntries, emitBeforeFork, emitBeforeSessionSwitch, getGlobalShortcuts, getExtensionCommands, setModel, emitModelChange, isReasoningModel, thinkingLevel, setThinkingLevel, switchSession, reloadExtensions, nil)
|
||||
return runInteractiveModeBubbleTea(ctx, appInstance, modelName, providerName, loadingMessage, serverNames, toolNames, mcpToolCount, extensionToolCount, usageTracker, extCommands, promptTemplates, contextPaths, skillItems, extensionItems, getPromptTemplates, getSkillItems, getExtensionItems, getToolNames, getMCPToolCount, mcpPrompts, getMCPPrompts, expandMCPPrompt, getWidgets, getHeader, getFooter, getToolRenderer, getEditorInterceptor, getUIVisibility, getStatusBarEntries, emitBeforeFork, emitBeforeSessionSwitch, getGlobalShortcuts, getExtensionCommands, setModel, emitModelChange, isReasoningModel, thinkingLevel, setThinkingLevel, switchSession, reloadExtensions, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1402,7 +1443,7 @@ func writeJSONError(err error) {
|
||||
// 4. Calls program.Run() which blocks until the user quits (Ctrl+C or /quit).
|
||||
//
|
||||
// SetupCLI is not used for interactive mode; the TUI (AppModel) handles its own rendering.
|
||||
func runInteractiveModeBubbleTea(_ context.Context, appInstance *app.App, modelName, providerName, loadingMessage string, serverNames, toolNames []string, mcpToolCount, extensionToolCount int, usageTracker *ui.UsageTracker, extCommands []commands.ExtensionCommand, promptTemplates []*prompts.PromptTemplate, contextPaths []string, skillItems []ui.SkillItem, getPromptTemplates func() []*prompts.PromptTemplate, getSkillItems func() []ui.SkillItem, getToolNames func() []string, getMCPToolCount func() int, mcpPrompts []ui.MCPPromptInfo, getMCPPrompts func() []ui.MCPPromptInfo, expandMCPPrompt func(string, string, map[string]string) (*ui.MCPPromptExpandResult, error), getWidgets func(string) []ui.WidgetData, getHeader, getFooter func() *ui.WidgetData, getToolRenderer func(string) *ui.ToolRendererData, getEditorInterceptor func() *ui.EditorInterceptor, getUIVisibility func() *ui.UIVisibility, getStatusBarEntries func() []ui.StatusBarEntryData, emitBeforeFork func(string, bool, string) (bool, string), emitBeforeSessionSwitch func(string) (bool, string), getGlobalShortcuts func() map[string]func(), getExtensionCommands func() []commands.ExtensionCommand, setModel func(string) error, emitModelChange func(string, string, string), isReasoningModel bool, thinkingLevel string, setThinkingLevel func(string) error, switchSession func(string) error, reloadExtensions func() error, startupExtensionMessages []string) error {
|
||||
func runInteractiveModeBubbleTea(_ context.Context, appInstance *app.App, modelName, providerName, loadingMessage string, serverNames, toolNames []string, mcpToolCount, extensionToolCount int, usageTracker *ui.UsageTracker, extCommands []commands.ExtensionCommand, promptTemplates []*prompts.PromptTemplate, contextPaths []string, skillItems []ui.SkillItem, extensionItems []ui.ExtensionItem, getPromptTemplates func() []*prompts.PromptTemplate, getSkillItems func() []ui.SkillItem, getExtensionItems func() []ui.ExtensionItem, getToolNames func() []string, getMCPToolCount func() int, mcpPrompts []ui.MCPPromptInfo, getMCPPrompts func() []ui.MCPPromptInfo, expandMCPPrompt func(string, string, map[string]string) (*ui.MCPPromptExpandResult, error), getWidgets func(string) []ui.WidgetData, getHeader, getFooter func() *ui.WidgetData, getToolRenderer func(string) *ui.ToolRendererData, getEditorInterceptor func() *ui.EditorInterceptor, getUIVisibility func() *ui.UIVisibility, getStatusBarEntries func() []ui.StatusBarEntryData, emitBeforeFork func(string, bool, string) (bool, string), emitBeforeSessionSwitch func(string) (bool, string), getGlobalShortcuts func() map[string]func(), getExtensionCommands func() []commands.ExtensionCommand, setModel func(string) error, emitModelChange func(string, string, string), isReasoningModel bool, thinkingLevel string, setThinkingLevel func(string) error, switchSession func(string) error, reloadExtensions func() error, startupExtensionMessages []string) error {
|
||||
// Redirect all log output (stdlib and charm) to a file so that log
|
||||
// messages don't write to stderr and corrupt the TUI. Bubble Tea
|
||||
// captures stdout for rendering; any stray stderr output from
|
||||
@@ -1447,6 +1488,8 @@ func runInteractiveModeBubbleTea(_ context.Context, appInstance *app.App, modelN
|
||||
ContextPaths: contextPaths,
|
||||
SkillItems: skillItems,
|
||||
GetSkillItems: getSkillItems,
|
||||
ExtensionItems: extensionItems,
|
||||
GetExtensionItems: getExtensionItems,
|
||||
StartupExtensionMessages: startupExtensionMessages,
|
||||
GetWidgets: getWidgets,
|
||||
GetHeader: getHeader,
|
||||
|
||||
+55
-2
@@ -135,6 +135,15 @@ type SkillItem struct {
|
||||
Description string // Short summary used in autocomplete and help.
|
||||
}
|
||||
|
||||
// ExtensionItem holds display metadata about a loaded extension for the
|
||||
// startup [Extensions] section. Built by the CLI layer from the SDK's
|
||||
// []kit.ExtensionInfo.
|
||||
type ExtensionItem struct {
|
||||
Name string // Extension display name (filename without .go extension).
|
||||
Path string // Absolute path to the extension's .go file.
|
||||
Source string // "project" or "user" (global).
|
||||
}
|
||||
|
||||
// MCPPromptInfo describes an MCP prompt for display in the TUI (autocomplete,
|
||||
// help). This is a pure UI type — it carries no MCP client dependencies.
|
||||
type MCPPromptInfo struct {
|
||||
@@ -364,6 +373,16 @@ type AppModelOptions struct {
|
||||
// watcher detects changes. May be nil if skill hot-reload is not needed.
|
||||
GetSkillItems func() []SkillItem
|
||||
|
||||
// ExtensionItems lists loaded extensions for the [Extensions] startup
|
||||
// section. Each entry shows the filename of an extension that was
|
||||
// discovered and loaded (global, project-local, or explicit).
|
||||
ExtensionItems []ExtensionItem
|
||||
|
||||
// GetExtensionItems, if non-nil, returns the current extension items.
|
||||
// Called on extension hot-reload to refresh the list. May be nil if no
|
||||
// extensions are loaded.
|
||||
GetExtensionItems func() []ExtensionItem
|
||||
|
||||
// MCPToolCount is the number of tools loaded from external MCP servers.
|
||||
MCPToolCount int
|
||||
|
||||
@@ -608,6 +627,14 @@ type AppModel struct {
|
||||
// skill list after content hot-reload. May be nil.
|
||||
getSkillItems func() []SkillItem
|
||||
|
||||
// extensionItems lists loaded extensions for the [Extensions] startup
|
||||
// section (filenames only).
|
||||
extensionItems []ExtensionItem
|
||||
|
||||
// getExtensionItems returns the current extension items. Used to refresh
|
||||
// the list after extension hot-reload. May be nil.
|
||||
getExtensionItems func() []ExtensionItem
|
||||
|
||||
// mcpToolCount and extensionToolCount track tool counts by source for
|
||||
// the startup info display.
|
||||
mcpToolCount int
|
||||
@@ -861,6 +888,8 @@ func NewAppModel(appCtrl AppController, opts AppModelOptions) *AppModel {
|
||||
m.contextPaths = opts.ContextPaths
|
||||
m.skillItems = opts.SkillItems
|
||||
m.getSkillItems = opts.GetSkillItems
|
||||
m.extensionItems = opts.ExtensionItems
|
||||
m.getExtensionItems = opts.GetExtensionItems
|
||||
m.mcpToolCount = opts.MCPToolCount
|
||||
m.extensionToolCount = opts.ExtensionToolCount
|
||||
m.startupExtensionMessages = opts.StartupExtensionMessages
|
||||
@@ -1029,8 +1058,21 @@ func (m *AppModel) AddStartupMessageToScrollList() {
|
||||
pairs = append(pairs, [2]string{"Skills", strings.Join(names, ", ")})
|
||||
}
|
||||
|
||||
// Extension tool count (only shown when > 0).
|
||||
if m.extensionToolCount > 0 {
|
||||
// Extensions — listed by filename. Each extension shows its basename
|
||||
// without the .go suffix, matching the [Skills] section's style.
|
||||
if len(m.extensionItems) > 0 {
|
||||
names := make([]string, len(m.extensionItems))
|
||||
for i, ei := range m.extensionItems {
|
||||
names[i] = ei.Name
|
||||
}
|
||||
value := strings.Join(names, ", ")
|
||||
if m.extensionToolCount > 0 {
|
||||
value += fmt.Sprintf(" (%d tools)", m.extensionToolCount)
|
||||
}
|
||||
pairs = append(pairs, [2]string{"Extensions", value})
|
||||
} else if m.extensionToolCount > 0 {
|
||||
// Fallback: tool count only (extensions registered tools but the CLI
|
||||
// did not provide ExtensionItems for some reason).
|
||||
pairs = append(pairs, [2]string{"Extensions", fmt.Sprintf("%d tools", m.extensionToolCount)})
|
||||
}
|
||||
|
||||
@@ -2359,6 +2401,7 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if msg.err != nil {
|
||||
m.printSystemMessage(fmt.Sprintf("Extension reload failed: %v", msg.err))
|
||||
} else {
|
||||
m.refreshExtensionItems()
|
||||
m.printSystemMessage("Extensions reloaded.")
|
||||
}
|
||||
|
||||
@@ -3457,6 +3500,16 @@ func (m *AppModel) refreshSkillItems() {
|
||||
}
|
||||
}
|
||||
|
||||
// refreshExtensionItems reloads extension items from the provider callback
|
||||
// so the [Extensions] startup section reflects the current set after a
|
||||
// hot-reload. Called from the extReloadResultMsg handler.
|
||||
func (m *AppModel) refreshExtensionItems() {
|
||||
if m.getExtensionItems == nil {
|
||||
return
|
||||
}
|
||||
m.extensionItems = m.getExtensionItems()
|
||||
}
|
||||
|
||||
// formatSkillDescription returns the autocomplete description for a skill,
|
||||
// prefixed with [project] or [user] so users can tell colliding names apart.
|
||||
func formatSkillDescription(s SkillItem) string {
|
||||
|
||||
@@ -76,6 +76,22 @@ type ExtensionAPI interface {
|
||||
// Lifecycle
|
||||
Reload() error
|
||||
HasExtensions() bool
|
||||
|
||||
// Loaded returns metadata about the extensions currently loaded.
|
||||
Loaded() []ExtensionInfo
|
||||
}
|
||||
|
||||
// ExtensionInfo describes a single loaded extension for display purposes
|
||||
// (e.g. the startup banner or `kit extensions list`).
|
||||
type ExtensionInfo struct {
|
||||
// Path is the absolute path of the extension's .go file.
|
||||
Path string
|
||||
// ToolCount is the number of tools registered by the extension.
|
||||
ToolCount int
|
||||
// CommandCount is the number of slash commands registered.
|
||||
CommandCount int
|
||||
// HandlerCount is the total number of event handlers registered.
|
||||
HandlerCount int
|
||||
}
|
||||
|
||||
// extensionAPI implements ExtensionAPI by wrapping a Kit instance.
|
||||
@@ -456,3 +472,27 @@ func (e *extensionAPI) Reload() error {
|
||||
func (e *extensionAPI) HasExtensions() bool {
|
||||
return e.kit.extRunner != nil
|
||||
}
|
||||
|
||||
func (e *extensionAPI) Loaded() []ExtensionInfo {
|
||||
if e.kit.extRunner == nil {
|
||||
return nil
|
||||
}
|
||||
exts := e.kit.extRunner.Extensions()
|
||||
if len(exts) == 0 {
|
||||
return nil
|
||||
}
|
||||
infos := make([]ExtensionInfo, 0, len(exts))
|
||||
for _, ex := range exts {
|
||||
handlerCount := 0
|
||||
for _, hs := range ex.Handlers {
|
||||
handlerCount += len(hs)
|
||||
}
|
||||
infos = append(infos, ExtensionInfo{
|
||||
Path: ex.Path,
|
||||
ToolCount: len(ex.Tools),
|
||||
CommandCount: len(ex.Commands),
|
||||
HandlerCount: handlerCount,
|
||||
})
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user