- Add ExtensionInfo type and Loaded() method to the public ExtensionAPI
so SDK consumers can inspect which extensions are active.
- Introduce ui.ExtensionItem and thread ExtensionItems/GetExtensionItems
through AppModelOptions, mirroring the existing SkillItem pattern.
- Render an [Extensions] row in AddStartupMessageToScrollList showing
the filename of each loaded extension (with a (N tools) suffix when
extensions register tools). Falls back to tool count only when items
are unavailable, and is omitted entirely when no extensions load.
- Refresh the list on /reload-ext via a new refreshExtensionItems hook
so the banner stays accurate across hot-reloads.
- Add buildExtensionItems helper in cmd/root.go that strips .go and
resolves subdirectory extensions to their parent dir name, tagging
each as project or user scope based on cwd.
- register loaded skills into the input autocomplete under category
"Skills" with HasArgs so Enter populates "/skill:name " instead of
auto-submitting, leaving room for trailing args
- prefix descriptions with [project] or [user] to disambiguate
colliding skill names across sources
- extend refreshSkillItems to prune & re-add Skills entries on
ContentReloadEvent, matching the pattern used for prompt templates
and MCP prompts
- add Description field to ui.SkillItem and populate it from
kit.Skill.Description in both initial build and hot-reload paths
SetupCLIForNonInteractive returns nil when --quiet is active, matching
the pre-existing nil checks elsewhere in the same block (e.g. the
buffered debug-message branch). Without this guard the new
'System Prompt loaded' notice panicked on quiet, non-interactive runs.
Discovered via tmux smoke test of the #25 fix.
When system-prompt was a file path (via --system-prompt, config entry,
or SDK Options.SystemPrompt), the path string itself was used as the
base prompt because config.LoadSystemPrompt only ran later in
BuildProviderConfig — by which point viper had been overwritten with
the path-augmented composed text. The LLM received the path instead of
the prompt contents.
- Call config.LoadSystemPrompt on the raw viper value in New() before
PromptBuilder composes runtime context (AGENTS.md / skills / date).
- Add HasCustomSystemPrompt() and GetSystemPromptSource() so SDK callers
can inspect prompt state without reaching into viper.
- Display 'System Prompt loaded: <source>' at startup in CLI and TUI
modes, paralleling the per-server 'MCP server loaded' notice.
- Add regression tests covering both file-path and inline prompt paths.
Fixes#25
Per AGENTS.md 'Yaegi function field bug', named function/method
references assigned to extensions.Context fields return zero values
across the interpreter boundary. The two SetContext literals in
runNormalMode (now consolidated in buildInteractiveExtensionContext)
inherited 9 bare references that need to be anonymous closure literals:
PrintBlock, GetChildren, GetAvailableSkills, ParseTemplate,
RenderTemplate, ParseArguments, SimpleParseArguments,
ResolveModelChain, CheckModelAvailable
Each is now wrapped as 'func(args) ret { return <orig>(args) }'.
Behaviour unchanged in regular Go; Yaegi extensions that consume these
fields will now see callable closures instead of zero values.
Verified with go test -race ./...
The previous runNormalMode contained two nearly-identical 400-line
extensions.Context literal expressions:
* the startup-time literal (cmd/root.go:853-1307) that buffered
Print* calls into startupExtensionMessages
* the runtime literal (cmd/root.go:1311-1605) that routed Print*
through appInstance.PrintFromExtension
Every other field — Compact, SendMultimodalMessage, the four prompt
factories, all 25+ data-access fields, all four bridge phases — was
duplicated byte-for-byte. Maintainers had to remember to update both
copies whenever an extension Context field was added.
cmd/root.go is now 1463 lines (was 2225). The new helper lives in
cmd/extension_context.go (455 lines, mostly the closures verbatim) and
returns an extensions.Context with every field populated except
Print/PrintInfo/PrintError, which each call site sets afterwards to
match its phase. This preserves AGENTS.md's 'function field bug'
guarantee — all assignments remain anonymous closure literals.
Output of 'kit --version' / 'kit --help' unchanged. Full test suite
passes.
The same ~40-line block — building a kit.SubagentConfig, wrapping
OnEvent through sdkEventToSubagentEvent, calling kitInstance.Subagent,
and translating the SDK result into extensions.SubagentResult — was
copy-pasted three times:
* cmd/root.go (interactive TUI Context, line 1148)
* cmd/root.go (post-SessionStart runtime Context, line 1446)
* internal/acpserver/session.go (ACP server Context, line 154)
A separate sdkEventToSubagentEvent function was duplicated byte-for-byte
between cmd/root.go and internal/acpserver/session.go.
Both are now consolidated in a new internal/extbridge package which is
the only module-internal home that can legitimately import both
pkg/kit/ (the public SDK) and internal/extensions/. cmd/ and
internal/acpserver/ both import it, so SDK-event-to-extension-event
schema changes only have one site to update.
Also fixes pkg/kit/events.go godoc comment that named the underlying
LLM library, per AGENTS.md 'No Dependency Name Leakage' rule for
exported SDK symbols.
go test -race ./... passes.
Removes ~600 lines of unreferenced code surfaced by deadcode + manual
audit (none of it reachable from production code paths or test setup):
- internal/models/pool.go: ProviderPool was never wired into kitsetup
or the agent; the global pool singleton had zero callers.
- internal/ui/debug_logger.go: CLIDebugLogger was unreachable; debug
routing goes through internal/tools/buffered_logger.go instead.
- internal/ui/tool_approval_input.go: tea.Model never instantiated;
approvals are handled inline in model.go.
- internal/ui/cli.go: DisplayAssistantMessage / DisplayCancellation /
GetDebugLogger had zero callers (the *WithModel variant is what
event_handler.go uses).
- internal/ui/style/enhanced.go: Style{Card,Header,Subheader,Muted,
Success,Error,Warning,Info} + Create{Separator,ProgressBar} — none
used. CreateBadge stays (used by model.go).
- internal/ui/style/themes.go: RefreshThemeRegistry — never called.
- internal/ui/block_renderer.go: With{FullWidth,MarginTop,Padding{Left,
Right},Background,Foreground,Width} — option helpers nobody calls.
- internal/ui/render/blocks.go: UserBlock, ToolBlock — replaced by
inline rendering elsewhere; the test for UserBlock was rewritten to
directly exercise HighlightFileTokens (which is what the test really
cared about).
- internal/ui/commands/commands.go: GetAllCommandNames — no callers.
- internal/ui/message_items.go: NewTextMessageItem,
NewSystemMessageItem + the entire SystemMessageItem type — model.go
uses NewStyledMessageItem instead.
- internal/prompts/loader.go: Deduplicate — the loader does dedup
internally; standalone helper was unused.
- internal/models/cache_options.go: mergeProviderOptions + its
test-only consumer.
- internal/extensions/installer.go: Installer.GetInstalledPackages —
intended for a 'kit ext list' command that was never built.
- internal/extensions/manifest.go: saveManifestToScope,
saveManifestToPath, GetGlobalManifest, GetProjectManifest,
addEntryToManifest, removeEntryFromManifest — package-level
duplicates of *Installer methods. Tests rewritten to exercise the
live Installer methods instead, which fixes a latent path-resolution
inconsistency between manifestPathForScope and Installer.manifestPath
(the former hard-coded paths, the latter respects projectGitRoot).
- internal/extensions/subagent.go: SpawnSubagent + helpers
(generateSubagentID, findKitBinary, subagentJSONOutput). The
subprocess-spawn implementation is unreachable; production code
routes through kit.Kit.Subagent (in-process). Types
(SubagentConfig/Result/Handle/etc.) and the SubagentHandle methods
remain because they are exposed to extensions via Yaegi symbols and
the Context.SpawnSubagent field.
- cmd/root.go: LoadConfigWithEnvSubstitution — one-line wrapper around
kit.LoadConfigWithEnvSubstitution with zero callers.
go test -race ./... passes.
Add charset=utf-8 to Content-Type header and use HTML entity
✓ instead of raw Unicode checkmark to prevent garbled
text display in browsers.
Fixes#9
Adds 'none' thinking level to support OpenAI gpt-5.4 models which use
'reasoning_effort: none' instead of 'minimal'. Includes validation and
auto-adjustment when switching models with incompatible levels.
- Add ThinkingNone constant mapping to ReasoningEffortNone
- Add IsValidThinkingLevelForModel() with gpt-5.4 detection
- Add SuggestThinkingLevelFallback() for level migration
- Auto-adjust thinking level on model switch with user notification
- Update all docs to include 'none' in valid levels
Fixes#11
Add --set-default flag to 'kit auth login' to automatically set the
provider's default model after successful authentication. When no Anthropic
credentials exist but OpenAI credentials are detected, error messages
now suggest using OpenAI with the correct --model flag.
Fixes#9
- Raise --max-tokens default from 4096 to 8192.
- Auto-raise MaxTokens toward the model's catalog Limit.Output (capped at
32768) when the user hasn't set --max-tokens explicitly and no per-model
modelSettings override applied. Prevents silent 4k/8k truncation on
models that support 32k-262k output.
- Surface FinishReasonLength at turn end: the app now subscribes to
TurnEndEvent and renders a system-message banner explaining the current
cap, the model's known ceiling, and how to raise it. Previously the TUI
swallowed 'length' stops, producing 'ghost' truncations.
- Export FinishReason* constants on pkg/kit (Stop, Length, ToolCalls,
ContentFilter, Error, Other, Unknown) and fix stale comments that used
Anthropic-style strings.
- Add Kit.MaxTokens() and Kit.MaxOutputLimit() SDK accessors, backed by
Agent.GetMaxTokens() which correctly returns 0 for providers that
suppress the param (e.g. Codex OAuth).
- Tests: rightSizeMaxTokens covers 7 paths (cap, raise, preserve,
explicit flag, nil info, zero limit); handleTurnEnd covers length/
non-length/nil-sendFn and the fallback message formatter.
- Docs: update configuration.md, cli/flags.md, and kit-extensions skill
to reflect the new default and behavior.
- Extract all MCP content types in prompt expansion: ImageContent,
AudioContent, EmbeddedResource (text and blob), and ResourceLink
- Add MCPFilePart type to carry decoded binary attachments through
the tools → SDK → bridge → UI layers
- Inline text resources as fenced code blocks with URI annotation
- Decode image/audio/blob content from base64 into LLMFilePart
attachments submitted via RunWithFiles
- Render ResourceLink as text annotation for the LLM
- Show attachment badges on user messages (e.g. '1 image(s) attached')
matching the existing clipboard paste UI pattern
- Log warnings on base64 decode failures instead of silently dropping
Phase 1: Smart @ for local files
- ProcessFileAttachments now returns FileAttachmentResult with separate
ProcessedText and FileParts fields instead of a plain string
- Binary files (images, audio, video, PDFs, etc.) detected via MIME type
are extracted as multimodal FileParts instead of XML-wrapped text garbage
- detectMediaType() uses extension-based lookup then content sniffing
- isBinaryMediaType() classifies image/*, audio/*, video/*, and specific
application types as binary
- @mcp:server:uri token format for referencing MCP resources in text
- All 4 submission paths (TUI submit, TUI steer, MCP prompt, CLI) updated
- App.RunOnceWithFiles/RunOnceResultWithFiles/RunOnceWithDisplayAndFiles
added for non-interactive multimodal submission
Phase 2: MCP resources in @ autocomplete
- MCPToolManager gains loadServerResources(), GetResources(), ReadResource(),
SubscribeResource(), UnsubscribeResource(), RefreshServerResources()
- MCPResource and MCPResourceContent types for resource metadata/content
- FileSuggestion extended with IsMCPResource, MCPServerName, MCPResourceURI
- InputComponent.SetMCPResourceProvider() wires resource suggestions into
the @ popup alongside local files
- @ popup merges local file suggestions with MCP resource suggestions,
sorted by fuzzy match score
- MCP resources display 'mcp:servername' in the popup description
- Selecting an MCP resource inserts @mcp:server:uri format
- ProcessFileAttachments resolves @mcp: tokens via MCPResourceReader callback
- Text resources are XML-wrapped as <resource>; binary resources become
FileParts for multimodal submission
- Agent, Kit SDK, and cmd/root.go wired end-to-end
Phase 3: Resource subscriptions (foundation)
- SubscribeResource/UnsubscribeResource on MCPToolManager
- onResourcesChanged callback for live refresh (wired but not yet
triggering UI refresh automatically)
- RefreshServerResources for manual resource list refresh
- Add tea.LogToFile in runInteractiveModeBubbleTea to send stdlib log
output to /tmp/kit/kit.log instead of stderr
- Replace charmbracelet/log with stdlib log in extensions loader,
runner, watcher, prompts loader, and pkg/kit so all log calls go
through the redirected stdlib logger
- Leave charmbracelet/log in CLI-only commands (install, acp) and
acpserver where stderr logging is correct
Background MCP tool loading (added in 7e54710) caused tools to not appear
in the UI because tool names and counts were captured at startup before
loading completed. This adds:
- MCPToolsReadyEvent and MCPServerLoadedEvent for progress notifications
- Dynamic GetToolNames/GetMCPToolCount callbacks for live updates
- Per-server status messages as each MCP server finishes loading
- Refresh handlers to update /tools output and status bar when ready
- Add internal/watcher package with general-purpose ContentWatcher
using fsnotify, configurable file extensions, and debouncing
- Add ContentReloadEvent and App.NotifyContentReload() for TUI signaling
- Add GetPromptTemplates/GetSkillItems callback fields on AppModelOptions
following the existing GetExtensionCommands lazy-provider pattern
- Add Kit.ReloadSkills() to re-discover skills from disk
- Wire fsnotify watcher for .kit/prompts/, .kit/skills/, .agents/skills/,
and global config directories, triggering on .md/.txt changes
- TUI refreshes autocomplete entries and skill list on reload
- Add --frequency-penalty and --presence-penalty CLI flags (0.0-2.0)
- Wire through config, viper, ProviderConfig, and fantasy agent options
- Support in config file, env vars (KIT_FREQUENCY_PENALTY), and SDK
- Pass to Ollama via options map (frequency_penalty, presence_penalty)
- Apply on both initial agent creation and runtime model swap
- Add MCPAuthHandler interface at SDK level (pkg/kit/) so all consumers
(CLI, TUI, SDK embedders) control the OAuth UX through one interface
- Default handler opens system browser + local callback server with PKCE
- CLIMCPAuthHandler wraps default with status messages (stderr pre-TUI,
system messages via TUI event system once running)
- Always enable OAuth on remote transports (streamable HTTP, SSE) when
handler is configured; harmless for servers that don't need it
- Dynamic client registration when no client ID is pre-configured
- File-based TokenStore persists tokens to ~/.config/.kit/mcp_tokens.json
keyed by server URL so users don't re-auth on restart
- Catch OAuthAuthorizationRequiredError at connection init (startup) and
tool execution (mid-session token expiry), run auth flow, retry once
- Fix error wrapping (%v -> %w) in connection pool so errors.As can
unwrap through the chain to find OAuth errors
- Thread AuthHandler through MCPToolManager -> AgentConfig ->
AgentCreationOptions -> AgentSetupOptions -> kit.Options
- ctx.Abort(): cancel current agent turn and clear queue without
injecting a new message (App.Abort + App.IsBusy methods)
- ctx.IsIdle(): check whether the agent is currently processing
- ctx.Compact(CompactConfig): trigger async context compaction with
OnComplete/OnError callbacks (App.CompactAsync method)
- ctx.SendMultimodalMessage(text, []FilePart): send text+image messages
to the agent, bridging ext.FilePart to fantasy.FilePart via RunWithFiles
- ctx.GetSessionUsage() SessionUsage: expose aggregated session token
usage and cost from the UsageTracker
New types: CompactConfig, FilePart, SessionUsage
Wired in both context setups in cmd/root.go with nil-guard defaults
in runner.go and Yaegi symbol exports in symbols.go
- Add `baseUrl` and `apiKey` fields to CustomModelConfig (config and models packages)
- Store them on ModelInfo so they travel through the registry
- createCustomProvider resolves URL/key from model definition first,
falling back to global --provider-url / --provider-api-key
- Fix registry initialisation: call ReloadGlobalRegistry() in InitConfig()
so customModels from config are visible on startup (not just at init time)
- Include custom provider in GetLLMProviders() so custom models appear
in the /model selector
- Hide the built-in custom/custom stub from the selector when user-defined
custom models are present
- Add fsnotify-based file watcher that auto-reloads extensions on .go
file changes in autoloaded dirs with 300ms debounce
- Add /reload-ext built-in command (alias /re) for manual reload
- Add Agent.SetExtraTools() so extension tools update on reload
instead of being baked in at agent creation time
- Run reload async via tea.Cmd to avoid prog.Send() deadlock when
extension handlers call ctx.Print() during SessionStart/Shutdown
- Wire watcher lifecycle into cmd/root.go with graceful shutdown
- Render ASCII logo and startup info exclusively in the ScrollList
instead of printing to stdout/terminal scrollback
- Remove PrintStartupInfo() and move kitBanner() to ui.KitBanner()
- Fix separator spacing: use single pre-rendered item with embedded
blank lines to avoid left-border artifacts on spacing rows
- Rewrite renderSessionHistory() to populate ScrollList with proper
MessageItems instead of legacy appendScrollback() calls
- Clear m.messages on /clear, /new, and /resume so the ScrollList
resets correctly when switching sessions
- Add pendingGotoBottom flag to defer scroll-to-bottom until after
distributeHeight() recalculates the correct viewport height
- Fix pre-existing test failures: initialize scrollList in test helper,
update 5 tests from tea.Println assertions to ScrollList checks
Added AddStartupMessageToScrollList() method that renders startup info
(model, context, skills, extensions, MCP tools) and extension startup
messages as system messages in the ScrollList.
This ensures startup info is visible and scrollable in alt screen mode,
rather than being printed before BubbleTea starts and becoming hidden
when alt screen takes over.
Changes:
- AppModelOptions: Added StartupExtensionMessages field
- AppModel: Store and render startup messages in Init()
- AddStartupMessageToScrollList(): Renders startup info + extension messages
- cmd/root.go: Pass startupExtensionMessages to NewAppModel
The startup info now appears at the top of conversation history and can
be scrolled back to at any time.
Replace concrete LLMMessage/LLMUsage/LLMResponse/LLMFilePart structs with
type aliases to charm.land/fantasy types, exposing them under clean
LLM-prefixed names. This gives SDK consumers full access to rich message
parts (tool calls, reasoning, tool results) without importing fantasy
directly.
Key changes:
- LLM types are now aliases: LLMMessage=fantasy.Message, etc.
- Added aliases for all part types: LLMTextPart, LLMToolCallPart, etc.
- Re-exported constructors: NewLLMUserMessage, NewLLMSystemMessage
- Removed lossy conversion helpers (llm_convert.go, fantasyMsgsToKit)
- Updated all internal packages to use aliases consistently
- Added ACP smoke test script and prompt template
- Fixed lint issues: unused vars, modernize min() usage
The compact display mode was purely a UI concern that added complexity
without providing unique value. Anyone wanting compact-style formatting
can implement it as an extension using the Renderer interface.
- Delete internal/ui/compact_renderer.go
- Remove renderToolBodyCompact and all compact tool body renderers from
tool_renderers.go
- Simplify NewCLI(debug bool) — drop compact parameter
- Simplify NewStreamComponent(width, modelName) — drop compactMode parameter
- Remove CompactMode from AppModelOptions, app.Options, CLISetupOptions
- Remove Compact from internal/config/config.go
- Remove --compact flag, var, and viper binding from cmd/root.go
- Update format.go: remove CompactRenderer interface compile-time check
and clean up comments
- Implement proper handling for all ACP content block types:
- ContentBlockText: extracts text content
- ContentBlockImage: decodes base64 to LLMFilePart
- ContentBlockAudio: decodes base64 to LLMFilePart
- ContentBlockResource: handles text and binary embedded resources
- ContentBlockResourceLink: reads files from disk
- Text files are now included inline in the message (not as FilePart)
to avoid OpenAI API errors. Only binary files (images, audio, PDFs)
are sent as FilePart attachments.
- Add fallback MIME types when not provided by client
- Add default prompt text when user attaches files without text
- Add comprehensive debug logging for content extraction
- Enable debug logging in ACP command when --debug flag is used
- Display kitBanner() before PrintStartupInfo() when running Kit normally
- The ASCII art banner with KITT scanner lights now appears at the top
of the screen, before Model, Context, Skills information
- Maintains consistent styling with the existing usage/help screen
Two related fixes for --provider-url handling:
1. Don't restore custom/* models from preferences without --provider-url
- When user runs with --provider-url, model defaults to custom/custom
- If they switch models, custom/custom gets persisted to preferences
- On next run without --provider-url, restoring custom/custom fails
- Now we skip restoring custom/* models when no --provider-url is provided
2. Auto-prefix bare model names with custom/ when --provider-url is set
- Users often provide just the model name (e.g., qwen3.5-35b-a3b)
- This failed with 'invalid model format' error
- Now auto-prefixed with custom/ for OpenAI-compatible endpoints
- Fix gofmt formatting issues in 7 files
- Replace atomic.AddUint64 with atomic.Uint64 type (modernize)
- Replace for i := 0; i < count; i++ with for i := range count (modernize)
- Replace strings.Split with strings.SplitSeq (modernize)
- Replace deprecated GetFantasyProviders with GetLLMProviders
- Replace deprecated GetFantasyMessages with GetLLMMessages
- Replace deprecated ConvertFromFantasyMessage with ConvertFromLLMMessage
- Replace deprecated FromFantasyMessage with FromLLMMessage
- Replace deprecated ToFantasyMessages with ToLLMMessages
- Remove 2 unused formatToolArgs functions
- Create ExtensionAPI interface with all extension-related methods
- Add extensionAPI type that wraps *Kit and implements the interface
- Add Kit.Extensions() method to access the ExtensionAPI
- Remove ~30 Extension* methods from Kit (breaking SDK change)
- Update all internal callers (cmd/, internal/acpserver/) to use Extensions().Method()
- Extensions themselves unaffected (use kit/ext API via Yaegi)
This cleans up the Kit API surface while maintaining full extension functionality.
Replace var function aliases with proper func wrappers (types.go)
- ParseModelString, CreateProvider, GetGlobalRegistry, LoadSystemPrompt
were package-level vars, making them reassignable and rendering oddly
in go doc. Now plain func wrappers with matching signatures.
Fix Subagent() double-error return convention
- Was returning both (*SubagentResult{Error: err}, err) simultaneously.
Now returns (nil, err) on failure, consistent with Go conventions.
- Removed SubagentResult.Error field; errors come from the error return.
- Updated all call sites in cmd/root.go, internal/acpserver, and kit.go.
Fix NavigateTo/SummarizeBranch/CollapseBranch string-encoded errors
- All three returned "" or an error string instead of error values,
making it impossible to distinguish success from failure in SummarizeBranch
(empty string meant both "no content" and "LLM failed").
- NavigateTo: string -> error
- SummarizeBranch: string -> (string, error)
- CollapseBranch: string -> error
- Updated cmd/root.go bridge closures to use err != nil and err.Error().
Remove duplicate GetSessionFilePath (use GetSessionPath)
- GetSessionPath (sessions.go) and GetSessionFilePath (kit.go) were
identical. Removed GetSessionFilePath; updated cmd/root.go and
internal/acpserver to call GetSessionPath directly.
## Dead code removal
- Delete slash_command_input.go (352 lines, never instantiated)
- Remove FormatCompactLine, StyleCompactSymbol/Label/Content from
enhanced_styles.go (zero call sites)
- Remove getTheme() alias in messages.go; standardize on GetTheme()
across compact_renderer.go (8 sites) and tool_renderers.go (14 sites)
## BubbleTea correctness
- Fix child model discards: all m.stream.Update() and m.input.Update()
calls now store the returned model via type-assertion (13 sites)
- Fix Init(): remove vestigial nil guards; StreamComponent.Init() always
returns nil so only m.input.Init() is needed
- Fix /clear divergence: remove silent InputComponent /clear handler so
parent AppModel handles it with the proper system message (one path)
## Architecture / maintainability
- Unify slash-command dispatch from two-pass (exact + prefix) to single
parse: strings.Cut once, GetCommandByName on name, pass args to
handleSlashCommand(sc, args); eliminates 3 separate dispatch sites
- Add noopCmd package-level var replacing three inline func()tea.Msg{nil}
sentinel returns
- Remove stale TAS-15/16/17 comments from interface declarations
- Deduplicate headerProviderForUI / footerProviderForUI in cmd/root.go
into a shared headerFooterProviderForUI helper (removes ~28 duplicated lines)
## Performance
- Cache glamour.TermRenderer keyed by width in styles.go; invalidate on
theme change — eliminates full goldmark parser re-init every flush tick
- Add styleMarginBottom1 package-level var replacing 9 per-frame
lipgloss.NewStyle().MarginBottom(1) allocations
- Add layoutDirty flag: replace 9 distributeHeight() calls in Update()
with m.layoutDirty=true; flush once in View() — guarantees exactly one
layout measurement per frame instead of N (reduces double-render)
- Add WidgetUpdateEvent coalescing in app.NotifyWidgetUpdate() via
atomic.Bool + 16ms debounce; prevents fast extension tickers from
flooding BubbleTea's message queue with redundant re-render triggers
## Concurrency safety
- Convert all NotifyWidgetUpdate() call sites in cmd/root.go to
go appInstance.NotifyWidgetUpdate() (16 sites) — eliminates deadlock
risk when called synchronously from inside BubbleTea's Update() handler
- Add bottom margin to startup header (KVGroup)
- Add bottom margin to thinking/reasoning blocks
- Fix thinking block footer to appear on new line without extra spacing
- Update spawn_subagent tool output to use bash-style formatting
- Add blank line after extension startup messages for visual separation
- Replace MessageRenderer with herald-based implementation
- Use herald alerts (Note, Tip, Warning, Caution) for message types
- Use blockquote for thinking/reasoning content
- Use KVGroup for startup info display
- Add margin-bottom to all message types for visual separation
- Simplify Read tool with herald CodeBlock and line numbers
- Add detectLanguage helper for syntax highlighting
- Capture extension startup messages and print after startup banner
- Remove ~200 lines of custom rendering code
Implements OAuth authentication for OpenAI ChatGPT Plus/Pro (Codex) similar to pi:
- Add OpenAICredentials type with OAuth and API key support
- Add OpenAI OAuth client with correct endpoints (auth.openai.com)
- Implement PKCE-based OAuth flow with local callback server on :1455
- Add login/logout/status commands for openai provider
- Support both ChatGPT/Codex OAuth tokens (chatgpt.com/backend-api) and
regular OpenAI API keys (api.openai.com)
- Extract and store ChatGPT account ID from JWT token
- Add custom HTTP transport with required Codex headers:
- chatgpt-account-id, originator, OpenAI-Beta: responses=experimental
- Update provider selection to use correct endpoint based on auth type
Usage:
kit auth login openai # OAuth with ChatGPT account
kit auth logout openai
kit auth status
The implementation follows the same patterns as the existing Anthropic OAuth
support, with automatic token refresh and secure credential storage in
~/.config/.kit/credentials.json
When switching models (e.g., via /model command or ctx.SetModel), the usage
tracker now updates its model info to reflect the new model's:
- Pricing for cost calculations
- Context limits for percentage display
- OAuth status (to show bash costs when using OAuth creds)
Previously, token costs and context percentages continued using the old
model's settings after a switch, causing incorrect display for:
- Users switching from paid to free/OAuth models
- Users switching between models with different pricing
Changes:
- Add UpdateModelInfo() method to UsageTracker
- Call UpdateModelInfo() in both SetModel callbacks (extension and UI)
- Add auth import for OAuth detection in root.go
Allow users to define custom models in ~/.kit.yml under the customModels
section. These models are automatically merged into the custom provider.
Example config:
customModels:
my-model:
name: "My Custom Model"
reasoning: true
temperature: true
cost:
input: 0.002
output: 0.004
limit:
context: 128000
output: 32000
Usage:
kit --model custom/my-model "Hello"
kit --provider-url "http://localhost:8080" --model custom/my-model "Hello"
Note: When --provider-url is specified without --model, kit defaults to
custom/custom. When --provider-url is specified WITH a custom model from
config, that model is used.
Bug fixes:
- Fixed kit.New() re-loading config file and overriding CLI-specified config
- Fixed models command to reload registry for custom models
When users pass --provider-url without --model, automatically default
to custom/custom instead of the saved model preference. This lets users
point kit at any OpenAI-compatible endpoint without needing a provider/model
pair from the database.
The custom/custom model has:
- Zero cost (input/output = 0)
- 262K context window, 65K output limit
- Reasoning and temperature support
- Routes through openaicompat fantasy provider
The skill command now runs 'npx skills add mark3labs/kit' without
filtering to a single skill, installing both available skills:
1. Extensions - creating Kit extensions
2. SDK - building with the Kit Go SDK
Add user-defined prompt templates that expand into full prompts with
shell-style argument substitution.
Features:
- Templates loaded from ~/.kit/prompts/*.md and .kit/prompts/*.md
- YAML frontmatter support for description
- Argument placeholders: $1, $2, $@, $ARGUMENTS, ${@:N}, ${@:N:L}
- Autocomplete integration (templates appear as /name commands)
- CLI flags: --prompt-template and --no-prompt-templates
- First-match-wins collision handling with logged diagnostics
Example template:
---
description: Review code for issues
---
Review the following code for bugs and security issues.
Focus on $1 specifically.
Usage: /review error handling
Model and thinking level choices now survive restarts, matching the
existing theme persistence pattern. Selections are saved to
~/.config/kit/preferences.yml and restored on next launch.
Precedence: CLI flag > config file > saved preference > default
Changes:
- Extended preferences struct with model and thinking_level fields
- Refactored preferences.go to shared load/save helpers (DRY)
- Added SaveModelPreference/LoadModelPreference
- Added SaveThinkingLevelPreference/LoadThinkingLevelPreference
- Persist on /model, model selector, /thinking, and Shift+Tab cycle
- Restore at startup in runNormalMode when no explicit flag/config
- Added modelFlagChanged/thinkingFlagChanged to detect explicit flags
- Comprehensive tests for all preference operations
Theme choices via /theme or ctx.SetTheme() were previously lost on
restart. Now the selected theme name is saved to
~/.config/kit/preferences.yml and restored on next launch.
Precedence: .kit.yml theme > preferences.yml > default (kitt).
- Add internal/ui/preferences.go with atomic save/load
- ApplyTheme() now persists; ApplyThemeWithoutSave() for startup
- Fallback to saved preference in cmd/root.go init()
Add RegisterTheme, SetTheme, and ListThemes to the extension Context,
allowing extensions to create custom themes at runtime and switch
between them. Uses ThemeColor/ThemeColorConfig concrete structs (no
interfaces) for Yaegi safety.
Include neon-theme.go example extension demonstrating the API.