combine startup info into single system message block

Merge Context, Skills, and tool counts into one KIT System block
instead of separate styled sections. Add separate MCP and extension
tool counts to Agent, only displaying each when > 0.
This commit is contained in:
Ed Zynda
2026-02-27 17:19:13 +03:00
parent 215a3186ff
commit 2cf7464e76
5 changed files with 100 additions and 58 deletions
+28 -18
View File
@@ -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.
+6 -2
View File
@@ -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.
+13
View File
@@ -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
+11 -3
View File
@@ -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
}
+42 -35
View File
@@ -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.