2024-12-08 19:27:53 +03:00
package cmd
import (
"context"
2026-03-01 21:16:34 +03:00
"encoding/json"
2024-12-08 19:27:53 +03:00
"fmt"
2026-03-19 18:04:56 +03:00
"image/color"
2025-06-09 14:38:31 +03:00
"log"
2024-12-08 19:27:53 +03:00
"os"
2026-04-07 14:09:59 +03:00
"path/filepath"
2024-12-08 19:27:53 +03:00
"strings"
2026-02-26 01:24:24 +03:00
tea "charm.land/bubbletea/v2"
2026-02-26 16:59:59 +03:00
"github.com/mark3labs/kit/internal/app"
2026-03-25 18:09:36 +03:00
"github.com/mark3labs/kit/internal/auth"
2026-02-26 16:59:59 +03:00
"github.com/mark3labs/kit/internal/config"
2026-02-27 00:08:48 +03:00
"github.com/mark3labs/kit/internal/extensions"
2026-03-02 14:31:35 +03:00
"github.com/mark3labs/kit/internal/models"
2026-03-22 19:09:15 +03:00
"github.com/mark3labs/kit/internal/prompts"
2026-02-26 16:59:59 +03:00
"github.com/mark3labs/kit/internal/ui"
2026-04-01 13:54:10 +03:00
"github.com/mark3labs/kit/internal/ui/commands"
2026-04-14 17:17:01 +03:00
"github.com/mark3labs/kit/internal/ui/progress"
2026-04-07 14:09:59 +03:00
"github.com/mark3labs/kit/internal/watcher"
2026-02-27 10:42:27 +03:00
kit "github.com/mark3labs/kit/pkg/kit"
2024-12-08 19:27:53 +03:00
"github.com/spf13/cobra"
2025-06-09 23:44:01 +03:00
"github.com/spf13/viper"
2026-02-26 01:24:24 +03:00
"golang.org/x/term"
2024-12-08 19:27:53 +03:00
)
var (
2024-12-27 19:10:29 +08:00
configFile string
2025-04-23 23:39:21 +09:00
systemPromptFile string
2025-06-09 14:38:31 +03:00
modelFlag string
2025-06-11 11:45:55 +03:00
providerURL string
providerAPIKey string
2025-06-09 14:38:31 +03:00
debugMode bool
2026-04-15 13:01:36 +03:00
positionalPrompt string // set by processPositionalArgs from CLI positional args
positionalFiles [ ] ui . FilePart // binary @file parts from processPositionalArgs
// MCP resource callbacks, set in runNormalMode, consumed by runInteractiveModeBubbleTea.
mcpGetResources func ( ) [ ] ui . FileSuggestion
mcpResourceReader ui . MCPResourceReader
quietFlag bool
jsonFlag bool
noExitFlag bool
maxSteps int
streamFlag bool // Enable streaming output
autoCompactFlag bool // Enable auto-compaction near context limit
2025-06-11 13:09:51 +03:00
2025-06-25 20:25:14 +03:00
// Session management
2026-02-27 12:11:17 +03:00
sessionPath string
2025-06-25 20:25:14 +03:00
2026-02-26 18:47:10 +03:00
// Tree session management (pi-style)
continueFlag bool // --continue / -c: resume most recent session for cwd
resumeFlag bool // --resume / -r: interactive session picker
noSessionFlag bool // --no-session: ephemeral mode, no persistence
2025-06-11 11:45:55 +03:00
// Model generation parameters
2026-04-06 10:52:33 +03:00
maxTokens int
temperature float32
topP float32
topK int32
frequencyPenalty float32
presencePenalty float32
stopSequences [ ] string
thinkingLevel string
2025-06-13 11:36:10 +03:00
// Ollama-specific parameters
numGPU int32
mainGPU int32
2025-07-24 13:56:33 +03:00
2026-02-27 00:08:48 +03:00
// Extensions control
noExtensionsFlag bool
extensionPaths [ ] string
2025-08-05 21:00:58 +07:00
// TLS configuration
tlsSkipVerify bool
2026-03-22 13:52:06 +03:00
2026-03-22 19:09:15 +03:00
// Prompt templates
promptTemplatePaths [ ] string
noPromptTemplates bool
2026-03-22 13:52:06 +03:00
// Preference restoration flags — set in RunE after cobra parses, used
// in runNormalMode to decide whether to apply saved preferences.
modelFlagChanged bool
thinkingFlagChanged bool
2024-12-10 17:40:02 +03:00
)
2026-02-28 01:01:12 +03:00
// kitUIAdapter adapts *kit.Kit to ui.AgentInterface so the CLI setup layer
// can display tool/server metadata without importing internal types.
type kitUIAdapter struct {
kit * kit . Kit
2025-06-27 17:41:18 +03:00
}
2026-02-28 01:01:12 +03:00
func ( a * kitUIAdapter ) GetLoadingMessage ( ) string {
return a . kit . GetLoadingMessage ( )
2025-06-27 17:41:18 +03:00
}
2026-02-28 01:01:12 +03:00
func ( a * kitUIAdapter ) GetTools ( ) [ ] any {
names := a . kit . GetToolNames ( )
result := make ( [ ] any , len ( names ) )
for i , name := range names {
result [ i ] = name
2025-06-27 17:41:18 +03:00
}
return result
}
2026-02-28 01:01:12 +03:00
func ( a * kitUIAdapter ) GetLoadedServerNames ( ) [ ] string {
return a . kit . GetLoadedServerNames ( )
2025-06-27 17:41:18 +03:00
}
2026-02-28 01:01:12 +03:00
func ( a * kitUIAdapter ) GetMCPToolCount ( ) int {
return a . kit . GetMCPToolCount ( )
2026-02-27 17:19:13 +03:00
}
2026-02-28 01:01:12 +03:00
func ( a * kitUIAdapter ) GetExtensionToolCount ( ) int {
return a . kit . GetExtensionToolCount ( )
2026-02-27 17:19:13 +03:00
}
2025-11-12 16:48:46 +03:00
// rootCmd represents the base command when called without any subcommands.
2026-02-26 16:59:59 +03:00
// This is the main entry point for the KIT CLI application, providing
2025-11-12 16:48:46 +03:00
// an interface to interact with various AI models through a unified interface
// with support for MCP servers and tool integration.
2024-12-08 19:27:53 +03:00
var rootCmd = & cobra . Command {
2026-03-05 18:57:00 +03:00
Use : "kit [@file...] [prompt]" ,
2024-12-20 18:15:14 +03:00
Short : "Chat with AI models through a unified interface" ,
2026-02-26 18:09:26 +03:00
Long : ` KIT (Knowledge Inference Tool) — A lightweight AI agent for coding ` ,
2026-03-05 18:57:00 +03:00
Args : cobra . ArbitraryArgs ,
2024-12-08 19:27:53 +03:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2026-03-05 18:57:00 +03:00
// Parse positional args: @-prefixed args are file attachments,
// remaining args form the prompt (like Pi: kit @code.ts "Review this").
if len ( args ) > 0 {
processPositionalArgs ( args )
}
2026-03-22 13:52:06 +03:00
// Record whether --model / --thinking-level were explicitly set by the
// user so that runNormalMode can fall back to saved preferences when
// they weren't. Must be captured here (after cobra parses) and before
// runKit because rootCmd can't be referenced inside runNormalMode
// without creating an initialization cycle.
if f := cmd . PersistentFlags ( ) . Lookup ( "model" ) ; f != nil {
modelFlagChanged = f . Changed
}
if f := cmd . PersistentFlags ( ) . Lookup ( "thinking-level" ) ; f != nil {
thinkingFlagChanged = f . Changed
}
2026-02-26 16:59:59 +03:00
return runKit ( context . Background ( ) )
2024-12-08 19:27:53 +03:00
} ,
}
2025-11-12 16:48:46 +03:00
// GetRootCommand returns the root command with the version set.
2026-02-26 16:59:59 +03:00
// This function is the main entry point for the KIT CLI and should be
2025-11-12 16:48:46 +03:00
// called from main.go with the appropriate version string.
2025-08-11 20:10:20 +03:00
func GetRootCommand ( v string ) * cobra . Command {
2025-06-25 18:18:29 +03:00
rootCmd . Version = v
2025-08-11 20:10:20 +03:00
return rootCmd
2024-12-08 19:27:53 +03:00
}
2026-02-26 16:59:59 +03:00
// InitConfig initializes the configuration for KIT by loading config files,
2026-02-27 18:17:32 +03:00
// environment variables. It delegates to the SDK's
2026-02-27 10:42:27 +03:00
// InitConfig, injecting the CLI-specific configFile flag and debug mode.
2025-11-12 16:48:46 +03:00
// This function is automatically called by cobra before command execution.
2025-09-02 15:42:37 +03:00
func InitConfig ( ) {
2026-02-27 10:42:27 +03:00
if err := kit . InitConfig ( configFile , debugMode ) ; err != nil {
fmt . Fprintf ( os . Stderr , "%v\n" , err )
os . Exit ( 1 )
2025-06-17 21:52:18 +03:00
}
2026-04-03 12:37:14 +03:00
// Rebuild the model registry now that viper has the config loaded,
// so customModels defined in the config file are picked up.
models . ReloadGlobalRegistry ( )
2025-06-17 21:52:18 +03:00
}
2026-03-19 18:04:56 +03:00
// adaptiveOrDefault converts a config.AdaptiveColor to a resolved color.Color,
// falling back to fallback when both Light and Dark are empty.
func adaptiveOrDefault ( ac config . AdaptiveColor , fallback color . Color ) color . Color {
if ac . Light == "" && ac . Dark == "" {
return fallback
}
return ui . AdaptiveColor ( ac . Light , ac . Dark )
}
func configToUiTheme ( cfg config . Theme ) ui . Theme {
def := ui . DefaultTheme ( )
2025-09-02 09:30:20 +02:00
return ui . Theme {
2026-03-19 18:04:56 +03:00
Primary : adaptiveOrDefault ( cfg . Primary , def . Primary ) ,
Secondary : adaptiveOrDefault ( cfg . Secondary , def . Secondary ) ,
Success : adaptiveOrDefault ( cfg . Success , def . Success ) ,
Warning : adaptiveOrDefault ( cfg . Warning , def . Warning ) ,
Error : adaptiveOrDefault ( cfg . Error , def . Error ) ,
Info : adaptiveOrDefault ( cfg . Info , def . Info ) ,
Text : adaptiveOrDefault ( cfg . Text , def . Text ) ,
Muted : adaptiveOrDefault ( cfg . Muted , def . Muted ) ,
VeryMuted : adaptiveOrDefault ( cfg . VeryMuted , def . VeryMuted ) ,
Background : adaptiveOrDefault ( cfg . Background , def . Background ) ,
Border : adaptiveOrDefault ( cfg . Border , def . Border ) ,
MutedBorder : adaptiveOrDefault ( cfg . MutedBorder , def . MutedBorder ) ,
System : adaptiveOrDefault ( cfg . System , def . System ) ,
Tool : adaptiveOrDefault ( cfg . Tool , def . Tool ) ,
Accent : adaptiveOrDefault ( cfg . Accent , def . Accent ) ,
Highlight : adaptiveOrDefault ( cfg . Highlight , def . Highlight ) ,
DiffInsertBg : adaptiveOrDefault ( cfg . DiffInsertBg , def . DiffInsertBg ) ,
DiffDeleteBg : adaptiveOrDefault ( cfg . DiffDeleteBg , def . DiffDeleteBg ) ,
DiffEqualBg : adaptiveOrDefault ( cfg . DiffEqualBg , def . DiffEqualBg ) ,
DiffMissingBg : adaptiveOrDefault ( cfg . DiffMissingBg , def . DiffMissingBg ) ,
CodeBg : adaptiveOrDefault ( cfg . CodeBg , def . CodeBg ) ,
GutterBg : adaptiveOrDefault ( cfg . GutterBg , def . GutterBg ) ,
WriteBg : adaptiveOrDefault ( cfg . WriteBg , def . WriteBg ) ,
Markdown : ui . MarkdownThemeColors {
Text : adaptiveOrDefault ( cfg . Markdown . Text , def . Markdown . Text ) ,
Muted : adaptiveOrDefault ( cfg . Markdown . Muted , def . Markdown . Muted ) ,
Heading : adaptiveOrDefault ( cfg . Markdown . Heading , def . Markdown . Heading ) ,
Emph : adaptiveOrDefault ( cfg . Markdown . Emph , def . Markdown . Emph ) ,
Strong : adaptiveOrDefault ( cfg . Markdown . Strong , def . Markdown . Strong ) ,
Link : adaptiveOrDefault ( cfg . Markdown . Link , def . Markdown . Link ) ,
Code : adaptiveOrDefault ( cfg . Markdown . Code , def . Markdown . Code ) ,
Error : adaptiveOrDefault ( cfg . Markdown . Error , def . Markdown . Error ) ,
Keyword : adaptiveOrDefault ( cfg . Markdown . Keyword , def . Markdown . Keyword ) ,
String : adaptiveOrDefault ( cfg . Markdown . String , def . Markdown . String ) ,
Number : adaptiveOrDefault ( cfg . Markdown . Number , def . Markdown . Number ) ,
Comment : adaptiveOrDefault ( cfg . Markdown . Comment , def . Markdown . Comment ) ,
} ,
2025-09-02 09:30:20 +02:00
}
}
2026-04-01 00:39:32 +03:00
// kitBanner returns the KIT ASCII art title with KITT scanner lights.
// Delegates to ui.KitBanner() which owns the logo rendering.
2026-02-26 17:15:55 +03:00
func kitBanner ( ) string {
2026-04-01 00:39:32 +03:00
return ui . KitBanner ( )
2026-02-26 17:15:55 +03:00
}
2024-12-08 19:27:53 +03:00
func init ( ) {
2025-09-02 15:42:37 +03:00
cobra . OnInitialize ( InitConfig )
2026-02-26 17:15:55 +03:00
rootCmd . Long = kitBanner ( ) + "\n\n" + rootCmd . Long
2025-09-02 09:30:20 +02:00
var theme config . Theme
err := config . FilepathOr ( "theme" , & theme )
if err == nil && viper . InConfig ( "theme" ) {
uiTheme := configToUiTheme ( theme )
ui . SetTheme ( uiTheme )
2026-03-21 21:01:25 +03:00
} else if pref := ui . LoadThemePreference ( ) ; pref != "" {
// No explicit theme in config — fall back to persisted preference.
_ = ui . ApplyThemeWithoutSave ( pref )
2025-09-02 09:30:20 +02:00
}
2025-06-17 21:52:18 +03:00
2024-12-08 19:27:53 +03:00
rootCmd . PersistentFlags ( ) .
2026-02-26 16:59:59 +03:00
StringVar ( & configFile , "config" , "" , "config file (default is $HOME/.kit.yml)" )
2025-04-23 23:39:21 +09:00
rootCmd . PersistentFlags ( ) .
2025-06-11 11:45:55 +03:00
StringVar ( & systemPromptFile , "system-prompt" , "" , "system prompt text or path to text file" )
2024-12-20 18:15:14 +03:00
rootCmd . PersistentFlags ( ) .
2026-02-25 18:41:49 +03:00
StringVarP ( & modelFlag , "model" , "m" , "anthropic/claude-sonnet-4-5-20250929" ,
"model to use (format: provider/model)" )
2024-12-20 18:15:14 +03:00
rootCmd . PersistentFlags ( ) .
BoolVar ( & debugMode , "debug" , false , "enable debug logging" )
2026-03-05 19:03:47 +03:00
2025-06-09 17:02:08 +03:00
rootCmd . PersistentFlags ( ) .
2026-03-05 19:00:51 +03:00
BoolVar ( & quietFlag , "quiet" , false , "suppress all output (non-interactive mode only)" )
2026-03-01 21:16:34 +03:00
rootCmd . PersistentFlags ( ) .
2026-03-05 19:00:51 +03:00
BoolVar ( & jsonFlag , "json" , false , "output response as JSON (non-interactive mode only)" )
2025-06-16 17:35:01 +03:00
rootCmd . PersistentFlags ( ) .
2026-03-05 19:00:51 +03:00
BoolVar ( & noExitFlag , "no-exit" , false , "enter interactive mode after non-interactive prompt completes" )
2025-06-09 23:44:01 +03:00
rootCmd . PersistentFlags ( ) .
IntVar ( & maxSteps , "max-steps" , 0 , "maximum number of agent steps (0 for unlimited)" )
2025-06-26 18:15:17 +03:00
rootCmd . PersistentFlags ( ) .
BoolVar ( & streamFlag , "stream" , true , "enable streaming output for faster response display" )
2026-02-27 12:38:07 +03:00
rootCmd . PersistentFlags ( ) .
BoolVar ( & autoCompactFlag , "auto-compact" , false , "auto-compact conversation when near context limit" )
2025-06-25 20:25:14 +03:00
rootCmd . PersistentFlags ( ) .
2026-02-27 12:11:17 +03:00
StringVarP ( & sessionPath , "session" , "s" , "" , "open a specific JSONL session file" )
2026-02-26 18:47:10 +03:00
rootCmd . PersistentFlags ( ) .
BoolVarP ( & continueFlag , "continue" , "c" , false , "continue the most recent session for the current directory" )
rootCmd . PersistentFlags ( ) .
BoolVarP ( & resumeFlag , "resume" , "r" , false , "interactive session picker" )
rootCmd . PersistentFlags ( ) .
BoolVar ( & noSessionFlag , "no-session" , false , "ephemeral mode — no session persistence" )
2026-02-27 00:08:48 +03:00
rootCmd . PersistentFlags ( ) .
2026-02-27 18:17:32 +03:00
BoolVar ( & noExtensionsFlag , "no-extensions" , false , "disable all extensions" )
2026-02-27 00:08:48 +03:00
rootCmd . PersistentFlags ( ) .
StringSliceVarP ( & extensionPaths , "extension" , "e" , nil , "load additional extension file(s)" )
2025-06-25 20:25:14 +03:00
2024-12-27 19:10:29 +08:00
flags := rootCmd . PersistentFlags ( )
2025-06-11 11:45:55 +03:00
flags . StringVar ( & providerURL , "provider-url" , "" , "base URL for the provider API (applies to OpenAI, Anthropic, Ollama, and Google)" )
flags . StringVar ( & providerAPIKey , "provider-api-key" , "" , "API key for the provider (applies to OpenAI, Anthropic, and Google)" )
2025-08-05 21:00:58 +07:00
flags . BoolVar ( & tlsSkipVerify , "tls-skip-verify" , false , "skip TLS certificate verification (WARNING: insecure, use only for self-signed certificates)" )
2025-06-11 11:45:55 +03:00
2026-03-22 19:09:15 +03:00
// Prompt template flags
flags . StringArrayVar ( & promptTemplatePaths , "prompt-template" , nil , "load prompt template file or directory (repeatable)" )
flags . BoolVar ( & noPromptTemplates , "no-prompt-templates" , false , "disable prompt template discovery" )
2025-06-11 11:45:55 +03:00
// Model generation parameters
2026-04-16 23:12:10 +03:00
flags . IntVar ( & maxTokens , "max-tokens" , 8192 , "maximum number of output tokens per response (auto-raised up to 32768 for models with higher known output limits; see internal/models/embedded_models.json)" )
2025-06-11 11:45:55 +03:00
flags . Float32Var ( & temperature , "temperature" , 0.7 , "controls randomness in responses (0.0-1.0)" )
flags . Float32Var ( & topP , "top-p" , 0.95 , "controls diversity via nucleus sampling (0.0-1.0)" )
flags . Int32Var ( & topK , "top-k" , 40 , "controls diversity by limiting top K tokens to sample from" )
2026-04-06 10:52:33 +03:00
flags . Float32Var ( & frequencyPenalty , "frequency-penalty" , 0.0 , "penalizes tokens based on frequency of appearance (0.0-2.0)" )
flags . Float32Var ( & presencePenalty , "presence-penalty" , 0.0 , "penalizes tokens based on whether they have appeared (0.0-2.0)" )
2025-06-11 11:45:55 +03:00
flags . StringSliceVar ( & stopSequences , "stop-sequences" , nil , "custom stop sequences (comma-separated)" )
2026-04-21 20:19:00 +03:00
flags . StringVar ( & thinkingLevel , "thinking-level" , "off" , "extended thinking level: off, none, minimal, low, medium, high" )
2025-06-09 23:44:01 +03:00
2025-06-13 11:36:10 +03:00
// Ollama-specific parameters
2025-06-26 13:32:18 +03:00
flags . Int32Var ( & numGPU , "num-gpu-layers" , - 1 , "number of model layers to offload to GPU for Ollama models (-1 for auto-detect)" )
2026-02-25 22:51:45 +03:00
_ = flags . MarkHidden ( "num-gpu-layers" ) // Advanced option, hidden from help
2025-06-26 13:32:18 +03:00
flags . Int32Var ( & mainGPU , "main-gpu" , 0 , "main GPU device to use for Ollama models" )
2025-06-13 11:36:10 +03:00
2025-06-09 23:44:01 +03:00
// Bind flags to viper for config file support
2026-02-25 22:51:45 +03:00
_ = viper . BindPFlag ( "system-prompt" , rootCmd . PersistentFlags ( ) . Lookup ( "system-prompt" ) )
_ = viper . BindPFlag ( "model" , rootCmd . PersistentFlags ( ) . Lookup ( "model" ) )
_ = viper . BindPFlag ( "debug" , rootCmd . PersistentFlags ( ) . Lookup ( "debug" ) )
_ = viper . BindPFlag ( "max-steps" , rootCmd . PersistentFlags ( ) . Lookup ( "max-steps" ) )
_ = viper . BindPFlag ( "stream" , rootCmd . PersistentFlags ( ) . Lookup ( "stream" ) )
2026-02-27 12:38:07 +03:00
_ = viper . BindPFlag ( "auto-compact" , rootCmd . PersistentFlags ( ) . Lookup ( "auto-compact" ) )
2026-02-26 09:47:10 +03:00
2026-02-25 22:51:45 +03:00
_ = viper . BindPFlag ( "provider-url" , rootCmd . PersistentFlags ( ) . Lookup ( "provider-url" ) )
_ = viper . BindPFlag ( "provider-api-key" , rootCmd . PersistentFlags ( ) . Lookup ( "provider-api-key" ) )
_ = viper . BindPFlag ( "max-tokens" , rootCmd . PersistentFlags ( ) . Lookup ( "max-tokens" ) )
_ = viper . BindPFlag ( "temperature" , rootCmd . PersistentFlags ( ) . Lookup ( "temperature" ) )
_ = viper . BindPFlag ( "top-p" , rootCmd . PersistentFlags ( ) . Lookup ( "top-p" ) )
_ = viper . BindPFlag ( "top-k" , rootCmd . PersistentFlags ( ) . Lookup ( "top-k" ) )
2026-04-06 10:52:33 +03:00
_ = viper . BindPFlag ( "frequency-penalty" , rootCmd . PersistentFlags ( ) . Lookup ( "frequency-penalty" ) )
_ = viper . BindPFlag ( "presence-penalty" , rootCmd . PersistentFlags ( ) . Lookup ( "presence-penalty" ) )
2026-02-25 22:51:45 +03:00
_ = viper . BindPFlag ( "stop-sequences" , rootCmd . PersistentFlags ( ) . Lookup ( "stop-sequences" ) )
2026-03-07 21:27:46 +03:00
_ = viper . BindPFlag ( "thinking-level" , rootCmd . PersistentFlags ( ) . Lookup ( "thinking-level" ) )
2026-02-25 22:51:45 +03:00
_ = viper . BindPFlag ( "num-gpu-layers" , rootCmd . PersistentFlags ( ) . Lookup ( "num-gpu-layers" ) )
_ = viper . BindPFlag ( "main-gpu" , rootCmd . PersistentFlags ( ) . Lookup ( "main-gpu" ) )
_ = viper . BindPFlag ( "tls-skip-verify" , rootCmd . PersistentFlags ( ) . Lookup ( "tls-skip-verify" ) )
2026-02-27 00:08:48 +03:00
_ = viper . BindPFlag ( "no-extensions" , rootCmd . PersistentFlags ( ) . Lookup ( "no-extensions" ) )
_ = viper . BindPFlag ( "extension" , rootCmd . PersistentFlags ( ) . Lookup ( "extension" ) )
2026-03-22 19:09:15 +03:00
_ = viper . BindPFlag ( "prompt-template" , rootCmd . PersistentFlags ( ) . Lookup ( "prompt-template" ) )
_ = viper . BindPFlag ( "no-prompt-templates" , rootCmd . PersistentFlags ( ) . Lookup ( "no-prompt-templates" ) )
2025-06-11 10:26:52 +03:00
2025-06-17 21:52:18 +03:00
// Defaults are already set in flag definitions, no need to duplicate in viper
2025-06-25 14:27:19 +03:00
// Add subcommands
rootCmd . AddCommand ( authCmd )
2024-12-20 18:15:14 +03:00
}
2026-03-05 18:57:00 +03:00
// processPositionalArgs separates positional CLI arguments into @file
2026-04-15 13:01:36 +03:00
// attachments and prompt text. Text file content is read and prepended to
// positionalPrompt; binary files (images, audio) are stored in positionalFiles
// for multimodal submission. Positional args are the primary way to run
// non-interactive mode:
2026-03-05 18:57:00 +03:00
//
2026-03-05 19:00:51 +03:00
// kit "Explain this codebase"
2026-03-05 18:57:00 +03:00
// kit @code.ts @test.ts "Review these files"
2026-04-15 13:01:36 +03:00
// kit @screenshot.png "What's in this image?"
2026-03-05 18:57:00 +03:00
func processPositionalArgs ( args [ ] string ) {
cwd , err := os . Getwd ( )
if err != nil {
cwd = "."
}
var fileTokens [ ] string
var promptParts [ ] string
for _ , arg := range args {
if strings . HasPrefix ( arg , "@" ) && len ( arg ) > 1 {
fileTokens = append ( fileTokens , arg )
} else {
promptParts = append ( promptParts , arg )
}
}
// Build file content prefix from @file arguments.
2026-04-15 13:01:36 +03:00
// Text files are XML-wrapped inline; binary files become multimodal parts.
2026-03-05 18:57:00 +03:00
var fileContent strings . Builder
for _ , token := range fileTokens {
2026-04-15 13:01:36 +03:00
result := ui . ProcessFileAttachments ( token , cwd )
if result . ProcessedText != token {
// Text file was resolved — add it.
fileContent . WriteString ( result . ProcessedText )
2026-03-05 18:57:00 +03:00
fileContent . WriteString ( "\n\n" )
}
2026-04-15 13:01:36 +03:00
// Collect binary file parts for multimodal submission.
positionalFiles = append ( positionalFiles , result . FileParts ... )
2026-03-05 18:57:00 +03:00
}
2026-03-05 19:00:51 +03:00
// Combine: positional prompt text is appended to any existing --prompt
// value (for backward compat with subprocess invocations).
2026-03-05 18:57:00 +03:00
if len ( promptParts ) > 0 {
extra := strings . Join ( promptParts , " " )
2026-03-05 19:03:47 +03:00
if positionalPrompt != "" {
positionalPrompt = positionalPrompt + " " + extra
2026-03-05 18:57:00 +03:00
} else {
2026-03-05 19:03:47 +03:00
positionalPrompt = extra
2026-03-05 18:57:00 +03:00
}
}
// Prepend file content to the prompt.
if fileContent . Len ( ) > 0 {
2026-03-05 19:03:47 +03:00
if positionalPrompt == "" {
positionalPrompt = strings . TrimSpace ( fileContent . String ( ) )
2026-03-05 18:57:00 +03:00
} else {
2026-03-05 19:03:47 +03:00
positionalPrompt = strings . TrimSpace ( fileContent . String ( ) ) + "\n\n" + positionalPrompt
2026-03-05 18:57:00 +03:00
}
}
}
2026-02-26 16:59:59 +03:00
func runKit ( ctx context . Context ) error {
2025-06-09 18:51:06 +03:00
return runNormalMode ( ctx )
}
2026-02-27 00:26:06 +03:00
// extensionCommandsForUI converts extension-registered CommandDefs into the
2026-04-01 13:54:10 +03:00
// commands.ExtensionCommand type used by the interactive TUI. Command names are
2026-02-27 00:26:06 +03:00
// normalised to start with "/" so they integrate with the slash-command
// autocomplete and dispatch pipeline.
2026-04-01 13:54:10 +03:00
func extensionCommandsForUI ( k * kit . Kit ) [ ] commands . ExtensionCommand {
2026-03-29 13:19:51 +03:00
defs := k . Extensions ( ) . Commands ( )
2026-02-27 00:26:06 +03:00
if len ( defs ) == 0 {
return nil
}
2026-04-01 13:54:10 +03:00
cmds := make ( [ ] commands . ExtensionCommand , 0 , len ( defs ) )
2026-02-27 00:26:06 +03:00
for _ , d := range defs {
name := d . Name
if len ( name ) > 0 && name [ 0 ] != '/' {
name = "/" + name
}
2026-04-01 13:54:10 +03:00
ec := commands . ExtensionCommand {
2026-02-27 00:26:06 +03:00
Name : name ,
Description : d . Description ,
2026-02-27 00:41:48 +03:00
Execute : func ( args string ) ( string , error ) {
2026-03-29 13:19:51 +03:00
return d . Execute ( args , k . Extensions ( ) . GetContext ( ) )
2026-02-27 00:41:48 +03:00
} ,
2026-03-02 15:37:52 +03:00
}
if d . Complete != nil {
ec . Complete = func ( prefix string ) [ ] string {
2026-03-29 13:19:51 +03:00
return d . Complete ( prefix , k . Extensions ( ) . GetContext ( ) )
2026-03-02 15:37:52 +03:00
}
}
cmds = append ( cmds , ec )
2026-02-27 00:26:06 +03:00
}
return cmds
}
2026-02-28 12:16:20 +03:00
// 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".
func widgetProviderForUI ( k * kit . Kit ) func ( string ) [ ] ui . WidgetData {
2026-03-29 13:19:51 +03:00
if ! k . Extensions ( ) . HasExtensions ( ) {
2026-02-28 12:16:20 +03:00
return nil
}
return func ( placement string ) [ ] ui . WidgetData {
2026-03-29 13:19:51 +03:00
configs := k . Extensions ( ) . GetWidgets ( extensions . WidgetPlacement ( placement ) )
2026-02-28 12:16:20 +03:00
if len ( configs ) == 0 {
return nil
}
widgets := make ( [ ] ui . WidgetData , len ( configs ) )
for i , c := range configs {
widgets [ i ] = ui . WidgetData {
Text : c . Content . Text ,
Markdown : c . Content . Markdown ,
BorderColor : c . Style . BorderColor ,
NoBorder : c . Style . NoBorder ,
}
}
return widgets
}
}
2026-03-29 11:34:16 +03:00
// headerFooterProviderForUI returns a provider func that maps an
// extensions.HeaderFooterConfig getter into the ui.WidgetData shape
// expected by AppModel. The getter argument selects header vs footer.
func headerFooterProviderForUI ( k * kit . Kit , getter func ( ) * extensions . HeaderFooterConfig ) func ( ) * ui . WidgetData {
2026-03-29 13:19:51 +03:00
if ! k . Extensions ( ) . HasExtensions ( ) {
2026-02-28 14:11:52 +03:00
return nil
}
return func ( ) * ui . WidgetData {
2026-03-29 11:34:16 +03:00
cfg := getter ( )
if cfg == nil {
2026-02-28 14:11:52 +03:00
return nil
}
return & ui . WidgetData {
2026-03-29 11:34:16 +03:00
Text : cfg . Content . Text ,
Markdown : cfg . Content . Markdown ,
BorderColor : cfg . Style . BorderColor ,
NoBorder : cfg . Style . NoBorder ,
2026-02-28 14:11:52 +03:00
}
}
}
2026-03-29 11:34:16 +03:00
// headerProviderForUI returns a function that converts the extension header
// to a *ui.WidgetData for the TUI. Returns nil if extensions are disabled,
// which is safe — the UI treats a nil GetHeader as "no header".
func headerProviderForUI ( k * kit . Kit ) func ( ) * ui . WidgetData {
2026-03-29 13:19:51 +03:00
return headerFooterProviderForUI ( k , func ( ) * extensions . HeaderFooterConfig {
return k . Extensions ( ) . GetHeader ( )
} )
2026-03-29 11:34:16 +03:00
}
2026-02-28 16:23:45 +03:00
// toolRendererProviderForUI returns a function that converts extension tool
// renderers to ui.ToolRendererData for the TUI. Returns nil if extensions are
// disabled, which is safe — the UI treats a nil GetToolRenderer as "no
// custom renderers".
func toolRendererProviderForUI ( k * kit . Kit ) func ( string ) * ui . ToolRendererData {
2026-03-29 13:19:51 +03:00
if ! k . Extensions ( ) . HasExtensions ( ) {
2026-02-28 16:23:45 +03:00
return nil
}
return func ( toolName string ) * ui . ToolRendererData {
2026-03-29 13:19:51 +03:00
config := k . Extensions ( ) . GetToolRenderer ( toolName )
2026-02-28 16:23:45 +03:00
if config == nil {
return nil
}
return & ui . ToolRendererData {
DisplayName : config . DisplayName ,
BorderColor : config . BorderColor ,
Background : config . Background ,
BodyMarkdown : config . BodyMarkdown ,
RenderHeader : config . RenderHeader ,
RenderBody : config . RenderBody ,
}
}
}
2026-02-28 17:46:41 +03:00
// editorInterceptorProviderForUI returns a function that converts the
// extension editor interceptor to a *ui.EditorInterceptor for the TUI.
// Returns nil if extensions are disabled, which is safe — the UI treats a
// nil GetEditorInterceptor as "no interceptor".
func editorInterceptorProviderForUI ( k * kit . Kit ) func ( ) * ui . EditorInterceptor {
2026-03-29 13:19:51 +03:00
if ! k . Extensions ( ) . HasExtensions ( ) {
2026-02-28 17:46:41 +03:00
return nil
}
return func ( ) * ui . EditorInterceptor {
2026-03-29 13:19:51 +03:00
config := k . Extensions ( ) . GetEditor ( )
2026-02-28 17:46:41 +03:00
if config == nil {
return nil
}
var handleKey func ( string , string ) ui . EditorKeyAction
if config . HandleKey != nil {
extHandleKey := config . HandleKey
handleKey = func ( key , text string ) ui . EditorKeyAction {
r := extHandleKey ( key , text )
return ui . EditorKeyAction {
Type : ui . EditorKeyActionType ( r . Type ) ,
RemappedKey : r . RemappedKey ,
SubmitText : r . SubmitText ,
}
}
}
var render func ( int , string ) string
if config . Render != nil {
extRender := config . Render
render = func ( width int , defaultContent string ) string {
return extRender ( width , defaultContent )
}
}
return & ui . EditorInterceptor {
HandleKey : handleKey ,
Render : render ,
}
}
}
2026-03-01 15:24:48 +03:00
// uiVisibilityProviderForUI returns a function that converts extension UI
// visibility overrides to a *ui.UIVisibility for the TUI. Returns nil if
// extensions are disabled — the UI treats nil as "show everything".
func uiVisibilityProviderForUI ( k * kit . Kit ) func ( ) * ui . UIVisibility {
2026-03-29 13:19:51 +03:00
if ! k . Extensions ( ) . HasExtensions ( ) {
2026-03-01 15:24:48 +03:00
return nil
}
return func ( ) * ui . UIVisibility {
2026-03-29 13:19:51 +03:00
v := k . Extensions ( ) . GetUIVisibility ( )
2026-03-01 15:24:48 +03:00
if v == nil {
return nil
}
return & ui . UIVisibility {
HideStartupMessage : v . HideStartupMessage ,
HideStatusBar : v . HideStatusBar ,
HideSeparator : v . HideSeparator ,
HideInputHint : v . HideInputHint ,
}
}
}
2026-02-28 14:11:52 +03:00
// footerProviderForUI returns a function that converts the extension footer
// to a *ui.WidgetData for the TUI. Returns nil if extensions are disabled,
// which is safe — the UI treats a nil GetFooter as "no footer".
func footerProviderForUI ( k * kit . Kit ) func ( ) * ui . WidgetData {
2026-03-29 13:19:51 +03:00
return headerFooterProviderForUI ( k , func ( ) * extensions . HeaderFooterConfig {
return k . Extensions ( ) . GetFooter ( )
} )
2026-02-28 14:11:52 +03:00
}
2026-03-02 01:33:56 +03:00
// statusBarProviderForUI returns a function that fetches extension status bar
// entries and converts them to ui.StatusBarEntryData for the TUI. Returns nil
// if extensions are disabled, which is safe — the TUI treats a nil
// GetStatusBarEntries as "no extension entries".
func statusBarProviderForUI ( k * kit . Kit ) func ( ) [ ] ui . StatusBarEntryData {
2026-03-29 13:19:51 +03:00
if ! k . Extensions ( ) . HasExtensions ( ) {
2026-03-02 01:33:56 +03:00
return nil
}
return func ( ) [ ] ui . StatusBarEntryData {
2026-03-29 13:19:51 +03:00
entries := k . Extensions ( ) . GetStatusEntries ( )
2026-03-02 01:33:56 +03:00
if len ( entries ) == 0 {
return nil
}
result := make ( [ ] ui . StatusBarEntryData , len ( entries ) )
for i , e := range entries {
result [ i ] = ui . StatusBarEntryData {
Key : e . Key ,
Text : e . Text ,
Priority : e . Priority ,
}
}
return result
}
}
2026-03-02 16:35:00 +03:00
// beforeForkProviderForUI returns a callback that emits a BeforeFork event
// and returns (cancelled, reason). Returns nil if extensions are disabled —
// the UI treats nil as "no hook".
func beforeForkProviderForUI ( k * kit . Kit ) func ( string , bool , string ) ( bool , string ) {
2026-03-29 13:19:51 +03:00
if ! k . Extensions ( ) . HasExtensions ( ) {
2026-03-02 16:35:00 +03:00
return nil
}
2026-03-29 13:19:51 +03:00
return func ( targetID string , isUserMsg bool , userText string ) ( bool , string ) {
return k . Extensions ( ) . EmitBeforeFork ( targetID , isUserMsg , userText )
}
2026-03-02 16:35:00 +03:00
}
// beforeSessionSwitchProviderForUI returns a callback that emits a
// BeforeSessionSwitch event and returns (cancelled, reason). Returns nil
// if extensions are disabled — the UI treats nil as "no hook".
func beforeSessionSwitchProviderForUI ( k * kit . Kit ) func ( string ) ( bool , string ) {
2026-03-29 13:19:51 +03:00
if ! k . Extensions ( ) . HasExtensions ( ) {
2026-03-02 16:35:00 +03:00
return nil
}
2026-03-29 13:19:51 +03:00
return func ( switchReason string ) ( bool , string ) {
return k . Extensions ( ) . EmitBeforeSessionSwitch ( switchReason )
}
2026-03-02 16:35:00 +03:00
}
2026-03-02 19:04:37 +03:00
// globalShortcutsProviderForUI returns a callback that queries the extension
// runner for registered keyboard shortcuts. Returns nil if extensions are
// disabled — the UI treats nil as "no shortcuts".
func globalShortcutsProviderForUI ( k * kit . Kit ) func ( ) map [ string ] func ( ) {
2026-03-29 13:19:51 +03:00
if ! k . Extensions ( ) . HasExtensions ( ) {
2026-03-02 19:04:37 +03:00
return nil
}
2026-03-29 13:19:51 +03:00
return func ( ) map [ string ] func ( ) {
return k . Extensions ( ) . GetShortcuts ( )
}
2026-03-02 19:04:37 +03:00
}
2025-06-09 18:51:06 +03:00
func runNormalMode ( ctx context . Context ) error {
2025-06-09 17:02:08 +03:00
// Validate flag combinations
2026-03-05 19:03:47 +03:00
if quietFlag && positionalPrompt == "" {
2026-03-05 19:00:51 +03:00
return fmt . Errorf ( "--quiet requires a prompt (e.g. kit \"your question\" --quiet)" )
2025-06-09 17:02:08 +03:00
}
2026-03-05 19:03:47 +03:00
if jsonFlag && positionalPrompt == "" {
2026-03-05 19:00:51 +03:00
return fmt . Errorf ( "--json requires a prompt (e.g. kit \"your question\" --json)" )
2026-03-01 21:16:34 +03:00
}
if jsonFlag && noExitFlag {
return fmt . Errorf ( "--json and --no-exit flags cannot be used together" )
}
2026-03-05 19:03:47 +03:00
if noExitFlag && positionalPrompt == "" {
2026-03-05 19:00:51 +03:00
return fmt . Errorf ( "--no-exit requires a prompt (e.g. kit \"your question\" --no-exit)" )
2025-06-16 17:35:01 +03:00
}
2025-06-09 17:02:08 +03:00
2025-06-09 14:38:31 +03:00
// Set up logging
if debugMode {
log . SetFlags ( log . LstdFlags | log . Lshortfile )
2024-12-18 11:31:27 +03:00
}
2024-12-08 19:27:53 +03:00
2026-02-27 13:51:08 +03:00
// Update debug mode from viper
if viper . GetBool ( "debug" ) && ! debugMode {
debugMode = viper . GetBool ( "debug" )
log . SetFlags ( log . LstdFlags | log . Lshortfile )
}
2026-03-22 13:52:06 +03:00
// Restore persisted model preference when no explicit --model flag or
// config file model is set. Precedence: CLI flag > config file > saved
// preference > built-in default. This mirrors how themes are persisted.
2026-03-30 16:12:16 +03:00
// Skip custom/* models unless --provider-url is also provided, since the
// custom provider requires a URL that was only valid for the previous session.
2026-03-22 13:52:06 +03:00
if ! modelFlagChanged && ! viper . InConfig ( "model" ) {
if pref := ui . LoadModelPreference ( ) ; pref != "" {
2026-03-30 16:12:16 +03:00
if strings . HasPrefix ( pref , "custom/" ) && viper . GetString ( "provider-url" ) == "" {
// Don't restore custom models without a provider URL
} else {
viper . Set ( "model" , pref )
}
2026-03-22 13:52:06 +03:00
}
}
// Restore persisted thinking level preference (same precedence chain).
if ! thinkingFlagChanged && ! viper . InConfig ( "thinking-level" ) {
if pref := ui . LoadThinkingLevelPreference ( ) ; pref != "" {
viper . Set ( "thinking-level" , pref )
}
}
2026-03-24 13:28:23 +03:00
// When --provider-url is set but no explicit --model was provided,
// default to "custom/custom" so the user doesn't need to remember a
// provider/model pair for custom OpenAI-compatible endpoints.
2026-03-24 14:19:49 +03:00
// This intentionally overrides saved preferences but respects config-file
// models — if you specify a model in ~/.kit.yml, it will be used with
// custom/custom's provider routing.
if viper . GetString ( "provider-url" ) != "" && ! modelFlagChanged && ! viper . InConfig ( "model" ) {
2026-03-24 13:28:23 +03:00
viper . Set ( "model" , "custom/custom" )
}
2026-03-30 16:12:16 +03:00
// When --provider-url is set with an explicit --model that lacks a provider
// prefix (no "/"), auto-prefix with "custom/" for OpenAI-compatible endpoints.
if viper . GetString ( "provider-url" ) != "" && modelFlagChanged {
model := viper . GetString ( "model" )
if model != "" && ! strings . Contains ( model , "/" ) {
viper . Set ( "model" , "custom/" + model )
}
}
2026-02-27 13:51:08 +03:00
// Load MCP configuration.
2026-02-27 14:11:27 +03:00
mcpConfig , err := config . LoadAndValidateConfig ( )
if err != nil {
return fmt . Errorf ( "failed to load MCP config: %v" , err )
2025-06-09 23:44:01 +03:00
}
2026-02-27 13:51:08 +03:00
// Create spinner function for agent creation.
var spinnerFunc kit . SpinnerFunc
2025-06-27 17:41:18 +03:00
if ! quietFlag {
2026-02-26 12:24:17 +03:00
spinnerFunc = func ( fn func ( ) error ) error {
2026-03-31 13:01:30 +03:00
tempCli , tempErr := ui . NewCLI ( viper . GetBool ( "debug" ) )
2025-06-27 17:41:18 +03:00
if tempErr == nil {
2026-02-26 12:24:17 +03:00
return tempCli . ShowSpinner ( fn )
2025-06-27 17:41:18 +03:00
}
return fn ( )
2025-06-26 13:32:18 +03:00
}
}
2026-02-27 13:51:08 +03:00
// Build Kit options from CLI flags and create the SDK instance.
2026-02-27 18:17:32 +03:00
// kit.New() handles: config → skills → agent → session → extension bridge.
2026-04-04 17:41:57 +03:00
authHandler , authErr := kit . NewCLIMCPAuthHandler ( )
if authErr != nil {
// Non-fatal: OAuth just won't be available for remote MCP servers.
fmt . Fprintf ( os . Stderr , "Warning: Failed to create OAuth handler: %v\n" , authErr )
}
2026-04-07 16:29:09 +03:00
// appInstancePtr is used to break the circular dependency between
// kit.New (which needs the OnMCPServerLoaded callback) and app.New
// (which is needed by the callback to send events to the TUI).
var appInstancePtr * app . App
2026-02-27 13:51:08 +03:00
kitOpts := & kit . Options {
2026-04-04 17:41:57 +03:00
Quiet : quietFlag ,
Debug : debugMode ,
NoSession : noSessionFlag ,
Continue : continueFlag ,
SessionPath : sessionPath ,
AutoCompact : autoCompactFlag ,
MCPAuthHandler : authHandler ,
2026-04-07 16:29:09 +03:00
// This callback is called when each MCP server finishes loading.
// We use a closure that captures appInstancePtr which is set after
// app.New() is called below.
OnMCPServerLoaded : func ( serverName string , toolCount int , err error ) {
if appInstancePtr != nil {
appInstancePtr . NotifyMCPServerLoaded ( serverName , toolCount , err )
}
} ,
2026-02-28 01:01:12 +03:00
CLI : & kit . CLIOptions {
2026-04-14 17:17:01 +03:00
MCPConfig : mcpConfig ,
ShowSpinner : true ,
SpinnerFunc : spinnerFunc ,
UseBufferedLogger : true ,
ProgressReaderFunc : progress . NewProgressReadCloser ,
2026-02-28 01:01:12 +03:00
} ,
2026-02-27 13:51:08 +03:00
}
if resumeFlag {
2026-03-20 18:08:48 +03:00
// When --resume is combined with interactive mode, the TUI session
// picker will be shown at startup. For non-interactive mode, fall
// back to auto-selecting the most recent session.
if positionalPrompt != "" {
sessions , _ := kit . ListSessions ( "" )
if len ( sessions ) > 0 {
kitOpts . SessionPath = sessions [ 0 ] . Path
}
2026-02-27 13:51:08 +03:00
}
2026-03-20 18:08:48 +03:00
// Interactive mode: ShowSessionPicker is set below on AppModelOptions.
2026-02-27 13:51:08 +03:00
}
kitInstance , err := kit . New ( ctx , kitOpts )
2025-04-23 23:39:21 +09:00
if err != nil {
2026-02-26 16:40:46 +03:00
return err
2025-04-23 23:39:21 +09:00
}
2026-02-27 13:51:08 +03:00
defer func ( ) { _ = kitInstance . Close ( ) } ( )
2025-04-23 23:39:21 +09:00
2026-05-08 10:39:14 +03:00
// Build the "System Prompt loaded" notice shown at startup, paralleling the
// per-server "MCP server loaded" notifications so users can confirm that a
// configured prompt file was found and applied.
var systemPromptLoadedMsg string
if kitInstance . HasCustomSystemPrompt ( ) {
if src := kitInstance . GetSystemPromptSource ( ) ; src != "" {
systemPromptLoadedMsg = "System Prompt loaded: " + src
}
}
2026-02-28 01:01:12 +03:00
// Extract metadata for display and app options.
parsedProvider , modelName , serverNames , toolNames , mcpToolCount , extensionToolCount := CollectAgentMetadata ( kitInstance , mcpConfig )
2024-12-08 19:27:53 +03:00
2026-02-26 16:40:46 +03:00
// Create CLI for non-interactive mode only.
2026-02-26 01:31:57 +03:00
var cli * ui . CLI
2026-03-05 19:03:47 +03:00
if positionalPrompt != "" {
2026-02-28 01:01:12 +03:00
cli , err = SetupCLIForNonInteractive ( kitInstance )
2026-02-26 01:31:57 +03:00
if err != nil {
return fmt . Errorf ( "failed to setup CLI: %v" , err )
2025-08-08 09:32:04 +03:00
}
2026-02-26 01:31:57 +03:00
// Display buffered debug messages if any (non-interactive path only).
2026-02-28 01:01:12 +03:00
if msgs := kitInstance . GetBufferedDebugMessages ( ) ; len ( msgs ) > 0 && cli != nil {
cli . DisplayDebugMessage ( strings . Join ( msgs , "\n " ) )
2025-06-11 11:45:55 +03:00
}
2024-12-08 19:27:53 +03:00
2026-02-28 01:01:12 +03:00
DisplayDebugConfig ( cli , kitInstance , mcpConfig , parsedProvider )
2026-05-08 10:44:01 +03:00
if systemPromptLoadedMsg != "" && cli != nil {
2026-05-08 10:39:14 +03:00
cli . DisplayInfo ( systemPromptLoadedMsg )
}
2024-12-08 19:27:53 +03:00
}
2026-02-27 12:11:17 +03:00
// Load existing messages from resumed/continued sessions.
2026-02-27 13:51:08 +03:00
treeSession := kitInstance . GetTreeSession ( )
2026-03-31 14:26:49 +03:00
var messages [ ] kit . LLMMessage
2026-02-27 12:11:17 +03:00
if treeSession != nil {
2026-03-29 14:36:03 +03:00
messages = treeSession . GetLLMMessages ( )
2025-06-25 20:25:14 +03:00
}
2025-06-10 01:21:17 +03:00
2026-02-27 13:51:08 +03:00
// Create the app.App instance.
2026-02-27 14:39:48 +03:00
appOpts := BuildAppOptions ( mcpConfig , modelName , serverNames , toolNames )
2026-02-27 13:51:08 +03:00
appOpts . Kit = kitInstance
2026-02-26 18:47:10 +03:00
appOpts . TreeSession = treeSession
2026-02-26 01:45:24 +03:00
2026-02-26 09:55:20 +03:00
// Create a usage tracker that is shared between the app layer (for recording
2026-02-27 13:51:08 +03:00
// usage after each step) and the TUI (for /usage display).
2026-02-26 09:55:20 +03:00
var usageTracker * ui . UsageTracker
2026-02-26 01:45:24 +03:00
if cli != nil {
2026-02-26 09:55:20 +03:00
usageTracker = cli . GetUsageTracker ( )
} else {
2026-02-26 16:40:46 +03:00
usageTracker = ui . CreateUsageTracker ( viper . GetString ( "model" ) , viper . GetString ( "provider-api-key" ) )
2026-02-26 01:45:24 +03:00
}
2026-02-26 09:55:20 +03:00
if usageTracker != nil {
appOpts . UsageTracker = usageTracker
}
2026-02-26 01:19:20 +03:00
appInstance := app . New ( appOpts , messages )
2026-04-07 16:29:09 +03:00
appInstancePtr = appInstance // Wire up the MCP server loaded callback.
2026-02-26 01:19:20 +03:00
defer appInstance . Close ( )
2026-04-04 17:41:57 +03:00
// Wire OAuth handler to route messages through the TUI once it's running.
if authHandler != nil {
authHandler . NotifyFunc = func ( serverName , message string ) {
appInstance . PrintFromExtension ( "info" , message )
}
}
2026-03-27 20:54:43 +03:00
// Buffer for extension messages during startup (printed after startup banner).
var startupExtensionMessages [ ] string
2026-05-08 10:39:14 +03:00
if systemPromptLoadedMsg != "" {
startupExtensionMessages = append ( startupExtensionMessages , systemPromptLoadedMsg )
}
2026-03-27 20:54:43 +03:00
2026-02-27 13:51:08 +03:00
// Set up extension context and emit SessionStart.
2026-03-29 13:19:51 +03:00
if kitInstance . Extensions ( ) . HasExtensions ( ) {
2026-02-27 12:11:17 +03:00
cwd , _ := os . Getwd ( )
2026-05-07 12:28:18 +03:00
extCtx := buildInteractiveExtensionContext ( extensionContextDeps {
ctx : ctx ,
cwd : cwd ,
modelName : modelName ,
interactive : positionalPrompt == "" ,
kitInstance : kitInstance ,
appInstance : appInstance ,
usageTracker : usageTracker ,
2026-02-27 00:08:48 +03:00
} )
2026-05-07 12:28:18 +03:00
extCtx . Print = func ( text string ) {
// Capture messages during startup, print after startup banner.
startupExtensionMessages = append ( startupExtensionMessages , text )
}
extCtx . PrintInfo = func ( text string ) {
startupExtensionMessages = append ( startupExtensionMessages , text )
}
extCtx . PrintError = func ( text string ) {
startupExtensionMessages = append ( startupExtensionMessages , text )
}
kitInstance . Extensions ( ) . SetContext ( extCtx )
2026-03-29 13:19:51 +03:00
kitInstance . Extensions ( ) . EmitSessionStart ( )
2026-03-27 20:54:43 +03:00
// Restore normal print functions for runtime use.
2026-05-07 12:28:18 +03:00
extCtx = buildInteractiveExtensionContext ( extensionContextDeps {
ctx : ctx ,
cwd : cwd ,
modelName : modelName ,
interactive : positionalPrompt == "" ,
kitInstance : kitInstance ,
appInstance : appInstance ,
usageTracker : usageTracker ,
2026-03-27 20:54:43 +03:00
} )
2026-05-07 12:28:18 +03:00
extCtx . Print = func ( text string ) { appInstance . PrintFromExtension ( "" , text ) }
extCtx . PrintInfo = func ( text string ) { appInstance . PrintFromExtension ( "info" , text ) }
extCtx . PrintError = func ( text string ) { appInstance . PrintFromExtension ( "error" , text ) }
kitInstance . Extensions ( ) . SetContext ( extCtx )
2026-02-27 00:08:48 +03:00
}
2026-02-27 00:26:06 +03:00
// Convert extension commands to UI-layer type for the interactive TUI.
2026-02-28 01:01:12 +03:00
extCommands := extensionCommandsForUI ( kitInstance )
2026-02-27 00:26:06 +03:00
2026-03-22 19:09:15 +03:00
// Load prompt templates from standard locations and explicit paths.
var promptTemplates [ ] * prompts . PromptTemplate
if ! noPromptTemplates {
homeDir , _ := os . UserHomeDir ( )
cwd , _ := os . Getwd ( )
tpls , diags , err := prompts . LoadAll ( prompts . LoadOptions {
Cwd : cwd ,
HomeDir : homeDir ,
ExtraPaths : promptTemplatePaths ,
ConfigPaths : viper . GetStringSlice ( "prompts" ) ,
IncludeDefaults : true ,
} )
if err != nil {
log . Printf ( "Warning: failed to load some prompt templates: %v" , err )
}
promptTemplates = tpls
for _ , d := range diags {
log . Printf ( "Prompt template collision: /%s kept from %s, dropped from %s" , d . Name , d . KeptPath , d . DroppedPath )
}
}
2026-02-27 16:02:11 +03:00
// Build context/skills display metadata for the startup banner.
var contextPaths [ ] string
for _ , cf := range kitInstance . GetContextFiles ( ) {
contextPaths = append ( contextPaths , cf . Path )
}
cwd , _ := os . Getwd ( )
var skillItems [ ] ui . SkillItem
for _ , s := range kitInstance . GetSkills ( ) {
source := "user"
if strings . HasPrefix ( s . Path , cwd ) {
source = "project"
}
skillItems = append ( skillItems , ui . SkillItem {
2026-05-13 15:35:07 +03:00
Name : s . Name ,
Path : s . Path ,
Source : source ,
Description : s . Description ,
2026-02-27 16:02:11 +03:00
} )
}
2026-04-07 14:09:59 +03:00
// Build prompt template and skill item provider callbacks for hot-reload.
// These are called by the TUI when ContentReloadEvent fires.
getPromptTemplates := func ( ) [ ] * prompts . PromptTemplate {
if noPromptTemplates {
return nil
}
homeDir , _ := os . UserHomeDir ( )
cwd , _ := os . Getwd ( )
tpls , _ , err := prompts . LoadAll ( prompts . LoadOptions {
Cwd : cwd ,
HomeDir : homeDir ,
ExtraPaths : promptTemplatePaths ,
ConfigPaths : viper . GetStringSlice ( "prompts" ) ,
IncludeDefaults : true ,
} )
if err != nil {
log . Printf ( "Warning: failed to reload prompt templates: %v" , err )
}
return tpls
}
getSkillItems := func ( ) [ ] ui . SkillItem {
// Re-discover skills from disk.
if err := kitInstance . ReloadSkills ( ) ; err != nil {
log . Printf ( "Warning: failed to reload skills: %v" , err )
return nil
}
cwd , _ := os . Getwd ( )
var items [ ] ui . SkillItem
for _ , s := range kitInstance . GetSkills ( ) {
source := "user"
if strings . HasPrefix ( s . Path , cwd ) {
source = "project"
}
items = append ( items , ui . SkillItem {
2026-05-13 15:35:07 +03:00
Name : s . Name ,
Path : s . Path ,
Source : source ,
Description : s . Description ,
2026-04-07 14:09:59 +03:00
} )
}
return items
}
2026-02-28 14:11:52 +03:00
// Build extension UI providers once (shared between both modes).
getWidgets := widgetProviderForUI ( kitInstance )
getHeader := headerProviderForUI ( kitInstance )
getFooter := footerProviderForUI ( kitInstance )
2026-02-28 16:23:45 +03:00
getToolRenderer := toolRendererProviderForUI ( kitInstance )
2026-02-28 17:46:41 +03:00
getEditorInterceptor := editorInterceptorProviderForUI ( kitInstance )
2026-03-01 15:24:48 +03:00
getUIVisibility := uiVisibilityProviderForUI ( kitInstance )
2026-03-02 01:33:56 +03:00
getStatusBarEntries := statusBarProviderForUI ( kitInstance )
2026-03-02 16:35:00 +03:00
emitBeforeFork := beforeForkProviderForUI ( kitInstance )
emitBeforeSessionSwitch := beforeSessionSwitchProviderForUI ( kitInstance )
2026-03-02 19:04:37 +03:00
getGlobalShortcuts := globalShortcutsProviderForUI ( kitInstance )
2026-04-01 13:54:10 +03:00
getExtensionCommands := func ( ) [ ] commands . ExtensionCommand {
2026-03-02 19:32:19 +03:00
return extensionCommandsForUI ( kitInstance )
}
2026-02-28 14:11:52 +03:00
2026-04-07 16:29:09 +03:00
// Build dynamic tool name and MCP tool count providers. These are called
// by the TUI when MCPToolsReadyEvent fires to refresh the /tools list
// and startup info bar after background MCP tool loading completes.
getToolNames := func ( ) [ ] string {
return kitInstance . GetToolNames ( )
}
getMCPToolCount := func ( ) int {
return kitInstance . GetMCPToolCount ( )
}
2026-04-15 11:50:33 +03:00
// Build MCP prompt provider callbacks for the TUI.
// Convert kit.MCPPrompt → ui.MCPPromptInfo for the UI layer.
convertMCPPromptsForUI := func ( ) [ ] ui . MCPPromptInfo {
prompts := kitInstance . ListMCPPrompts ( )
if len ( prompts ) == 0 {
return nil
}
result := make ( [ ] ui . MCPPromptInfo , len ( prompts ) )
for i , p := range prompts {
args := make ( [ ] ui . MCPPromptArgInfo , len ( p . Arguments ) )
for j , a := range p . Arguments {
args [ j ] = ui . MCPPromptArgInfo {
Name : a . Name ,
Description : a . Description ,
Required : a . Required ,
}
}
result [ i ] = ui . MCPPromptInfo {
Name : p . Name ,
Description : p . Description ,
Arguments : args ,
ServerName : p . ServerName ,
}
}
return result
}
mcpPrompts := convertMCPPromptsForUI ( )
getMCPPrompts := func ( ) [ ] ui . MCPPromptInfo {
return convertMCPPromptsForUI ( )
}
expandMCPPrompt := func ( serverName , promptName string , args map [ string ] string ) ( * ui . MCPPromptExpandResult , error ) {
result , err := kitInstance . GetMCPPrompt ( context . Background ( ) , serverName , promptName , args )
if err != nil {
return nil , err
}
msgs := make ( [ ] ui . MCPPromptMessageInfo , len ( result . Messages ) )
for i , m := range result . Messages {
msgs [ i ] = ui . MCPPromptMessageInfo {
2026-04-15 15:23:01 +03:00
Role : m . Role ,
Content : m . Content ,
FileParts : m . FileParts ,
2026-04-15 11:50:33 +03:00
}
}
return & ui . MCPPromptExpandResult { Messages : msgs } , nil
}
2026-04-15 13:01:36 +03:00
// MCP resource callbacks for @ autocomplete and submit-time resolution.
getMCPResources := func ( ) [ ] ui . FileSuggestion {
resources := kitInstance . ListMCPResources ( )
suggestions := make ( [ ] ui . FileSuggestion , len ( resources ) )
for i , r := range resources {
suggestions [ i ] = ui . FileSuggestion {
RelPath : r . Name ,
IsMCPResource : true ,
MCPServerName : r . ServerName ,
MCPResourceURI : r . URI ,
MCPMIMEType : r . MIMEType ,
Score : 100 , // default score, filtered later
}
}
return suggestions
}
mcpResourceReaderFn := func ( serverName , uri string ) ( string , [ ] byte , string , bool , error ) {
content , err := kitInstance . ReadMCPResource ( context . Background ( ) , serverName , uri )
if err != nil {
return "" , nil , "" , false , err
}
return content . Text , content . BlobData , content . MIMEType , content . IsBlob , nil
}
// Store MCP resource callbacks at package level for consumption by
// runInteractiveModeBubbleTea and runNonInteractiveModeApp.
mcpGetResources = getMCPResources
mcpResourceReader = mcpResourceReaderFn
2026-04-07 16:29:09 +03:00
// Start a goroutine that waits for background MCP tool loading to
// complete and notifies the TUI so it can refresh tool names and counts.
if len ( mcpConfig . MCPServers ) > 0 {
go func ( ) {
_ = kitInstance . WaitForMCPTools ( )
appInstance . NotifyMCPToolsReady ( )
} ( )
}
2026-03-06 18:50:32 +03:00
// Build model switching callbacks for the /model command.
setModelForUI := func ( modelString string ) error {
err := kitInstance . SetModel ( context . Background ( ) , modelString )
if err != nil {
return err
}
// Update the extension context's Model field so handlers see it.
2026-03-29 13:19:51 +03:00
kitInstance . Extensions ( ) . UpdateContextModel ( modelString )
2026-03-06 18:50:32 +03:00
// NOTE: We do NOT call appInstance.NotifyModelChanged() here because
// this callback runs synchronously inside BubbleTea's Update(), and
// NotifyModelChanged calls prog.Send() which deadlocks. The UI layer
// updates m.providerName and m.modelName directly after setModel returns.
2026-03-25 18:09:36 +03:00
// Update usage tracker with new model info for correct token counting.
if usageTracker != nil {
newProvider , newModel , _ := models . ParseModelString ( modelString )
if newProvider != "unknown" && newModel != "unknown" && newProvider != "ollama" {
registry := models . GetGlobalRegistry ( )
if modelInfo := registry . LookupModel ( newProvider , newModel ) ; modelInfo != nil {
// Check OAuth status for Anthropic models
isOAuth := false
if newProvider == "anthropic" {
_ , source , err := auth . GetAnthropicAPIKey ( viper . GetString ( "provider-api-key" ) )
if err == nil && strings . HasPrefix ( source , "stored OAuth" ) {
isOAuth = true
}
}
usageTracker . UpdateModelInfo ( modelInfo , newProvider , isOAuth )
}
}
}
2026-03-06 18:50:32 +03:00
return nil
}
emitModelChangeForUI := func ( newModel , previousModel , source string ) {
2026-03-29 13:19:51 +03:00
kitInstance . Extensions ( ) . EmitModelChange ( newModel , previousModel , source )
2026-03-06 18:50:32 +03:00
}
2026-03-07 21:27:46 +03:00
// Build thinking level callback.
setThinkingLevelForUI := func ( level string ) error {
return kitInstance . SetThinkingLevel ( context . Background ( ) , level )
}
2026-03-20 18:08:48 +03:00
// Build session-switching callback. Opens a JSONL session file and
// replaces the active tree session on both the Kit SDK and App layer.
switchSessionForUI := func ( path string ) error {
ts , err := kit . OpenTreeSession ( path )
if err != nil {
return fmt . Errorf ( "failed to open session: %w" , err )
}
kitInstance . SetTreeSession ( ts )
appInstance . SwitchTreeSession ( ts )
return nil
}
2026-04-02 15:41:54 +03:00
// Build extension reload callback for the /reload-ext command.
reloadExtensionsForUI := func ( ) error {
err := kitInstance . Extensions ( ) . Reload ( )
if err != nil {
return err
}
go appInstance . NotifyWidgetUpdate ( )
return nil
}
// Start file watcher for automatic extension hot-reload.
extraPaths := viper . GetStringSlice ( "extension" )
watchDirs := extensions . WatchedDirs ( extraPaths )
if len ( watchDirs ) > 0 {
extWatcher , watchErr := extensions . NewWatcher ( watchDirs , func ( ) {
if err := reloadExtensionsForUI ( ) ; err != nil {
log . Printf ( "auto-reload extensions failed: %v" , err )
}
} )
if watchErr != nil {
log . Printf ( "extension file watcher not started: %v" , watchErr )
} else {
go extWatcher . Start ( ctx )
defer func ( ) { _ = extWatcher . Close ( ) } ( )
}
}
2026-04-07 14:09:59 +03:00
// Start file watchers for automatic prompt and skill hot-reload.
{
homeDir , _ := os . UserHomeDir ( )
cwd , _ := os . Getwd ( )
// Collect prompt template directories.
promptDirs := watcher . CollectDirs (
[ ] string {
filepath . Join ( homeDir , ".kit" , "prompts" ) ,
filepath . Join ( cwd , ".kit" , "prompts" ) ,
} ,
append ( promptTemplatePaths , viper . GetStringSlice ( "prompts" ) ... ) ,
)
// Collect skill directories.
skillDirs := watcher . CollectDirs (
[ ] string {
filepath . Join ( homeDir , ".config" , "kit" , "skills" ) ,
filepath . Join ( cwd , ".agents" , "skills" ) ,
filepath . Join ( cwd , ".kit" , "skills" ) ,
} ,
nil ,
)
// Combine all content directories and start a single watcher.
allContentDirs := append ( promptDirs , skillDirs ... )
if len ( allContentDirs ) > 0 {
contentWatcher , watchErr := watcher . New ( watcher . Options {
Dirs : allContentDirs ,
Extensions : [ ] string { ".md" , ".txt" } ,
Label : "prompts/skills" ,
OnReload : func ( ) {
log . Printf ( "auto-reloading prompts and skills" )
appInstance . NotifyContentReload ( )
} ,
} )
if watchErr != nil {
log . Printf ( "content file watcher not started: %v" , watchErr )
} else {
go contentWatcher . Start ( ctx )
defer func ( ) { _ = contentWatcher . Close ( ) } ( )
}
}
}
2025-06-09 17:02:08 +03:00
// Check if running in non-interactive mode
2026-03-05 19:03:47 +03:00
if positionalPrompt != "" {
2026-04-15 11:50:33 +03:00
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 )
2025-06-09 17:02:08 +03:00
}
2025-06-10 01:21:17 +03:00
2025-06-09 17:02:08 +03:00
// Quiet mode is not allowed in interactive mode
if quietFlag {
2026-03-05 19:00:51 +03:00
return fmt . Errorf ( "--quiet requires a prompt" )
2025-06-09 17:02:08 +03:00
}
2025-06-10 01:21:17 +03:00
2026-04-15 11:50:33 +03:00
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 )
2025-06-09 17:02:08 +03:00
}
2026-02-26 01:26:47 +03:00
// runNonInteractiveModeApp executes a single prompt via the app layer and exits,
// or transitions to the interactive BubbleTea TUI when --no-exit is set.
//
2026-02-26 16:40:46 +03:00
// In quiet mode, RunOnce is used (no intermediate output, final response only).
// Otherwise, RunOnceWithDisplay streams tool calls and responses through the
2026-02-27 14:11:27 +03:00
// shared CLIEventHandler — giving --prompt mode the same rich output as
// interactive mode.
2026-02-26 01:26:47 +03:00
//
2026-02-26 16:40:46 +03:00
// When --no-exit is set, after the prompt completes the interactive BubbleTea
// TUI is started so the user can continue the conversation.
2026-04-15 11:50:33 +03:00
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 {
2026-03-05 18:54:17 +03:00
// Expand @file references in the prompt before sending to the agent.
2026-04-15 13:01:36 +03:00
// Text files are XML-inlined; binary files are extracted as multimodal parts.
var fileParts [ ] kit . LLMFilePart
2026-03-05 18:54:17 +03:00
if cwd , err := os . Getwd ( ) ; err == nil {
2026-04-15 13:01:36 +03:00
result := ui . ProcessFileAttachments ( prompt , cwd , mcpResourceReader )
prompt = result . ProcessedText
for _ , fp := range result . FileParts {
fileParts = append ( fileParts , kit . LLMFilePart {
Filename : fp . Filename ,
Data : fp . Data ,
MediaType : fp . MediaType ,
} )
}
}
// Also include binary files from processPositionalArgs (CLI @file args).
for _ , fp := range positionalFiles {
fileParts = append ( fileParts , kit . LLMFilePart {
Filename : fp . Filename ,
Data : fp . Data ,
MediaType : fp . MediaType ,
} )
2026-03-05 18:54:17 +03:00
}
2026-03-01 21:16:34 +03:00
if jsonOutput {
// JSON mode: no intermediate display, structured JSON output.
2026-04-15 13:01:36 +03:00
result , err := appInstance . RunOnceResultWithFiles ( ctx , prompt , fileParts )
2026-03-01 21:16:34 +03:00
if err != nil {
writeJSONError ( err )
return err
}
data , err := buildJSONOutput ( result , modelName )
if err != nil {
return fmt . Errorf ( "failed to marshal JSON output: %w" , err )
}
fmt . Println ( string ( data ) )
} else if quiet {
2026-02-26 16:40:46 +03:00
// Quiet mode: no intermediate display, just print final response.
2026-04-15 13:01:36 +03:00
if err := appInstance . RunOnceWithFiles ( ctx , prompt , fileParts ) ; err != nil {
2026-02-26 16:40:46 +03:00
return err
}
} else if cli != nil {
// Display user message before running the agent.
cli . DisplayUserMessage ( prompt )
// Route events through the shared CLI event handler.
eventHandler := ui . NewCLIEventHandler ( cli , modelName )
2026-04-15 13:01:36 +03:00
err := appInstance . RunOnceWithDisplayAndFiles ( ctx , prompt , eventHandler . Handle , fileParts )
2026-02-26 16:40:46 +03:00
eventHandler . Cleanup ( )
if err != nil {
return err
}
} else {
// No CLI available (shouldn't happen in non-quiet mode, but be safe).
2026-04-15 13:01:36 +03:00
if err := appInstance . RunOnceWithFiles ( ctx , prompt , fileParts ) ; err != nil {
2026-02-26 16:40:46 +03:00
return err
2026-02-26 16:10:43 +03:00
}
}
2026-02-26 01:26:47 +03:00
// If --no-exit was requested, hand off to the interactive TUI.
if noExit {
2026-04-15 11:50:33 +03:00
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 )
2026-02-26 01:26:47 +03:00
}
return nil
}
2026-03-01 21:16:34 +03:00
// ---------------------------------------------------------------------------
// JSON output helpers (--json mode)
// ---------------------------------------------------------------------------
// buildJSONOutput converts a TurnResult into a structured JSON byte slice
// suitable for machine consumption.
func buildJSONOutput ( result * kit . TurnResult , model string ) ( [ ] byte , error ) {
type jsonPart struct {
Type string ` json:"type" `
Data any ` json:"data" `
}
type jsonMessage struct {
Role string ` json:"role" `
Parts [ ] jsonPart ` json:"parts" `
}
type jsonUsage struct {
InputTokens int64 ` json:"input_tokens" `
OutputTokens int64 ` json:"output_tokens" `
TotalTokens int64 ` json:"total_tokens" `
CacheReadTokens int64 ` json:"cache_read_tokens" `
CacheCreationTokens int64 ` json:"cache_creation_tokens" `
}
type jsonEnvelope struct {
2026-03-16 11:10:05 +03:00
Response string ` json:"response" `
Model string ` json:"model" `
StopReason string ` json:"stop_reason,omitempty" `
SessionID string ` json:"session_id,omitempty" `
Usage * jsonUsage ` json:"usage,omitempty" `
Messages [ ] jsonMessage ` json:"messages" `
2026-03-01 21:16:34 +03:00
}
out := jsonEnvelope {
2026-03-16 11:10:05 +03:00
Response : result . Response ,
Model : model ,
StopReason : result . StopReason ,
SessionID : result . SessionID ,
2026-03-01 21:16:34 +03:00
}
if result . TotalUsage != nil {
out . Usage = & jsonUsage {
InputTokens : result . TotalUsage . InputTokens ,
OutputTokens : result . TotalUsage . OutputTokens ,
TotalTokens : result . TotalUsage . TotalTokens ,
CacheReadTokens : result . TotalUsage . CacheReadTokens ,
CacheCreationTokens : result . TotalUsage . CacheCreationTokens ,
}
}
for _ , fmsg := range result . Messages {
2026-03-29 14:36:03 +03:00
converted := kit . ConvertFromLLMMessage ( fmsg )
2026-03-01 21:16:34 +03:00
m := jsonMessage { Role : string ( converted . Role ) }
for _ , p := range converted . Parts {
switch c := p . ( type ) {
case kit . TextContent :
m . Parts = append ( m . Parts , jsonPart { Type : "text" , Data : c } )
case kit . ToolCall :
m . Parts = append ( m . Parts , jsonPart { Type : "tool_call" , Data : c } )
case kit . ToolResult :
m . Parts = append ( m . Parts , jsonPart { Type : "tool_result" , Data : c } )
case kit . ReasoningContent :
m . Parts = append ( m . Parts , jsonPart { Type : "reasoning" , Data : c } )
case kit . Finish :
m . Parts = append ( m . Parts , jsonPart { Type : "finish" , Data : c } )
}
}
out . Messages = append ( out . Messages , m )
}
return json . MarshalIndent ( out , "" , " " )
}
// writeJSONError writes a JSON-formatted error object to stdout so that
// callers using --json always receive parseable output.
func writeJSONError ( err error ) {
type jsonError struct {
Error string ` json:"error" `
}
data , _ := json . MarshalIndent ( jsonError { Error : err . Error ( ) } , "" , " " )
fmt . Fprintln ( os . Stderr , string ( data ) )
}
2026-02-26 01:24:24 +03:00
// runInteractiveModeBubbleTea starts the new unified Bubble Tea interactive TUI.
//
// It:
// 1. Gets the terminal dimensions (falls back to 80x24 if unavailable).
// 2. Creates a ui.AppModel (parent model) with the appInstance as the controller,
// wiring up all child components (InputComponent, StreamComponent).
// 3. Creates a single tea.NewProgram and registers it with appInstance via SetProgram
// so that agent events are routed to the TUI.
// 4. Calls program.Run() which blocks until the user quits (Ctrl+C or /quit).
//
2026-02-26 01:31:57 +03:00
// SetupCLI is not used for interactive mode; the TUI (AppModel) handles its own rendering.
2026-04-15 11:50:33 +03:00
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 {
2026-04-07 21:20:04 +03:00
// 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
// background goroutines (watchers, extension handlers, SDK internals)
// will visually corrupt the terminal.
logDir := filepath . Join ( os . TempDir ( ) , "kit" )
_ = os . MkdirAll ( logDir , 0 o700 )
logFile , logErr := tea . LogToFile ( filepath . Join ( logDir , "kit.log" ) , "kit" )
if logErr == nil {
defer func ( ) { _ = logFile . Close ( ) } ( )
}
2026-02-26 01:24:24 +03:00
// Determine terminal size; fall back gracefully.
termWidth , termHeight , err := term . GetSize ( int ( os . Stdout . Fd ( ) ) )
if err != nil || termWidth == 0 {
termWidth = 80
termHeight = 24
}
2026-03-05 18:46:25 +03:00
cwd , _ := os . Getwd ( )
2026-03-20 18:08:48 +03:00
2026-02-26 01:24:24 +03:00
appModel := ui . NewAppModel ( appInstance , ui . AppModelOptions {
2026-03-31 19:03:21 +03:00
ModelName : modelName ,
ProviderName : providerName ,
LoadingMessage : loadingMessage ,
Cwd : cwd ,
Width : termWidth ,
Height : termHeight ,
ServerNames : serverNames ,
ToolNames : toolNames ,
2026-04-07 16:29:09 +03:00
GetToolNames : getToolNames ,
GetMCPToolCount : getMCPToolCount ,
2026-03-31 19:03:21 +03:00
MCPToolCount : mcpToolCount ,
ExtensionToolCount : extensionToolCount ,
UsageTracker : usageTracker ,
ExtensionCommands : extCommands ,
PromptTemplates : promptTemplates ,
2026-04-07 14:09:59 +03:00
GetPromptTemplates : getPromptTemplates ,
2026-04-15 11:50:33 +03:00
MCPPrompts : mcpPrompts ,
GetMCPPrompts : getMCPPrompts ,
ExpandMCPPrompt : expandMCPPrompt ,
2026-03-31 19:03:21 +03:00
ContextPaths : contextPaths ,
SkillItems : skillItems ,
2026-04-07 14:09:59 +03:00
GetSkillItems : getSkillItems ,
2026-03-31 19:03:21 +03:00
StartupExtensionMessages : startupExtensionMessages ,
GetWidgets : getWidgets ,
GetHeader : getHeader ,
GetFooter : getFooter ,
GetToolRenderer : getToolRenderer ,
GetEditorInterceptor : getEditorInterceptor ,
GetUIVisibility : getUIVisibility ,
GetStatusBarEntries : getStatusBarEntries ,
EmitBeforeFork : emitBeforeFork ,
EmitBeforeSessionSwitch : emitBeforeSessionSwitch ,
GetGlobalShortcuts : getGlobalShortcuts ,
GetExtensionCommands : getExtensionCommands ,
SetModel : setModel ,
EmitModelChange : emitModelChange ,
ThinkingLevel : thinkingLevel ,
IsReasoningModel : isReasoningModel ,
SetThinkingLevel : setThinkingLevel ,
SwitchSession : switchSession ,
2026-04-02 15:41:54 +03:00
ReloadExtensions : reloadExtensions ,
2026-03-31 19:03:21 +03:00
ShowSessionPicker : resumeFlag ,
2026-04-15 13:01:36 +03:00
GetMCPResources : mcpGetResources ,
MCPResourceReader : mcpResourceReader ,
2026-02-26 01:24:24 +03:00
} )
program := tea . NewProgram ( appModel )
// Register the program with the app layer so agent events are sent to the TUI.
appInstance . SetProgram ( program )
_ , runErr := program . Run ( )
return runErr
}