diff --git a/cmd/root.go b/cmd/root.go index 05bdf248..1d832702 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -85,6 +85,14 @@ func (a *agentUIAdapter) GetLoadedServerNames() []string { return a.agent.GetLoadedServerNames() } +func (a *agentUIAdapter) GetMCPToolCount() int { + return a.agent.GetMCPToolCount() +} + +func (a *agentUIAdapter) GetExtensionToolCount() int { + return a.agent.GetExtensionToolCount() +} + // rootCmd represents the base command when called without any subcommands. // This is the main entry point for the KIT CLI application, providing // an interface to interact with various AI models through a unified interface @@ -365,7 +373,7 @@ func runNormalMode(ctx context.Context) error { // Extract agent + metadata for display and app options. mcpAgent := kitInstance.GetAgent() - parsedProvider, modelName, serverNames, toolNames := CollectAgentMetadata(mcpAgent, mcpConfig) + parsedProvider, modelName, serverNames, toolNames, mcpToolCount, extensionToolCount := CollectAgentMetadata(mcpAgent, mcpConfig) // Create CLI for non-interactive mode only. var cli *ui.CLI @@ -456,7 +464,7 @@ func runNormalMode(ctx context.Context) error { // Check if running in non-interactive mode if promptFlag != "" { - return runNonInteractiveModeApp(ctx, appInstance, cli, promptFlag, quietFlag, noExitFlag, modelName, parsedProvider, mcpAgent.GetLoadingMessage(), serverNames, toolNames, usageTracker, extCommands, contextPaths, skillItems) + return runNonInteractiveModeApp(ctx, appInstance, cli, promptFlag, quietFlag, noExitFlag, modelName, parsedProvider, mcpAgent.GetLoadingMessage(), serverNames, toolNames, mcpToolCount, extensionToolCount, usageTracker, extCommands, contextPaths, skillItems) } // Quiet mode is not allowed in interactive mode @@ -464,7 +472,7 @@ func runNormalMode(ctx context.Context) error { return fmt.Errorf("--quiet flag can only be used with --prompt/-p") } - return runInteractiveModeBubbleTea(ctx, appInstance, modelName, parsedProvider, mcpAgent.GetLoadingMessage(), serverNames, toolNames, usageTracker, extCommands, contextPaths, skillItems) + return runInteractiveModeBubbleTea(ctx, appInstance, modelName, parsedProvider, mcpAgent.GetLoadingMessage(), serverNames, toolNames, mcpToolCount, extensionToolCount, usageTracker, extCommands, contextPaths, skillItems) } // runNonInteractiveModeApp executes a single prompt via the app layer and exits, @@ -477,7 +485,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, noExit bool, modelName, providerName, loadingMessage string, serverNames, toolNames []string, usageTracker *ui.UsageTracker, extCommands []ui.ExtensionCommand, contextPaths []string, skillItems []ui.SkillItem) error { +func runNonInteractiveModeApp(ctx context.Context, appInstance *app.App, cli *ui.CLI, prompt string, quiet, noExit bool, modelName, providerName, loadingMessage string, serverNames, toolNames []string, mcpToolCount, extensionToolCount int, usageTracker *ui.UsageTracker, extCommands []ui.ExtensionCommand, contextPaths []string, skillItems []ui.SkillItem) error { if quiet { // Quiet mode: no intermediate display, just print final response. if err := appInstance.RunOnce(ctx, prompt); err != nil { @@ -503,7 +511,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, usageTracker, extCommands, contextPaths, skillItems) + return runInteractiveModeBubbleTea(ctx, appInstance, modelName, providerName, loadingMessage, serverNames, toolNames, mcpToolCount, extensionToolCount, usageTracker, extCommands, contextPaths, skillItems) } return nil @@ -520,7 +528,7 @@ func runNonInteractiveModeApp(ctx context.Context, appInstance *app.App, cli *ui // 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, usageTracker *ui.UsageTracker, extCommands []ui.ExtensionCommand, contextPaths []string, skillItems []ui.SkillItem) error { +func runInteractiveModeBubbleTea(_ context.Context, appInstance *app.App, modelName, providerName, loadingMessage string, serverNames, toolNames []string, mcpToolCount, extensionToolCount int, usageTracker *ui.UsageTracker, extCommands []ui.ExtensionCommand, contextPaths []string, skillItems []ui.SkillItem) error { // Determine terminal size; fall back gracefully. termWidth, termHeight, err := term.GetSize(int(os.Stdout.Fd())) if err != nil || termWidth == 0 { @@ -529,18 +537,20 @@ func runInteractiveModeBubbleTea(_ context.Context, appInstance *app.App, modelN } appModel := ui.NewAppModel(appInstance, ui.AppModelOptions{ - CompactMode: viper.GetBool("compact"), - ModelName: modelName, - ProviderName: providerName, - LoadingMessage: loadingMessage, - Width: termWidth, - Height: termHeight, - ServerNames: serverNames, - ToolNames: toolNames, - UsageTracker: usageTracker, - ExtensionCommands: extCommands, - ContextPaths: contextPaths, - SkillItems: skillItems, + CompactMode: viper.GetBool("compact"), + ModelName: modelName, + ProviderName: providerName, + LoadingMessage: loadingMessage, + Width: termWidth, + Height: termHeight, + ServerNames: serverNames, + ToolNames: toolNames, + MCPToolCount: mcpToolCount, + ExtensionToolCount: extensionToolCount, + UsageTracker: usageTracker, + ExtensionCommands: extCommands, + ContextPaths: contextPaths, + SkillItems: skillItems, }) // Print startup info to stdout before Bubble Tea takes over the screen. diff --git a/cmd/setup.go b/cmd/setup.go index 0c87baea..c3012537 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -13,7 +13,8 @@ import ( // CollectAgentMetadata extracts model display info and tool/server name lists // from the agent, used to populate app.Options and UI setup. -func CollectAgentMetadata(mcpAgent *agent.Agent, mcpConfig *config.Config) (provider, modelName string, serverNames, toolNames []string) { +// It also returns the number of MCP tools and extension tools separately. +func CollectAgentMetadata(mcpAgent *agent.Agent, mcpConfig *config.Config) (provider, modelName string, serverNames, toolNames []string, mcpToolCount, extensionToolCount int) { modelString := viper.GetString("model") provider, modelName, _ = kit.ParseModelString(modelString) if modelName == "" { @@ -29,7 +30,10 @@ func CollectAgentMetadata(mcpAgent *agent.Agent, mcpConfig *config.Config) (prov toolNames = append(toolNames, info.Name) } - return provider, modelName, serverNames, toolNames + mcpToolCount = mcpAgent.GetMCPToolCount() + extensionToolCount = mcpAgent.GetExtensionToolCount() + + return provider, modelName, serverNames, toolNames, mcpToolCount, extensionToolCount } // BuildAppOptions constructs the app.Options struct from the current state. diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 1f623925..48b34e79 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -455,6 +455,19 @@ func (a *Agent) GetTools() []fantasy.AgentTool { return allTools } +// GetMCPToolCount returns the number of tools loaded from external MCP servers. +func (a *Agent) GetMCPToolCount() int { + if a.toolManager == nil { + return 0 + } + return len(a.toolManager.GetTools()) +} + +// GetExtensionToolCount returns the number of tools registered by extensions. +func (a *Agent) GetExtensionToolCount() int { + return len(a.extraTools) +} + // GetLoadingMessage returns the loading message from provider creation. func (a *Agent) GetLoadingMessage() string { return a.loadingMessage diff --git a/internal/ui/factory.go b/internal/ui/factory.go index a44cbedd..bc51c9dc 100644 --- a/internal/ui/factory.go +++ b/internal/ui/factory.go @@ -14,6 +14,8 @@ type AgentInterface interface { GetLoadingMessage() string GetTools() []any // Using any to avoid importing tool types GetLoadedServerNames() []string // Add this method for debug config + GetMCPToolCount() int // Tools loaded from external MCP servers + GetExtensionToolCount() int // Tools registered by extensions } // CLISetupOptions encapsulates all configuration parameters needed to initialize @@ -120,9 +122,15 @@ func SetupCLI(opts *CLISetupOptions) (*CLI, error) { cli.DisplayInfo(loadingMessage) } - // Display tool count - tools := opts.Agent.GetTools() - cli.DisplayInfo(fmt.Sprintf("Loaded %d tools from MCP servers", len(tools))) + // Display extension tool count (only when > 0). + if extCount := opts.Agent.GetExtensionToolCount(); extCount > 0 { + cli.DisplayInfo(fmt.Sprintf("Loaded %d extension tools", extCount)) + } + + // Display MCP tool count (only when > 0). + if mcpCount := opts.Agent.GetMCPToolCount(); mcpCount > 0 { + cli.DisplayInfo(fmt.Sprintf("Loaded %d tools from MCP servers", mcpCount)) + } return cli, nil } diff --git a/internal/ui/model.go b/internal/ui/model.go index 48fc9f5b..f5938ffc 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -111,6 +111,12 @@ type AppModelOptions struct { // SkillItems lists loaded skills for the [Skills] startup section. SkillItems []SkillItem + + // MCPToolCount is the number of tools loaded from external MCP servers. + MCPToolCount int + + // ExtensionToolCount is the number of tools registered by extensions. + ExtensionToolCount int } // AppModel is the root Bubble Tea model for the interactive TUI. It owns the @@ -192,6 +198,11 @@ type AppModel struct { contextPaths []string skillItems []SkillItem + // mcpToolCount and extensionToolCount track tool counts by source for + // the startup info display. + mcpToolCount int + extensionToolCount int + // width and height track the terminal dimensions. width int height int @@ -261,9 +272,11 @@ func NewAppModel(appCtrl AppController, opts AppModelOptions) *AppModel { // Store extension commands for dispatch. m.extensionCommands = opts.ExtensionCommands - // Store context/skills metadata for startup display. + // Store context/skills metadata and tool counts for startup display. m.contextPaths = opts.ContextPaths m.skillItems = opts.SkillItems + m.mcpToolCount = opts.MCPToolCount + m.extensionToolCount = opts.ExtensionToolCount // Wire up child components now that we have the concrete implementations. m.input = NewInputComponent(width, "Enter your prompt (Type /help for commands, Ctrl+C to quit)", appCtrl) @@ -306,24 +319,12 @@ func (m *AppModel) Init() tea.Cmd { return tea.Batch(cmds...) } -// PrintStartupInfo writes startup messages (model loaded, context, skills, -// tool count) to stdout. Call this before program.Run() so the messages are +// PrintStartupInfo prints the startup banner (model name, context, skills, +// tool counts) to stdout. Call this before program.Run() so the messages are // visible above the Bubble Tea managed region. // -// The output matches the Pi SDK's startup display: -// -// [Context] -// ~/Workspace/project/AGENTS.md -// -// [Skills] -// project -// ~/Workspace/project/.agents/skills/foo/SKILL.md +// All startup information is rendered inside a single system message block. func (m *AppModel) PrintStartupInfo() { - theme := GetTheme() - headerStyle := lipgloss.NewStyle().Foreground(theme.Warning) - dimStyle := lipgloss.NewStyle().Foreground(theme.Muted) - boldStyle := lipgloss.NewStyle().Foreground(theme.Text).Bold(true) - render := func(text string) string { if m.compactMode { return m.compactRdr.RenderSystemMessage(text, time.Now()).Content @@ -333,40 +334,46 @@ func (m *AppModel) PrintStartupInfo() { fmt.Println() + // Build the combined startup content. + var lines []string + if m.providerName != "" && m.modelName != "" { - fmt.Println(render(fmt.Sprintf("Model loaded: %s (%s)", m.providerName, m.modelName))) + lines = append(lines, fmt.Sprintf("Model loaded: %s (%s)", m.providerName, m.modelName)) } if m.loadingMessage != "" { - fmt.Println(render(m.loadingMessage)) + lines = append(lines, m.loadingMessage) } - // [Context] section — loaded AGENTS.md files. + // Context — loaded AGENTS.md files. if len(m.contextPaths) > 0 { - fmt.Println() - fmt.Println(headerStyle.Render("[Context]")) for _, p := range m.contextPaths { - fmt.Println(dimStyle.Render(" " + tildeHome(p))) + lines = append(lines, fmt.Sprintf("Context: %s", tildeHome(p))) } } - // [Skills] section — loaded skills grouped by source. + // Skills — listed by name. if len(m.skillItems) > 0 { - fmt.Println() - fmt.Println(headerStyle.Render("[Skills]")) - // Group by source and display. - var currentSource string - for _, si := range m.skillItems { - if si.Source != currentSource { - currentSource = si.Source - fmt.Println(boldStyle.Render(" " + currentSource)) - } - fmt.Println(dimStyle.Render(" " + tildeHome(si.Path))) + names := make([]string, len(m.skillItems)) + for i, si := range m.skillItems { + names[i] = si.Name } + lines = append(lines, fmt.Sprintf("Skills: %s", strings.Join(names, ", "))) } - fmt.Println() - fmt.Println(render(fmt.Sprintf("Loaded %d tools from MCP servers", len(m.toolNames)))) + // Extension tool count (only shown when > 0). + if m.extensionToolCount > 0 { + lines = append(lines, fmt.Sprintf("Loaded %d extension tools", m.extensionToolCount)) + } + + // MCP tool count (only shown when > 0). + if m.mcpToolCount > 0 { + lines = append(lines, fmt.Sprintf("Loaded %d tools from MCP servers", m.mcpToolCount)) + } + + if len(lines) > 0 { + fmt.Println(render(strings.Join(lines, "\n\n"))) + } } // tildeHome replaces the user's home directory prefix with ~ for display.