* cmd: add --no-skills, --skill, and --skills-dir CLI flags
The pkg/kit Options struct already had full backend support for skills
control (NoSkills, Skills []string, SkillsDir) wired into loadSkills()
in pkg/kit/kit.go, but there were no corresponding CLI flags to drive
them. This commit closes that gap.
Changes in cmd/root.go:
- Add three package-level flag variables alongside the existing
noExtensionsFlag/extensionPaths group:
noSkillsFlag bool
skillsPaths []string
skillsDir string
- Register three persistent cobra flags in init():
--no-skills disable skill loading (auto-discovery and explicit)
--skill <path> load a skill file or directory (repeatable)
--skills-dir <dir> override the project-local skills directory
used for auto-discovery
- Wire all three into the kitOpts struct literal in runNormalMode()
so they flow directly into kit.New() -> loadSkills().
No changes to pkg/kit or internal/skills -- the backend was already
complete. No viper binding is needed because kit.go reads these fields
directly from opts rather than from viper (unlike NoExtensions which
uses the viper fallback path).
Example usage:
kit --no-skills "prompt"
kit --skill ./my-skill.md --skill ./other-skill.md "prompt"
kit --skills-dir /path/to/skills "prompt"
Co-authored-by: Claude <claude@anthropic.com>
* docs: document --no-skills, --skill, and --skills-dir CLI flags
Add the three new skills CLI flags to all relevant documentation:
- README.md: add Skills section under Global Flags CLI reference
- www/pages/cli/flags.md: add Skills table (mirrors Extensions section pattern)
- www/pages/cli/commands.md: expand the Skills section with usage examples
and a description of auto-discovery vs explicit loading vs --no-skills
Co-authored-by: Claude <claude@anthropic.com>
* feat: add config file support for skills options
Skills could previously only be controlled via CLI flags or SDK Options
fields. This commit wires all three skills settings into viper so they
can also be set in .kit.yml / .kit.yaml / .kit.json and via KIT_*
environment variables — matching the pattern used by no-extensions,
no-core-tools, and prompt-template.
cmd/root.go:
- Bind --no-skills, --skill, and --skills-dir flags to viper keys
(no-skills, skill, skills-dir) so config file values flow through.
pkg/kit/kit.go:
- At skill-load time, merge opts fields with viper values:
- noSkills = opts.NoSkills || v.GetBool("no-skills")
- skillPaths: opts.Skills if non-empty, else v.GetStringSlice("skill")
- skillsDir: opts.SkillsDir if non-empty, else v.GetString("skills-dir")
- Build a shallow-copied mergedOpts so loadSkills() picks up the
resolved values without mutating the original Options struct.
docs:
- README.md: add skills keys to the Basic Configuration YAML example
- www/pages/configuration.md: add no-skills, skill, skills-dir rows to
the All configuration keys table
Config file example (.kit.yml):
no-skills: false
skill:
- /path/to/skill.md
skills-dir: /path/to/skills/
Co-authored-by: Claude <claude@anthropic.com>
* config: add skills keys to default .kit.yml template
Add no-skills, skill, and skills-dir as commented-out examples in the
default config file generated by EnsureConfigExists(), alongside the
existing application settings block.
Co-authored-by: Claude <claude@anthropic.com>
* test: add test coverage for skills CLI flags and config keys
Four test locations updated:
pkg/kit/export_test.go:
- Add ConfigStringSliceForTest() helper to expose v.GetStringSlice()
from the Kit's isolated viper store, needed to assert skill list values.
pkg/kit/kit_test.go (TestNewWithSkillsOptions):
- NoSkills=true: GetSkills() returns empty slice
- SkillsDir=<empty dir>: kit.New() succeeds with zero skills
- Skills=[file]: single explicit skill file is loaded and name parsed correctly
pkg/kit/viper_isolation_test.go:
- TestSkillsViperKeys: no-API-key struct-level checks for NoSkills, Skills,
and SkillsDir fields on Options
- TestSkillsConfigFileKeys: full kit.New() round-trips via a written .kit.yml
for each of the three config keys:
no-skills: true → GetSkills() returns empty
skill: [path] → named skill loaded from config file path
skills-dir: dir → custom discovery root accepted without error
internal/config/config_test.go (TestEnsureConfigExists):
- Assert generated ~/.kit.yml template contains '# Skills configuration',
'no-skills:', and 'skills-dir:' comment blocks.
Co-authored-by: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
* Remove dead code: 5 unused symbols across internal packages
- internal/models: LoadModelSettingsFromConfig (zero refs)
- internal/prompts: PromptTemplate.ExpandWithArgs (zero refs)
- internal/app: NewMessageStore (tests migrated to NewMessageStoreWithMessages)
- internal/config: HasEnvVars (+ its test)
- internal/core: ContextWithSudoPassword (test migrated to context.WithValue)
* pkg/kit: use TreeManager alias in exported signatures
NewTreeManagerAdapter and InitTreeSession now spell their signatures with
the public kit.TreeManager alias instead of internal/session.TreeManager,
so go doc renders domain types rather than internal paths.
* Consolidate tool-kind classification into internal/extensions
coreToolKinds + toolKindFor were duplicated verbatim in
internal/extensions/wrapper.go and pkg/kit/events.go, risking silent
divergence between extension events and SDK events. Single source of
truth now lives in internal/extensions/toolkinds.go; pkg/kit re-exports
the constants.
* Consolidate Anthropic OAuth detection and usage-tracker refresh
The 'is the active Anthropic credential a stored OAuth token' check was
copy-pasted at 5 sites, all prefix-matching the magic string
'stored OAuth' produced in internal/auth. Now:
- internal/auth: new CredentialSourceOAuth constant + IsAnthropicOAuth()
- internal/ui: new UpdateUsageTrackerForModel(); CreateUsageTracker and
SetupCLI share lookupTrackableModel (SetupCLI no longer re-inlines the
tracker construction)
- cmd/root.go + cmd/extension_context.go: verbatim-duplicated tracker
refresh blocks replaced with ui.UpdateUsageTrackerForModel
- pkg/kit isAnthropicOAuth delegates to auth.IsAnthropicOAuth
- internal/models compares source against the constant
* pkg/kit: consolidate model-path helpers and argument tokenizer
- ExtractModelFromPath mis-parsed model IDs containing '/' (e.g.
'openrouter/meta/llama' -> 'meta'); it now delegates to
RemoveProviderFromModel and is deprecated alongside
ExtractProviderFromPath (-> GetCurrentProvider)
- parseFields delegated to prompts.ParseCommandArgs so extension argument
parsing and builtin prompt-template parsing share one quote/escape
grammar; ParseCommandArgs now also splits on tabs (superset of both
previous tokenizers)
* Unify the two {{variable}} template engines
internal/skills and pkg/kit/template_bridge each had their own grammar:
skills rejected '{{ name }}' (whitespace) but allowed digit-first names;
the bridge was the opposite. A template behaved differently depending on
whether it was loaded as a skill prompt or via the extension API.
internal/skills is now the single engine using the superset grammar
(\{\{\s*(\w+)\s*\}\}); pkg/kit ParseTemplate/RenderTemplate are thin
adapters over it. Expand is now regex-based so whitespace placeholders
expand consistently; missing variables are still left as-is.
* internal/ui: extract switchModel helper for model-switch flow
The model-selector handler (ModelSelectedMsg) and /model slash command
duplicated the full switch sequence (thinking-level fallback, setModel,
display-state update, preference persistence, ModelChange emit) and had
already drifted in ordering. Both now call a single switchModel method.
Display state is still updated directly (no prog.Send from Update).
* extbridge: extract shared BaseContext for extension wiring
cmd/extension_context.go and internal/acpserver/session.go each built a
giant extensions.Context literal, duplicating ~15 delegation closures
(GetContextStats, GetMessages, AppendEntry, options, SetModel core,
Complete, SpawnSubagent, ...) that had to be kept in sync by hand. New
data-access fields had to be wired in both places or ACP-mode extensions
silently got nil function fields.
extbridge.BaseContext now provides the headless half; both call sites
overlay only their UI-specific closures. As a side effect ACP mode gains
previously-missing APIs (state, tree navigation, skills, template
parsing, model resolution) that were nil before. The interactive TUI
keeps its exact SetModel/ReloadExtensions ordering via overrides.
* internal/tools: extract withOAuthRetry and marshalToolResult helpers
ExecuteTool repeated the OAuth-error/re-auth/retry stanza verbatim twice
(sync and task-augmented paths) and the marshal-and-wrap stanza four
times. Both are now single helpers with identical error strings, so a
fix to OAuth retry or error categorization applies everywhere at once.
* internal/ui: extract buildShareFile with defer-based cleanup
handleShareCommand repeated the close/remove/print/return cleanup chain
four times across its temp-file write error paths. File assembly now
lives in buildShareFile with a single deferred cleanup on error.
* cmd: extract flag validation, preference restore, and provider-URL routing from runNormalMode
runNormalMode opened with ~150 lines of policy logic (flag-combination
validation, persisted model/thinking-level preference restoration, and
two subtle --provider-url model-rewrite rules). These are now standalone
functions (validateModeFlags, restorePersistedPreferences,
applyProviderURLRouting) so the routing policy is independently readable
and testable. Behaviour unchanged; ordering preserved.
* fix: address review findings on SDK godoc and nil guard
- pkg/kit: remove internal package paths from exported godoc on
ParseTemplate and the ToolKind* constants (SDK doc surface must not
reference internal packages)
- internal/tools: guard marshalToolResult against a nil CallToolResult
(json.Marshal(nil) succeeds as 'null', then result.IsError panics if
a client returns nil result with nil error)
Skipped the TreeNode Children deep-copy suggestion: the slice already
comes from TreeManager.GetChildren which returns a fresh copy per call
into a throwaway intermediate, so no internal state is exposed.
While the tool list of the main agent could be controlled by several
options, subagent used to be equipped with all available tools (except
for the subagent tool itself).
With this change the list of tools is taken from the parent, the
subagent tool itself is removed and the remaining tool list is added
to the subagent.
Signed-off-by: Egbert Eich <eich@suse.com>
* feat(extensions): add OnLLMUsage, SetState, enriched AgentEndEvent (#53)
Three additive primitives to the extension API:
- OnLLMUsage event: per-LLM-call token + cost deltas attributed to the
specific model/provider used for each round-trip. Derived from the SDK
StepFinishEvent in the extension bridge. Enables accurate budget
enforcement between calls instead of only at turn boundaries.
- ctx.SetState / GetState / DeleteState / ListState: session-scoped,
last-write-wins key-value store backed by a sidecar file
(<session>.ext-state.json) outside the conversation tree. Reads are
O(1), writes don't grow the JSONL, and the store is not duplicated on
fork. State is preserved across hot-reloads.
- Enriched AgentEndEvent: ToolCallCount, ToolNames, LLMCallCount, token
deltas (input/output/cache-read/cache-write), CostDelta, and
DurationMs populated by a per-turn aggregator. Existing handlers
reading only Response/StopReason are unaffected.
Includes unit tests for the state store, LLMUsage registration,
enriched AgentEndEvent, turn aggregator, llmUsageMeta, and sidecar path
derivation. Adds examples/extensions/usage-budget.go demoing all three
primitives together. Documents the additions in README, the docs site
(extensions overview, capabilities, examples), and the kit-extensions
and kit-sdk skill guides.
Fixes#53
* fix(extensions): address review feedback on state store and llmUsageMeta
- Serialize SetState/DeleteState saver invocations through a new saverMu
so overlapping atomic-rename writes can no longer race on the shared
.tmp file and persist an older snapshot after a newer one.
- LoadStateFromFile now clears the in-memory store when the sidecar is
missing or empty, matching the documented "replace … with its
contents" contract. This makes session-switching safe by preventing
keys from a prior session leaking into a new one. Tests updated to
cover both the missing-file and empty-file cases.
- llmUsageMeta now detects Anthropic OAuth credentials and returns
Cost=0, matching the comment and the existing usage_tracker behavior
for OAuth users. Mirrors the OAuth detection already used in
cmd/extension_context.go.
- Document the single-in-flight-turn assumption baked into the
per-turn aggregator with a clear migration path (per-turn ID) for if
concurrent turns ever become a supported use case.
* fix(extensions): release saverMu on panic in state store
Extract a runSaver helper that locks saverMu and defers Unlock before
invoking the persistence callback. Without the deferred Unlock, a panic
inside the saver (e.g. disk full mid-write) would leave saverMu held
forever and deadlock the next SetState/DeleteState. Both SetState and
DeleteState now route through the helper. New TestRunner_State_Saver
PanicReleasesSaverMu reproduces the deadlock window with a 2s deadline
and proves the mutex is released after a panic.
* feat(auth): add Copilot login
Add experimental GitHub Copilot device login and copilot/* provider support for users with Copilot access but no OpenAI account.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(copilot): use responses for GPT-5
Route Copilot GPT-5 models through the Responses API because gpt-5.5 is not available on /chat/completions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(copilot): honor device flow timing
* docs(copilot): add auth helper docstrings
* fix(auth): address copilot review feedback
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When --provider-url was set with an explicit --model that already carried
a provider prefix (e.g. google/gemma-4-12b served by LM Studio), Kit
honored the prefix and routed through the Google wire protocol instead
of the user-supplied endpoint, producing confusing upstream errors.
- Strip any non-custom provider prefix from --model when --provider-url
is set, so the request always lands on the OpenAI-compatible custom
wire pointed at the user's URL.
- Leave behavior unchanged when --provider-url is absent.
- Document the rewrite in www/pages/providers.md.
- Add PopLastUserMessage() on *App: walks the current tree branch back to
the parent of the most recent user message, syncs the in-memory store,
and returns the prompt + image parts for resubmission.
- Register /retry (alias /rt) and wire handleRetryCommand which rebuilds
the visible ScrollList from the truncated branch before resubmitting
via Run/RunWithFiles. Mirrors SubmitMsg display path (badges, pending
prints, stateWorking transition).
- Recovers from transient provider errors (overloaded, timeout) without
duplicating the user message in context — the failed turn's entries
become orphaned off-branch rather than being re-sent to the LLM.
- Update help text, AppController interface, and stub controller.
- Add unit tests covering busy/closed/no-session guards, the happy-path
truncation, and the empty-branch error case.
- Extend PopupList with FullScreen mode, RenderItem callback, and
external-state setters (SetItems/SetCursor/SetSearch) so any popup
can reuse the same chrome (border, title, search, scroll, footer).
- Rewrite TreeSelector and SessionSelector as thin PopupList wrappers,
dropping ~500 lines of duplicated rendering. Selector-specific keys
(filter cycle, scope/named toggles, delete-confirm) are pre-handled;
everything else delegates to PopupList.
- Migrate the / and @ autocomplete popups in InputComponent to render
through PopupList, replacing the bespoke renderer.
- Fix /tree and /fork overflow with deep trees: measure tree-art
prefix width via lipgloss.Width (handles multi-byte box drawing),
truncate the prefix from the left with an ellipsis when it would
push text off the row, and collapse multi-line message content to
a single line so rows never wrap.
- Fix broken selection highlight in /tree, /fork, /sessions: emit a
plain string from RenderItem for the cursor row so the outer row
style paints one continuous fg+bg span instead of being shredded
by mid-row ANSI resets from inner Render calls.
- Center the cursor in the visible window so context is always shown
above and below the selection.
- New /edit (alias /ed) opens $EDITOR on a chosen file via tea.ExecProcess
- Typing '/edit ' activates a fuzzy file popup mirroring the @ trigger:
reuses GetFileSuggestions (git ls-files), supports directory drill-down,
excludes MCP resources
- Selecting a file auto-submits and runs $EDITOR ($VISUAL preferred);
on exit prints 'Edited <path>'
- Manual paths supported (~/, relative, absolute); non-existent paths
pass through so the editor can create them; directories are rejected
- /help updated with the new command
- Add sdkDefaultBaseURL map covering the 14 npm SDKs that ship a
hard-coded baseURL (groq, cerebras, mistral, xai, perplexity,
togetherai, deepinfra, cohere, v0, aihubmix, venice, merge-gateway,
openrouter, vercel gateway), so providers whose models.dev entry
omits the api field still auto-route correctly.
- Extend npmToWireProtocol so these thin OpenAI-compatible wrappers
route through fantasy's openaicompat provider.
- Add resolveTemplatedAPIURL to substitute ${VAR} placeholders for
cloudflare-workers-ai, databricks, snowflake-cortex from the env,
with friendly errors that name the missing vars.
- Wire amazon-bedrock and azure-cognitive-services aliases into the
existing native handlers; add createGoogleVertexProvider for the
google-vertex case.
- Expose kit.ResolveProviderBaseURL in the public SDK so embedders
can introspect the effective endpoint before instantiating a Kit.
- Refresh embedded_models.json from models.dev (5113 -> 5121 models;
139 providers unchanged).
- Remove deprecated GenerateWithLoopAndStreaming and TreeManager
AppendFantasyMessage / AddFantasyMessages / GetFantasyMessages to
close the SDK leakage caused by the kit.TreeManager type alias
- Switch extensionAPI method signatures to local Extension* aliases so
pkg.go.dev signatures no longer expose internal package names
- Bundle runNormalMode dependencies into a runModeDeps struct, shrinking
the runNonInteractive and runInteractive call sites from 40+ positional
args to (ctx, deps)
- Add generic subscribeTyped[E Event] helper and collapse ~30 typed OnXxx
wrappers in pkg/kit/events.go onto it (public signatures unchanged)
- Extract setupBashPipes / interpretBashExit in internal/core/bash.go to
deduplicate the buffered and streaming execution paths
- Extract resolveAutoRouteAPIKey and wrapProviderErr helpers in
internal/models/providers.go and uniformly apply them across every
createXxxProvider site
- Reimplement internal/extensions/watcher.go as a thin wrapper over the
general-purpose internal/watcher.ContentWatcher, eliminating ~130 LOC
of duplicated fsnotify logic while preserving the existing test API
- Add ctx.Err() pre-flight checks in executeRead / Write / Edit / Ls so
cancellation actually short-circuits pure file-IO tools
* fix(ui): show pasted image previews in input and transcript
The half-block thumbnail preview added in #47 rendered but was clipped
off the bottom of the screen, and submitted images showed only a text
badge in the conversation history.
- Mark the layout dirty when clipboardImageMsg / thumbnailReadyMsg reach
the parent, so distributeHeight re-measures the now-taller input region
instead of keeping a stale height that pushed the preview off-screen
- Render thumbnail previews in the transcript after a user message,
appended as a verbatim ScrollList item (raw ANSI half-blocks would be
mangled if folded into the word-wrapped user text block)
- Render transcript previews asynchronously via a tea.Cmd so decode +
resample never blocks the Bubble Tea event loop
- Add regression tests covering the input layout recompute and the
transcript preview flow
* fix(ui): anchor transcript image preview to its user message
- Insert the async thumbnail preview directly after the originating user
message (tracked via anchorID) instead of appending, so a streamed
assistant reply that lands first no longer pushes the preview out of place
- Make the layout regression test deterministic by forcing a truecolor
profile, avoiding flakes on low-color CI terminals where the thumbnail
would render empty
- Add tests for anchored insertion and the unknown-anchor append fallback
* feat(ui): render half-block thumbnails for attached images (#46)
- Add internal/ui/imagepreview package: Render() draws low-res
thumbnails using Unicode half-blocks (▀) + truecolor/256-color SGR,
which survives tmux/zellij (no graphics protocol)
- Cache a rendered thumbnail per pending clipboard image in the input
component; render once at attach time, never per frame
- Fall back to the existing [N image(s) attached] text pill when the
terminal lacks truecolor/256-color support
- Document Ctrl+V paste, Ctrl+U clear, and the preview in the docs
site and README keyboard shortcuts
Fixes#46
* fix(ui): render image thumbnails off the event loop and cap size
- Render thumbnails asynchronously via a tea.Cmd instead of calling
the decode + resample path synchronously inside Update(), which
blocked the Bubble Tea event loop
- Add thumbnailReadyMsg + an imageGen generation counter so async
results land on the correct pendingImages slot and stale renders
after a clear/re-attach are discarded
- Guard imagepreview.Render against decompression bombs by checking
DecodeConfig dimensions against a max before full decode
* fix(ui): skip image preview when input width is too small
- Return 0 from thumbCols when width <= 6 so a full-size thumbnail is
no longer rendered for tiny or uninitialized (width 0) terminals;
the caller falls back to the text pill
- replace npmToLLMProvider map with npmToWireProtocol (openai/anthropic/google)
- add createAutoRoutedGoogleProvider so @ai-sdk/google proxies work
(fixes opencode/gemini-* failing with "no LLM provider mapping")
- strip the genai-injected v1beta segment for proxies whose base URL
already carries a version (e.g. opencode's /zen/v1)
- preserve openai-compat fallback and clearer error for unroutable providers
- document auto-routing in README and providers docs; update CreateProvider godoc
- add regression tests for wire routing and version-path rewriting
Fixes#41
* feat(kit): isolate viper config per Kit instance + add NewAgent (#40)
- Give each kit.New()/NewAgent() call an isolated *viper.Viper store so
multiple Kit instances in one process no longer clobber each other's
config; runtime mutators (SetModel, SetThinkingLevel) touch only the
owning instance, making subagent spawning and multi-Kit embedding
race-free
- Thread the per-instance store through internal/config, internal/models
(ProviderConfig.ConfigStore), internal/kitsetup, and the extension
runner, with a nil -> process-global fallback so the CLI is unaffected
- Share the global store when Options.CLI != nil to preserve cobra flag
bindings (also opted in for internal/acpserver)
- Remove viperInitMu; preserve the tri-state IsSet precedence contract
and sdkDefaultMaxTokens floor
- Add ergonomic NewAgent + functional options (WithModel, WithStreaming,
Ephemeral, etc.); NewAgent defaults streaming on, opt out via
WithStreaming(false). New(ctx, *Options) behavior is unchanged
- Add config-isolation regression test and NewAgent/option coverage;
document NewAgent and per-instance isolation in README
Fixes#40
* docs(sdk): document NewAgent options and per-instance config isolation
- Add "Functional options (NewAgent)" and "Per-instance config isolation"
sections to the docs site SDK overview, with an options table and a
"when to use which" constructor comparison
- Cross-reference NewAgent from the SDK options page and correct the now
per-instance ProviderAPIKey precedence wording
- Document NewAgent + With* helpers and config isolation in pkg/kit/README
and list NewAgent/Option in the API reference
- Show the NewAgent constructor in the SDK examples getting-started snippet
* fix(kit): correct config loading and isolate ACP sessions
- Isolate each ACP session's config store instead of sharing the global
viper, preventing per-session SetModel/SetThinkingLevel races; seed the
root-command flag values (model, thinking-level, provider URL/key) so
`kit acp -m <model>` is still honored
- Run initConfig for isolated SDK stores by gating on opts.CLI instead of
v.GetString("model"), which setSDKDefaults always populates and thus
skipped .kit.yml / KIT_* loading for SDK callers
- Configure KIT_* env overrides unconditionally in initConfig so passing an
explicit config file no longer disables environment variable support
- Wrap config unmarshal/validate errors with %w to preserve the error chain
* fix(kit): make Options.Streaming a *bool to honor unset
- Change Options.Streaming from bool to *bool so a zero-valued Options no
longer forces stream=false; New only sets the key when non-nil, letting
streaming resolve through the precedence chain (env -> config -> default
true). This also fixes the CLI path, which never set the field
- Mirror the existing sampling-parameter pointer pattern instead of adding
a separate StreamingSet sentinel, keeping Options internally consistent
- Update WithStreaming/NewAgent, subagent, and ACP callers to the pointer
form; add regression tests for the nil-default and explicit opt-out paths
- Update SDK docs (README, pkg/kit/README, options page) with the ptrBool
helper and *bool semantics
* fix(kit): inherit parent provider config in subagents
- Copy the parent's effective provider/runtime config (API key, URL,
TLS, thinking level, max-tokens, samplers) onto child Options in
Kit.Subagent. After the per-instance viper isolation, the child's
isolated store only re-loaded .kit.yml / KIT_*, silently dropping
config the parent set via programmatic Options or runtime setters
like SetThinkingLevel
- Preserve the IsSet tri-state for max-tokens and samplers so per-model
defaults still apply on the child when the parent left them unset
- Add TestInheritProviderConfig covering propagation, unset keys, and
nil-safety
* feat(core): expose no-core-tools via CLI flag and config file
Allow users to disable all built-in core tools (bash, read, write,
edit, grep, find, ls, subagent) without recompiling, using a CLI flag,
environment variable, or .kit.yml config key.
Changes
-------
cmd/root.go
- Declare noCoreToolsFlag bool alongside noExtensionsFlag.
- Register --no-core-tools persistent flag with a descriptive help string
listing the affected tools.
- Bind the flag to viper key "no-core-tools" so the config file and
KIT_NO_CORE_TOOLS env var also work (viper's standard precedence:
CLI flag > env var > config file > default).
- Set kitOpts.DisableCoreTools = viper.GetBool("no-core-tools") when
assembling the Options struct in runNormalMode.
pkg/kit/kit.go
- Add disableCoreTools local variable inside the viperInitMu-protected
snapshot block, mirroring the noExtensions pattern exactly.
- Resolve it as opts.DisableCoreTools || viper.GetBool("no-core-tools")
so the SDK option and the viper key are both respected (OR semantics:
either source can enable the flag).
- Pass the resolved disableCoreTools into kitsetup.AgentSetupOptions
instead of the raw opts.DisableCoreTools, completing the chain.
Usage
-----
# CLI flag
kit --no-core-tools
# Environment variable
KIT_NO_CORE_TOOLS=true kit
# .kit.yml config file
no-core-tools: true
# SDK (unchanged, was already supported)
kit.New(ctx, &kit.Options{DisableCoreTools: true})
The downstream path (kitsetup → agent.AgentConfig.DisableCoreTools →
agent.NewAgent nil-tool branch) was already in place and required no
changes.
* docs(readme): document no-core-tools flag, config key, and env var
Update three locations in README.md to reflect the new no-core-tools
control surface introduced in the previous commit:
CLI Reference → Global Flags
Add --no-core-tools under the Extensions and tools section alongside
--no-extensions, with a description listing the affected tools.
Configuration → Basic Configuration
Add no-core-tools: false to the example .kit.yml block so users know
it is a valid config file key (equivalent to the CLI flag and env var).
Go SDK → With Options
Expand the DisableCoreTools comment to note that the same behaviour is
also available via --no-core-tools, KIT_NO_CORE_TOOLS, and
no-core-tools: true in .kit.yml, making the cross-surface relationship
explicit for SDK consumers.
* style: gofmt cmd/root.go
* feat(sdk): runtime skills and context-file management (#36)
Let SDK consumers add, remove, and replace skills and AGENTS.md-style
context files after Kit construction. Every mutation recomposes the
system prompt and applies it to the agent so the next turn picks up
the new instructions without restarting Kit.
- AddSkill / LoadAndAddSkill / RemoveSkill / SetSkills on *kit.Kit
- AddContextFile / AddContextFileContent / LoadAndAddContextFile /
RemoveContextFile / SetContextFiles on *kit.Kit
- RefreshSystemPrompt to force a manual recomposition
- agent.SetSystemPrompt / GetSystemPrompt on the internal agent so
the composed prompt rebuilds the fantasy agent on the next call
- Per-instance runtimeMu guards skills/contextFiles; GetSkills and
GetContextFiles return defensive snapshots safe for concurrent use
- Capture the resolved basePrompt during New so recomposition keeps
per-model overrides and --system-prompt file resolution intact
- Skills dedupe by Name; context files dedupe by Path (opaque ID,
not required to be a real filesystem path)
Tests cover add/remove/set/replace semantics, validation errors,
disk loading round-trips, prompt composition, and an 8-goroutine
race-stress sweep (go test -race clean).
Docs: pkg/kit/README, root README Go SDK section, www sdk/overview
"Runtime skills and context files" section, www sdk/options callout
cross-referencing the new API.
Fixes#36
* fix(agent): synchronize SetSystemPrompt against concurrent rebuilds
- add promptMu to Agent guarding systemPrompt writes and the fantasy
agent rebuild, fixing a data race when Kit.applyComposedSystemPrompt
is invoked concurrently
- read systemPrompt under the same lock in GetSystemPrompt
- update the thread-safety stress test to use a non-nil agent so the
SetSystemPrompt path is actually exercised under -race
fantasy v0.25.1+ bumps kaptinlin/jsonschema to v0.7.14+, which transitively
pulls in github.com/agentable/go-intl. Its internal/cldr/displaynames
package contains a ~143k-line / 5.4MB generated CLDR map literal that
compiles at ~6.7GB RSS, OOM-killing release builds on 7GB GitHub runners.
Pinning fantasy v0.25.0 (jsonschema v0.7.13, go-i18n v0.4.5,
messageformat-go v0.6.0) removes go-intl from the build graph entirely and
restores clean-build peak RSS from ~6.6GB back to ~1GB. Upstream issue filed
against charmbracelet/fantasy.
A single clean cross-build peaks at ~7GB RSS (internal/extensions yaegi
symbol table). goreleaser builds targets in parallel by default, which
exhausts the 7GB ubuntu-latest runner and OOM-kills the build with no
error output. Force --parallelism 1 and cap go compiler with GOFLAGS=-p=2.
- Remove unused SetOpenAICredentials/validateOpenAIAPIKey (internal/auth)
- Remove unused SudoPasswordRequiredMetadata/IsSudoPasswordRequiredResult
(internal/core)
- Add Extension* type aliases in pkg/kit/extension_api.go so the public
ExtensionAPI interface no longer exposes internal/extensions types
- Extract bridgeObserve generic helper and llmToContextMessages /
contextMessagesToLLM in pkg/kit/extensions_bridge.go (~150 lines saved)
- Extract parseHeaders and buildOAuthConfig in connection_pool.go to
deduplicate SSE/Streamable client construction (~60 lines saved)
- Eliminate redundant second buildInteractiveExtensionContext call in
cmd/root.go; swap print closures on the same context instead
- Replace 'Fantasy' with 'agent' in internal comment (pkg/kit/kit.go)
Previously GenerateWithCallbacks stored the most recent tool call's args
in a single shared variable, which got clobbered when a provider emitted
multiple tool_use blocks in a single step. Every OnToolResult callback
then received the args of the last OnToolCall, regardless of which call
it was actually resolving — breaking any downstream UI, log, or trace
that derived its description from the toolArgs parameter.
- Replace the shared currentToolArgs with a map keyed by ToolCallID,
guarded by a sync.Mutex in case the streaming layer dispatches
callbacks from multiple goroutines.
- Delete each entry in OnToolResult so the map cannot accumulate
across steps.
- Add a regression test driving the streaming wrapper with a fake
fantasy.Agent that emits two parallel tool calls before either
result, asserting each callback sees its own args.
Fixes#33
Open the /resume session picker faster by extracting per-file metadata
across a GOMAXPROCS-sized worker pool instead of sequentially. Each
extractSessionInfo call is I/O + JSON-parse bound and independent, so
wall time drops roughly proportionally to core count — meaningful for
users with many sessions, where ListSessions + ListAllSessions ran
back-to-back on the UI goroutine before the picker rendered.
- Switch NotifyWidgetUpdate from leading-only to leading+trailing edge
coalescing so a rapid SetWidget→RemoveWidget pair (e.g. emitted by
subagent-monitor on SubagentEnd) is never silently dropped.
- Without the trailing send the TUI keeps the pre-removal widget
height, leaving empty rows below the status bar until some other
event re-renders the layout.
- Match View() and getItemAndLineAtY() row counts for empty items so
streaming-reasoning placeholders no longer offset hit-testing by one
row each (exposed when extension widgets like subagent-monitor shrink
the scrollback).
- Honor IsLineInRange's endCol=-1 'to end of line' sentinel in
HighlightLine and ExtractText so the start row of a multi-line drag
actually renders highlighted and is included in clipboard copies.
- Add regression tests for both invariants in scrolllist and selection.
- Add prompts.GlobalDir() resolving $XDG_CONFIG_HOME/kit/prompts/
(default ~/.config/kit/prompts/) so prompt templates live alongside
extensions and skills under the same XDG-aligned root.
- LoadAll now discovers templates from both the legacy ~/.kit/prompts/
and the XDG location; existing legacy paths keep precedence.
- Include GlobalDir() in the prompts/skills file watcher so edits
under ~/.config/kit/prompts/ hot-reload automatically.
- Surface a visible 'Extensions reloaded.' (or error) message when
the extension watcher fires, matching /reload-ext feedback.
- Restore examples/extensions/subagent-monitor.go alongside its test
and update the test load path; previous move left the test broken.
- Remove .kit/extensions/go-edit-lint.go from the repo since the
extension is now installed under ~/.config/kit/extensions/ for
per-user use across all projects.
- 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.
- Lock viewport scroll while a drag-select is active so highlighted
content stays under the cursor (SetItems, appendStreamingChunk,
MouseWheelDown all now honor IsMouseDown).
- HandleMouseDrag defensively clears autoScroll on every update so a
racy re-enable can't shift the row mid-drag.
- Recompute scrollback yOffset/viewport height on each mouse event
via currentScrollbackBounds() instead of relying on stale values
cached during the previous View() pass.
- Account for canceling/ctrlCPressedOnce warning rows in
distributeHeight and mark layoutDirty when those flags toggle so
the height budget and mouse origin stay in sync.
- Add ScrollList regression tests covering the three invariants.
- Register /copy (alias /cp) in the System command category
- Walk the scrollback to find the last user/assistant/reasoning
message, skipping transient system messages
- Reuse internal/ui/clipboard.CopyToClipboard for OSC 52 + native
clipboard support (works over SSH)
- Document the command in /help
After /compact, BuildContext emitted [summary, post-compact, kept]
which placed an older kept user/assistant turn after the latest
post-compaction turn. This broke user/assistant alternation and caused
the model to respond as if the post-compaction turn never happened on
the next user message.
- Emit kept messages chronologically before post-compaction messages
- Mirror the same order in GetContextEntryIDs so cut-point to entry-ID
mapping stays aligned across repeat compactions
- Update TestCompactionWithNewMessagesAfterCompaction to assert the
correct chronological order
* fix(sdk): stop leaking fantasy types through pkg/kit.AgentConfig (#30)
Replace the alias-based AgentConfig and handler types with SDK-owned
structs and function types. CoreTools / ExtraTools / ToolWrapper now
accept []kit.Tool, and the handler types (ToolCallHandler,
ToolExecutionHandler, ToolResultHandler, ResponseHandler,
StreamingResponseHandler, ToolCallContentHandler) plus SpinnerFunc are
declared in pkg/kit/ with signatures that reference only SDK types.
Consumers no longer need to import charm.land/fantasy to populate an
AgentConfig or assign a handler. go doc pkg/kit AgentConfig output no
longer mentions fantasy.*.
- Add unexported (*AgentConfig).toInternal() to convert at the SDK
boundary; Tool is still an alias for the underlying tool type, so
slice and function fields convert without allocation.
- Add agent_config_internal_test.go covering nil receiver, scalar
fields, tool slices, ToolWrapper invocation, OnMCPServerLoaded, and
auth/token-factory wiring.
- Add types_test.go cases that populate AgentConfig and SpinnerFunc
without importing fantasy -- the file compiling is the regression
proof for the leak.
- Update pkg/kit/README.md Re-exported Types section to record that
AgentConfig and the handler types are now Kit-owned.
Fixes#30
* fix(sdk): add DebugLogger and MCPTaskConfig to kit.AgentConfig (#30)
The first revision of the SDK-owned AgentConfig dropped two fields that
internal/agent.AgentConfig carried: DebugLogger (tools.DebugLogger) and
MCPTaskConfig (tools.MCPTaskConfig). Restore them with SDK-owned
equivalents and wire them through toInternal().
- Add kit.DebugLogger interface (LogDebug / IsDebugEnabled) mirroring
tools.DebugLogger. Interface-to-interface assignment is automatic
because the method sets match.
- Add kit.MCPTaskConfig struct mirroring tools.MCPTaskConfig with SDK
types (MCPTaskMode, MCPTaskProgressHandler) and a toToolsConfig()
helper that converts at the SDK boundary.
- Wire both new fields in (*AgentConfig).toInternal().
- Extend agent_config_internal_test.go with cases for both fields.
- Document the additions in pkg/kit/README.md.
The MCP adapter previously wrapped any error returned by MCPToolManager.ExecuteTool
into a Go error returned from the fantasy.AgentTool.Run interface. The fantasy
agent loop treats those as critical errors and aborts the entire turn —
discarding all prior reasoning, tool calls, and results.
In practice that meant a single misbehaved MCP server returning a JSON-RPC
"-32602 Invalid params" (e.g. a Zod schema mismatch on the server's input
validation) would kill an in-progress turn after the model had already done
dozens of seconds of useful work, with no way for the model to see the
validation message and self-correct.
This mismatched the contract that native Kit tools follow: native tools
return errors via kit.ErrorResult(...), which become soft tool-result errors
that the model reads and can act on (retry with corrected args, try a
different tool, give up gracefully).
Make the MCP path behave the same way:
- JSON-RPC protocol errors, transport failures, and server-side schema
rejections are now returned as fantasy.NewTextErrorResponse(...) with
err == nil, so the agent loop continues and the model sees the failure
in-band as a tool result it can reason about.
- Context cancellation (ctx.Err() != nil) remains a critical error so
callers can abort turns deterministically. This is the only case where
bubbling up is correct — the caller intentionally tore the turn down
and the agent must not keep spinning.
- Server-side soft errors (CallToolResult{ isError: true }) and the
happy path are unchanged.
The agent loop's MaxSteps cap already bounds the worst case for a
permanently broken MCP server, so there is no risk of unbounded retries.
Side effect: extracted a tiny mcpExecutor interface for the one method the
adapter uses (ExecuteTool), purely so the adapter is unit-testable in
isolation without standing up a full MCPToolManager + connection pool.
Behavior change note for downstream consumers: code that relied on
host.PromptResult / Stream returning a Go error containing
"mcp tool execution failed" will no longer see those errors — the
failure information is now in the assistant's final response (or in the
OnAfterToolResult / OnToolResult hooks, where IsError will be true).
Context cancellation continues to surface as an error from those calls
as before.
Co-authored-by: space_cowboy <space_cowboy@mark3labs.com>
- 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
- Add unexported steerDrainFn test seam on App so unit tests can
inject fake steer items without standing up a full *kit.Kit
(Options.Kit is a concrete struct, not an interface).
- releaseBusyAfterCompact now prefers the seam over Kit.DrainSteer
via a small switch; production behaviour is unchanged when the
field is nil.
- Add TestReleaseBusyAfterCompact_splicesSteerAheadOfQueue, which
pre-populates both fake steer items and ordinary queue prompts,
invokes releaseBusyAfterCompact, and asserts the first dispatched
prompt is the steer item — proving steer messages retain 'act now'
priority and that drainQueue is actually launched (the bug from
#27).
- Add releaseBusyAfterCompact() shared deferred tail used by both
CompactConversation and CompactAsync. It drains the SDK steer
channel, splices steer items in front of any queued prompts, and
hands off to drainQueue so messages received during compaction
are dispatched automatically once compaction finishes.
- Previously, busy was simply cleared on completion and the queue
sat idle until the user submitted another prompt, which then
flushed everything together.
- Honor the closed flag so a teardown during compaction discards
pending items instead of spawning drainQueue against a torn-down
App.
- Add regression tests covering the queued-flush, idle-empty, and
closed-during-compact paths.
Fixes#27
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
Addresses two CodeRabbit feedback items on PR #24:
* Docstring coverage warning (was 57.14%, threshold 80%): adds godoc
comments to the four test functions added or substantially rewritten
in this PR — TestLoadAndSaveManifest, TestAddAndRemoveFromManifest,
TestFindInManifest, TestHighlightFileTokensInjectsANSI.
* Quick-win nitpick: replaces the manual os.Setenv/os.Unsetenv +
defer pattern in TestFindInManifest with t.Setenv, which restores
the env var automatically on cleanup even on panic or t.Fatal.
go test -race ./... still passes.
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 TestUserBlockHighlightsFileTokens test was rewritten to call
HighlightFileTokens directly (UserBlock was deleted in the dead-code
sweep). That left testTypography with no callers, so staticcheck U1000
flagged it.
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.
- Stale comment showed ~/.kit/sessions/--<cwd-path>--/ which does not
match the actual encoding (no leading/trailing dashes)
- Update to reflect the real format and point to encodeCwdForDir for
full rules
- Encode cwd via new encodeCwdForDir helper that handles both `/`
and `\` separators and strips characters illegal in Windows
directory names (`: < > " | ? *`)
- Fixes session creation on Windows where the drive-letter colon
produced names like `C:--test` and caused mkdir to fail
- Add regression tests covering Unix paths, Windows drive roots,
secondary drives, mixed separators, and other illegal chars
Fixes#18
Address two review findings on the MCP Tasks PR.
- Config.Validate() now rejects unknown tasksMode values with a clear
error naming the server and bad value. Without this a typo (e.g.
"alwasy") was silently downgraded to "auto" by the runtime parser.
- Kit.Subagent() now propagates the parent's six MCP task options
(mode map, timeout, TTL, poll interval, max poll interval, progress
callback) onto the child via a new inheritMCPTaskOptions helper.
Without this, child subagents always saw default polling and no
progress feedback regardless of parent configuration.
The propagation logic lives in a helper so the test exercises the real
code path instead of duplicating it; future task fields only need to be
added in one place.
- README: add tasksMode YAML example and MCP Tasks subsection with
SDK opt-in snippet
- pkg/kit/README: add MCP Tasks subsection covering MCPTaskMode,
progress callbacks, and List/Get/Cancel methods
- www/configuration: document the tasksMode server field plus a
per-mode behaviour table
- www/sdk/options: extend the Compaction & MCP table with the six
new Options fields and add a top-level MCP Tasks section
- www/sdk/overview: add a brief MCP Tasks section between MCP
prompts/resources and Context & compaction
All examples verified against the public symbols in pkg/kit/mcp_tasks.go;
docs site builds cleanly via npx tome build.
Implement Phase 1 of the MCP Tasks spec so long-running tools/call
requests can run asynchronously, survive proxy timeouts, and be
cancelled mid-flight.
- connection pool now advertises mcp.NewTasksCapability() during
initialize and captures the InitializeResult so callers can detect
per-server task support
- new MCPServerConfig.TasksMode (auto|never|always, default auto)
parsed from both new and legacy mcp.json shapes
- ExecuteTool augments tools/call with TaskParams when policy and
capability allow, polls tasks/get / tasks/result until terminal,
and best-effort tasks/cancel on context cancellation
- new MCPToolManager methods: SetTaskConfig, ListServerTasks,
GetServerTask, CancelServerTask
- public SDK surface in pkg/kit: MCPTask, MCPTaskStatus, MCPTaskMode,
MCPTaskProgress, MCPTaskProgressHandler, plus Options fields
(MCPTaskMode, MCPTaskTimeout, MCPTaskTTL, MCPTaskPollInterval,
MCPTaskMaxPollInterval, MCPTaskProgress) and Kit.{List,Get,Cancel}
MCPTask methods
- works around two upstream mcp-go v0.51.0 parser bugs
(ParseCallToolResult rejects task responses; ParseTaskResultResult
looks for content under a non-existent nested key) by decoding the
wire shape directly via the transport
- defaults to MCPTaskModeAuto so servers that don't advertise task
support behave exactly as before
Fixes#21
- Bump fantasy v0.21.0 -> v0.23.0, mcp-go v0.49.0 -> v0.51.0,
acp-go-sdk v0.12.0 -> v0.12.2, chroma v2.23.1 -> v2.24.1,
fsnotify v1.9.0 -> v1.10.1, ultraviolet, AWS SDK, Google API
- Implement CloseSession and ResumeSession on acpserver.Agent to
satisfy the expanded acp.Agent interface in acp-go-sdk v0.12.2
- Add sessionRegistry.remove helper to support session close
Fantasy v0.21.0 natively includes gpt-5.5 and other newer models in
its responsesModelIDs/responsesReasoningModelIDs lists, making our
workaround unnecessary.
- Delete responses_models.go (go:linkname hack + RegisterResponsesModels)
- Delete responses_models_test.go
- Replace isResponsesAPIModel/isResponsesReasoningModel heuristics with
direct openai.IsResponsesModel/openai.IsResponsesReasoningModel calls
- Remove RegisterResponsesModels calls from registry init/reload
- Remove hack documentation from AGENTS.md
- Update all deps (fantasy v0.21.0, smithy-go, ultraviolet, etc.)
Fantasy's hardcoded responsesModelIDs list gates whether a model uses
the Responses API or Chat Completions code path. When a new model
(e.g. gpt-5.5) is added via `kit update-models` but fantasy hasn't
been updated yet, the type mismatch between *ResponsesProviderOptions
and *ProviderOptions causes a crash.
- Add isResponsesAPIModel()/isResponsesReasoningModel() helpers that
supplement fantasy's checks with prefix-based heuristics for modern
OpenAI model families (gpt-4.1+, gpt-5+, o-series, codex, chatgpt)
- Add RegisterResponsesModels() using go:linkname to append missing
model IDs from our database into fantasy's internal slices at init
time and after ReloadGlobalRegistry()
- Replace all direct openai.IsResponsesModel/IsResponsesReasoningModel
calls in providers.go with the new helpers
- Merge embedded + cached model databases instead of cache-only fallback
- Bump fantasy v0.19.0 -> v0.20.0 to match existing import usage
- Document the technique and model-family update process in AGENTS.md
- Remove top-level old_text/new_text params from edit tool schema
- Make edits array the sole interface; single edits pass 1-item array
- Simplify normalizeEditInput, removing dual-mode branching logic
- Update UI renderer to only read from edits array
- Remove old_text/new_text from bodyKeys in message summarizer
- Update web session HTML to iterate edits array
- Convert all single-edit tests to use Edits array
- Replace mixed-mode test with empty-array validation test
Tool blocking via OnToolCall and SetActiveTools returned both a
ToolResponse (IsError=true) and a Go error. Fantasy treats a non-nil
Go error from tool.Run() as a critical failure, aborting the agent
loop without delivering the tool result to the LLM. The model never
saw the block reason and would retry or hallucinate.
- Return nil error for blocked tools (OnToolCall Block=true)
- Return nil error for disabled tools (SetActiveTools)
- Return nil error for extension tool execution failures
- Update tests to assert nil error (IsError response conveys the error)
Fixes#20
- Set context tokens per-step in recordStepUsage instead of waiting
for turn completion; each step re-sends the full conversation so
the reported usage monotonically increases
- Add UsageUpdatedEvent to trigger a TUI re-render after each step
so the status bar reflects updated tokens, cost, and context %
even during gaps between streaming chunks
- Update test to expect per-step context token updates
- Add heightCache map to ScrollList, keyed by item ID, avoiding
repeated Render() calls purely to count lines
- Rewrite GotoBottom() to walk backwards from the end in O(visible)
instead of two full O(N) forward passes over all items
- Replace all height-only Render() calls in clampOffset(), AtBottom(),
ScrollBy(), and ScrollPercent() with cached itemHeight() lookups
- Invalidate cache on width changes (SetWidth) and item mutations
(AppendChunk, AppendStdout/Stderr via InvalidateItemHeight)
- Refresh cache entries in View() from authoritative renders
- Add LLMToolResultOutputContentMedia alias (closes gap in tool result types)
- Add LLMToolResultContentType enum and constants (Text, Error, Media)
- Add LLMToolInfo, LLMProviderOptions, LLMProviderMetadata, LLMPrompt aliases
- Replace all fantasy.* references in hooks.go and hooks_test.go with
SDK-owned aliases, removing the charm.land/fantasy import from both
- Fix gofmt alignment in internal/extensions/symbols.go
- Update SDK skill doc with complete LLM type reference
- Infer Type="image" for image/* MIME types and Type="media" for all
other binary content so the downstream framework creates a media
content block instead of silently discarding Data bytes (#17)
- Extract shared toolOutputToResponse() helper to eliminate duplication
- Add ImageResult() and MediaResult() convenience constructors
- Add LLMToolCall and LLMToolResponse type aliases so SDK consumers
can call Tool.Run() without importing the underlying framework
- Add 6 regression tests covering image, media, and text responses
Closes#17
- Buffer session JSONL writes with bufio.Writer, flush at sync points;
ForkToNewSession and AddLLMMessages now batch N entries into ~1 syscall
- Cache lipgloss styles in style.CachedStyles, lazily built and
invalidated on SetTheme; eliminates ~15 NewStyle() calls per frame in
hot render paths (reasoning blocks, spinner, tool headers, margins)
- Cache git ls-files results for @file suggestions with 3s TTL; typing
@filename no longer spawns 3 subprocesses per keystroke
- Use strings.Builder for StreamingMessageItem.content; eliminates O(n²)
string copying during LLM response streaming
- Set Streaming: true in subagent childOpts to prevent
viper.Set("stream", false) from polluting global state
- Without this, concurrent subagents and the parent could read
stale stream=false from viper, causing provider-level issues
(e.g. Anthropic non-streaming timeouts with extended thinking)
- Update all Go dependencies (bubbletea v2.0.6, fantasy v0.19.0,
acp-go-sdk v0.12.0, mcp-go v0.49.0, and transitive deps)
- Replace SetSessionModel with SetSessionConfigOption to match new
acp-go-sdk Agent interface (union type with ValueId/Boolean variants)
- Add ListSessions stub returning empty list (new required method)
- Refresh embedded_models.json from models.dev/api.json
- Update ACP smoke test: add initialize handshake, session/list,
session/set_config_option, session/cancel, and fix update parsing
- remove "You" label and icon from user messages, use borderless content block
- remove input title bar ("Enter your prompt...") and hint line
- increase textarea from 3 to 4 rows with top/bottom margin
- hide input hints permanently for a cleaner UI
- match separator colors (use theme.Border for both startup and input dividers)
- make startup separator full terminal width instead of hardcoded 80
- add /help for help hint and pipe separators to status bar
- add printCustomMessage/RenderCustomMessage for custom alert labels
- render /help output as markdown with "Help" alert label
- add Ctrl+V (paste image) to help message keys section
- fix reasoning text wrapping using ANSI-aware lipgloss.Style.Width
- export HighlightFileTokens for cross-package use
- Add NoOAuth field to MCPServerConfig with JSON/YAML support
- Guard OAuth error handling and transport setup with the new flag
- Prevents failed dynamic client registration on servers like PubMed
that do not support OAuth
- First ctrl+c clears input and arms quit flag with 3s timeout
- Second ctrl+c within timeout window actually quits
- Show '⚠ Press Ctrl+C again to quit' warning after first press
- Empty input no longer quits immediately on single ctrl+c
- Prompt/overlay states: ctrl+c cancels dialog, re-dispatches to
main handler for double-press tracking instead of quitting
- Update placeholder, help text, and tests to match new behavior
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
- wrap thinking text in StreamComponent and render.ReasoningBlock
- plumb width through renderer and streaming item paths
- keeps style consistent with user/assistant blocks and avoids cut-off lines
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
Change Ctrl+C behavior to match other terminal AI tools (claude, codex, pi):
- First Ctrl+C clears the current input when text is present
- Second Ctrl+C (within 3 seconds) quits the application
- Ctrl+C on empty input quits immediately
- 3-second auto-reset timer clears the 'pressed once' state
- Flag also resets after message submission
Updates placeholder text and help message to reflect new behavior.
Fixes#13
Add defensive validation to detect and prevent cycles in the session tree
parent chain that could occur after compaction or file corruption.
- Add tree_validation.go with cycle detection and parent chain validation
- Validate parent chain before appending messages (AppendMessage)
- Validate firstKeptEntryID exists in AppendCompaction
- Add depth limit and cycle detection to buildTreeNode to prevent infinite recursion
- Log diagnostics on session open to detect existing cycles
- Add tests for cycle detection and graceful handling
Reflect the refactor that made MCPAuthHandler an explicit, opt-in
dependency for remote MCP OAuth. Four surfaces updated:
- README.md: new 'MCP OAuth (remote MCP servers)' subsection under the
Go SDK section, outlining the three consumer patterns (nil / CLI /
custom) and linking to the full options docs.
- pkg/kit/README.md: type cheat-sheet now lists MCPAuthHandler,
DefaultMCPAuthHandler, and CLIMCPAuthHandler alongside the existing
MCPTokenStore entries.
- skills/kit-sdk/SKILL.md: Options example annotated with nil-disables-
OAuth semantics; new 'MCP OAuth Authorization' section precedes the
existing token-storage section; re-exported types list expanded.
- www/pages/sdk/options.md: Options fields table gains MCPAuthHandler
row; new top-level 'MCP OAuth Authorization' section with consumer
matrix, CLI/custom/fully-custom code samples, and a warning callout
about the OnAuthURL nil-hang footgun.
Strip user-facing I/O out of the SDK's OAuth surface so library, daemon,
and web-app embedders are not surprised by port binds or browser opens.
- DefaultMCPAuthHandler no longer calls openBrowser; it exposes an
OnAuthURL(serverName, authURL) hook and performs no presentation I/O.
- kit.New no longer auto-constructs a default handler when
Options.MCPAuthHandler is nil. OAuth is opt-in; remote MCP servers
requiring authorization fail with a clear error if no handler is set.
- CLIMCPAuthHandler owns the CLI policy (browser open + stderr prints)
by wiring an OnAuthURL closure on the inner DefaultMCPAuthHandler.
- openBrowser is now unexported and colocated with its sole caller; no
new exported helper is added to the SDK surface.
BREAKING CHANGE: SDK consumers relying on implicit OAuth with a nil
MCPAuthHandler must now pass kit.NewCLIMCPAuthHandler() (or a custom
implementation) explicitly. The kit CLI is unaffected — cmd/root.go
already constructs the handler explicitly.
TestDetectMediaType/.go fails on CI images (Ubuntu mime-support) where
/etc/mime.types registers '.go → text/x-go', because mime.TypeByExtension
reads those files at init. The test intended to exercise the 'unknown
extension falls through to text/plain' branch but used a real extension,
making the assertion environment-dependent.
Replace '.go' with '.kitsyntheticext', an invented extension that no
system MIME database registers. The fallback path is now exercised
deterministically on any host.
The SDK applies Options by calling viper.Set on viper's process-global
store, which means two Kits constructed in the same process are not
isolated from each other: the second New overwrites the first's keys,
and downstream readers (SetModel, GetThinkingLevel, BuildProviderConfig)
observe the most recent value.
- Add a 'Global viper state warning' block to the Options godoc
explaining the leak, the zero-value-does-not-clear gotcha, and
pointing at viper.Reset() as the migration workaround.
- Add a matching warning to the New godoc so consumers discover the
constraint from either entry point.
- Detach the viperInitMu godoc (previously lodged inside New's comment
block) and clarify that the mutex only guards the construction
window, not instance isolation.
- Add a TODO noting the proper fix: refactor to a per-call viper.New()
instance so each Kit owns its own config store.
- InitConfig now installs a viper env key replacer so keys like
"max-tokens" bind to KIT_MAX_TOKENS under AutomaticEnv; previously
hyphenated keys silently missed their documented env overrides.
- Simplify TestNewPreservesIsSetSemantics: with SkipConfig: true no env
bindings are registered, so the os.Getenv guard and upper() helper
were dead weight. Remove both and drop the unused helper.
The SDK last-resort MaxTokens floor is applied in kit.New() when
Options.MaxTokens, KIT_MAX_TOKENS, .kit.yml, and per-model defaults
are all unset. It was 4096 (inherited from the old setSDKDefaults
viper default) while the CLI --max-tokens cobra default is 8192.
Bump the floor to 8192 so SDK and CLI callers start from the same
base value before rightSizeMaxTokens runs, then update README,
skills/kit-sdk/SKILL.md, and www/pages/{configuration,sdk/options}.md
to match.
Previously setSDKDefaults() registered viper.SetDefault for max-tokens,
temperature, top-p, top-k, frequency/presence-penalty, and thinking-level.
viper.SetDefault makes IsSet() return true, which silently suppressed
per-model defaults (ApplyModelSettings) and automatic right-sizing
(rightSizeMaxTokens) for every SDK-created Kit — and for CLI runs too,
since cmd/root.go routes through kit.New. Effective max-tokens for
claude-sonnet-4-5 was pinned at 4096 instead of 32768.
- Drop SetDefault for all IsSet-sensitive keys; keep only model,
system-prompt, stream, num-gpu-layers, main-gpu.
- Apply a 4096 max-tokens floor directly on the *models.ProviderConfig
struct in kit.New() when nothing else resolved a value. Keeps
viper.IsSet("max-tokens") == false so rightSizeMaxTokens and
per-model maxTokens overrides still fire.
- Update Options.MaxTokens / ThinkingLevel godoc to describe the real
precedence chain.
- Strengthen tests: add Temperature subtest; add
TestNewPreservesIsSetSemantics regression covering all seven keys;
split TestNewWithProviderOptions into three subtests including
Options-beats-viper-state and ProviderURL propagation; add
resetViper helper so subtests don't bleed state.
- Document the new SDK fields (MaxTokens, ThinkingLevel, Temperature,
TopP, TopK, FrequencyPenalty, PresencePenalty, ProviderAPIKey,
ProviderURL, TLSSkipVerify) in README, skills/kit-sdk, and the www
configuration / sdk/options / sdk/overview pages, including a
dedicated precedence table.
Adds programmatic overrides on kit.Options for the model/provider knobs
that were previously only reachable through viper.Set() — letting SDK
consumers (web apps, services, embedded agents) configure kit fully
in-code without polluting global viper state or shipping .kit.yml.
Generation parameters:
- MaxTokens int (max output tokens per response)
- ThinkingLevel string (off/low/medium/high)
- Temperature *float32
- TopP *float32
- TopK *int32
- FrequencyPenalty *float32
- PresencePenalty *float32
Sampling params use pointer types so explicit 0 is distinguishable from
unset; nil leaves provider/per-model defaults in place.
Provider configuration:
- ProviderAPIKey string
- ProviderURL string
- TLSSkipVerify bool
Implementation just pushes Options values into viper inside New(),
so all existing downstream code (BuildProviderConfig, SetModel,
modelSettings lookups, runtime model switching) picks them up
uniformly without any new code paths. Tests added for MaxTokens,
ThinkingLevel, and ProviderAPIKey.
- 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.
- UpdateTheme() only refreshed typography styles, leaving spinner
frames rendered with the old theme's colors
- Now calls knightRiderFrames() to rebuild frames with the new
theme's Primary, Muted, VeryMuted, and MutedBorder colors
- Replace hardcoded nameWidth of 15 with dynamic calculation based on
the longest command name in the filtered list
- Prevents truncation of longer names like /feature-request and
/release-tagger that were cut off with ellipsis
- Cap name column to leave at least 20 chars for descriptions
- Add 1 char gap between name and description columns
- Move thinking toggle from ctrl+t to leader chord (ctrl+x t) to avoid
conflicts with tmux/zellij tab mode and terminal new-tab shortcuts
- Change scrollback jump from alt+home/alt+end to ctrl+home/ctrl+end
for better compatibility across SSH and older tmux versions
- Remove ctrl+d as submit alias (enter suffices); avoids EOF convention
confusion and accidental shell disconnects
- Remove ctrl+a from tree selector filter shortcuts to avoid conflict
with the common tmux prefix remap (ctrl+o cycle still reaches all
filter modes)
Add core TUI support for handling sudo password prompts when executing
bash commands that require elevated privileges.
- Detect sudo commands and check if credentials are cached (sudo -n)
- Show modal password prompt with masked input (• characters) when needed
- Pipe password via stdin using sudo -S -p '' (no password in command string)
- Password flows through context callbacks, never stored in session history
- Add PasswordPromptHandler to agent and SDK event system
- Add password prompt overlay to TUI with 🔐 icon and hidden input
- Include tests for sudo command detection and rewriting
The password is never persisted to disk - it only exists in memory
during execution and is piped directly to sudo via stdin.
- Add InProcessServer field to MCPServerConfig (json:"-", never serialized)
- Add "inprocess" transport type to config, validation, and connection pool
- Add createInProcessClient() using mcp-go client.NewInProcessClient()
- Add Kit.AddInProcessMCPServer() convenience method
- Add Options.InProcessMCPServers for init-time registration
- Export MCPServer type alias (= server.MCPServer) in pkg/kit/types.go
- Add 8 tests covering config, pool, tool manager, and edge cases
- Update SDK README, kit-sdk skill, and www docs
- 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
- Rename ExtensionToolsAsFantasy -> ExtensionToolsAsLLMTools
- Rename convertKitMessagesToFantasy -> convertToLLMMessages
- Delete GetFantasyProviders, ToFantasyMessages, FromFantasyMessage
- Replace direct fantasy type usage with kit.LLM* aliases in app tests
- Scrub fantasy references from godoc comments across pkg/kit and internal
- Replace fantasy.AgentTool with plain MCPTool struct in MCPToolManager
- Move fantasy adapter from internal/tools to internal/agent as mcpAgentTool
- Add MCPToolManager.ExecuteTool() for framework-agnostic tool execution
- Remove dead fantasy.LanguageModel field from MCPConnectionPool
- Remove MCPToolManager.SetModel() (was only feeding the dead field)
internal/tools is now a pure MCP client library with no LLM framework
dependency. The fantasy-to-MCP bridging is confined to the agent layer
where it belongs.
- Add internal/fences package for detecting markdown code regions
(fenced blocks and inline code spans) with ReplaceOutside/StripCode
- SubstituteArgs, HasArgPlaceholders, RequiredArgs now skip $
placeholders inside ``` fences and `inline` code spans
- ProcessFileAttachments skips @file tokens inside code regions
- Add $+ placeholder: expands like $@ but requires at least 1 argument
- Add RequiredArgs() method; expandPromptTemplate validates arg count
and re-populates input on failure instead of submitting
- Update feature-request, file-issue, new-prompt to use $+
- Add HasArgPlaceholders() method to PromptTemplate to detect , $@,
$ARGUMENTS, etc. placeholders in template content
- Add HasArgs field to SlashCommand struct
- Set HasArgs when registering prompt templates as slash commands
- In fuzzy finder Enter handler, populate input with command + trailing
space when HasArgs is true, letting the user type arguments naturally
- Fix potential index bug by capturing selectedCmd before resetting index
- Add ctrl+x e leader key chord to open $VISUAL/$EDITOR in a temp file
pre-populated with the current input text
- On save & quit, replace the input textarea with the edited content
- On error exit (e.g. :cq in vim), silently preserve original input
- Use charmbracelet/x/editor for editor command construction
- Use tea.ExecProcess to suspend/resume the TUI around the editor
- Update input hint text at all width breakpoints to show the shortcut
- Add ctrl+x e to /help output
Closes#5
- Add highlightFileTokens() to style @file references with theme.Accent + bold
- Apply highlighting in UserBlock() after line wrapping, before herald rendering
- Support both unquoted (@path/to/file) and quoted (@"path with spaces") tokens
- Add tests for highlightFileTokens and UserBlock integration
Closes#6
- Add editor interceptor via OnSessionStart so !{...} expansions
appear in the user message block on screen
- Restrict OnInput handler to non-interactive sources (CLI, script,
queue) to avoid double expansion
- Extract expand() helper and hoist regex to package level
- Update doc comments and examples
- Convert 31 extension example entries from plain code spans to
Markdown links pointing to their source files
- Link the go-edit-lint.go and tool-logger_test.go references
- Store authHandler and tokenStoreFactory on Agent struct so
AddMCPServer() can propagate them to new MCPToolManagers (#3)
- Add OAuthClientID, OAuthClientSecret, OAuthScopes fields to
MCPServerConfig for servers without dynamic registration (#4)
- Pass OAuth fields from server config to transport OAuthConfig
in both SSE and Streamable HTTP client creation paths
- Add GetAuthHandler() accessor to MCPToolManager
- Add tests for auth handler propagation and OAuth config parsing
Closes#3, closes#4
- Include all token categories in context fill calculation:
InputTokens + CacheReadTokens + CacheCreationTokens + OutputTokens
- With Anthropic/kimi prompt caching, InputTokens can be near-zero
while CacheReadTokens holds the bulk of the context
- Include OutputTokens since assistant output becomes context next turn
- Remove max-only guard in SetContextTokens so context shrinks after
compaction instead of staying stuck at the high-water mark
- Reset context tokens to 0 after compaction in both SDK and UI layers
- Use real API-reported token counts in ShouldCompact() instead of
the chars/4 text heuristic which misses system prompts and tool defs
- Add NoExtensions bool to Options, OR with viper fallback
- Add NoSkills bool to Options, guards all skill loading
- Add NoContextFiles bool to Options, skips AGENTS.md discovery
- SDK consumers can disable autoloading without touching viper
- Add AddServer/RemoveServer to MCPToolManager for runtime server management
- Add RemoveConnection to MCPConnectionPool for per-server teardown
- Add AddMCPServer/RemoveMCPServer/ListMCPServers to Agent and SDK Kit
- Lazily create connection pool so AddServer works without prior LoadTools
- Wire onToolsChanged callback to trigger agent tool list rebuild
- Make MCPToolManager.Close nil-safe when pool was never initialized
Tests:
- Integration tests with real stdio MCP server (Python echo server)
- Agent-level tests using mock LLM model (no API key needed)
- Unit tests for error paths, callbacks, idempotency, nil safety
- SDK type surface tests
- Add MCPTokenStoreFactory option to kit.Options allowing SDK consumers
to provide custom token storage backends for remote MCP servers
- Thread TokenStoreFactory through the full chain: kit.Options →
kitsetup → agent → MCPToolManager → MCPConnectionPool
- Add createTokenStore() helper on connection pool that delegates to the
factory or falls back to the default FileTokenStore
- Export MCPTokenStore, MCPToken, MCPTokenStoreFactory, and ErrMCPNoToken
in pkg/kit/types.go following SDK naming conventions
- Default behavior (file-based storage) is preserved when factory is nil
- event_handler: route default extension print level through DisplayInfo
instead of bare fmt.Println for consistent styling and timestamps
- factory: remove orphan fmt.Println("") before system messages; the
renderer already manages its own spacing
- app: PrintFromExtension non-interactive fallback now respects level,
writing errors/info to stderr with prefix to keep stdout clean
- app: PrintBlockFromExtension non-interactive fallback writes framed
blocks to stderr instead of raw text to stdout
- Add systemPrompt field to GenerationParams and config structs
- On init, replace default system prompt with per-model prompt when
user hasn't explicitly set one (via flag, config, or SDK option)
- On model switch, detect per-model prompt and compose it with
AGENTS.md, skills, and date/cwd context
- Fix viper.IsSet bug: BindPFlag causes IsSet to return true for
unset flags, so compare against defaultSystemPrompt instead
- Agent.SetModel now updates stored system prompt from config
- Export LoadModelSettingsFromConfig, LoadSystemPromptValue, and
LookupModelForSettings for use by Kit.SetModel
- Add tests for prompt apply, precedence, file path, and
modelSettings override
- Add modelSettings config section for attaching generation params
(temperature, topP, topK, frequencyPenalty, presencePenalty,
maxTokens, stopSequences, thinkingLevel) to any model by
provider/model key
- Add params field to customModels definitions for inline defaults
- Change BuildProviderConfig and SetModel to use viper.IsSet so
unset params remain nil, allowing model-level defaults to apply
- Wire ApplyModelSettings into CreateProvider with priority order:
CLI flags > global config > modelSettings > customModels params
- Add GenerationParams to ModelInfo in the registry
- Update default config template with modelSettings and customModels
params examples
Change compaction behavior so the compaction entry has no parent (empty
ParentID), creating a new root for post-compaction history. This ensures
old compacted messages are not traversed when building LLM context.
- Modify AppendCompaction to create entries with empty ParentID
- Update BuildContext to collect kept messages via FirstKeptEntryID
- Update GetContextEntryIDs with same logic
- Add comprehensive tests for compaction behavior
- Add web viewer support for displaying compaction entries
Remove dead code now that pkg/kit transparently handles <thinking> and
tags at the agent layer. The ACP server no longer needs to:
- Track inThinkingTag state across chunks
- Parse and split reasoning/text from MessageUpdateEvent chunks
- Maintain tag format constants
MessageUpdateEvent now contains clean text, and ReasoningDeltaEvent
contains structured reasoning - no duplicate filtering needed.
- Add width parameter to UserBlock and apply lipgloss.Wrap() before
passing content to herald Tip alert
- Subtract 4 from width to account for alert bar prefix and margin
- Pass renderer width from RenderUserMessage to UserBlock
- Mirrors the assistant message wrapping added in e33564c
- Replace single Ctrl+S with Ctrl+X leader prefix followed by "s"
- Add leaderKeyActive flag to AppModel for two-key chord state
- Ctrl+X sets the leader flag; next keypress completes or cancels chord
- Update hint text in input component (adjust width thresholds)
- Update /help command output to reflect new keybind
- Add StepMessagesHandler callback to agent's GenerateWithLoopAndStreaming
so callers can persist messages as each step completes
- Wire onStepMessages in Kit.generate() to call session.AppendMessage
for each step's messages immediately on completion
- Track PersistedMessageCount on GenerateWithLoopResult so runTurn
skips already-persisted messages in post-generation cleanup
- Tool calls are always persisted as assistant+tool pairs (never orphaned)
- Document concurrency and incremental persistence requirements on
the SessionManager interface for custom implementations
- ToMarkdown() received a width param but never used it
- Apply lipgloss.Wrap() after herald-md render to break long lines
- Preserves ANSI styles/colors through the wrapping pass
- Fixes overflow for all markdown paths: assistant messages, tool
bodies, and overlay text
- Limit each queued/steering block to 3 visible content lines with ellipsis
- Account for soft-wrapping when counting visual lines
- Truncation is visual only; full text is preserved for scrollback
- Add truncateMessageForBlock helper with wrap-aware line counting
- Add 7 unit tests covering short, exact, overflow, wrapping, and mixed cases
Switch from standard log.Printf to charmbracelet/log for extension loading
messages. This ensures DEBUG output only appears when explicitly enabled.
- Remove unconditional WARN log for failed extension loads
- Convert DEBUG loaded extension message to structured log.Debug call
- Add ToolOutput struct, TextResult/ErrorResult helpers, and
ToolCallIDFromContext so SDK consumers can create custom tools
without importing charm.land/fantasy
- Add NewTool (sequential) and NewParallelTool (concurrent) generic
constructors with automatic JSON schema generation from struct tags
- Remove dead UpdateUsageFromResponse method and fantasy import from
internal/ui/cli.go
- Update SDK skill, README, and www/ docs with custom tool examples
and corrected hook signatures
- 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
Add SessionManager interface to allow pluggable session storage backends.
This enables users to implement custom session managers for databases,
cloud storage, or other persistence mechanisms instead of the default
JSONL file-based TreeManager.
Changes:
- Add SessionManager interface with methods for message storage,
tree navigation, compaction, and extension data
- Add treeManagerAdapter to wrap existing TreeManager for backward compatibility
- Update Kit struct to use SessionManager interface instead of concrete type
- Add SessionManager option to Options struct
- Update all session-related methods to use interface
- Add documentation for custom SessionManager usage
The default behavior is preserved - when no SessionManager is provided,
Kit automatically uses the TreeManager via the adapter.
Add two new Options fields for programmatic SDK usage:
- SkipConfig: Skip .kit.yml file loading while still using viper defaults
and environment variables. Useful for fully programmatic configuration.
- DisableCoreTools: Allow creating agents with 0 tools (chat-only mode) or
with only custom tools. When true and Tools is empty, no tools are loaded.
When combined with custom Tools, only those tools are loaded.
Updates documentation in README, pkg/kit/README, skills/kit-sdk/SKILL,
and www/pages/sdk/options.
Remove charmbracelet/log debug statements from the file watcher that
were writing directly to stderr, corrupting the Bubble Tea terminal UI.
- Remove log.Debug calls for directory operations and file changes
- Remove log.Warn for watcher errors (silently ignore instead)
- Remove the charmbracelet/log import entirely
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
- Detect new subdirectory creation in the fsnotify event loop and add
it to the watcher so files created inside trigger reload events
- Handle cp -r case by checking if new directories already contain
matching files and scheduling an immediate debounced reload
- Add dirContainsMatchingFiles helper method
- Add tests for both new-subdirectory and copy-with-existing-files cases
- 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
- Always fire onResponse callback even when response text is empty so
ResponseCompleteEvent reaches the TUI and resets the StreamComponent
- Check for existing StreamingMessageItem in flushStreamAndPendingUserMessages
before creating a new StyledMessageItem to avoid duplicate content
- Mark trailing StreamingMessageItem complete on StepComplete, StepCancelled,
and StepError to freeze live timers and prevent dangling streaming state
Load MCP server tools in the background so the UI appears immediately
instead of blocking until all servers connect. The first LLM call
automatically waits for tools to be ready before proceeding.
Key changes:
- NewAgent() starts MCP loading in a background goroutine and returns
immediately with core/extension tools only
- GenerateWithLoop() calls ensureMCPTools() to lazily wait and rebuild
the fantasy agent with full tool set before first LLM call
- Parallelize LoadTools() across all configured MCP servers
- Add WaitForMCPTools() and MCPToolsReady() for status checking
- Refactor SetModel/SetExtraTools to use shared rebuildFantasyAgent()
- Expose async MCP status methods in public SDK
- 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 SystemPromptEntry type to capture system prompt, model, and provider
when sharing sessions via /share command. The entry is inserted into the
JSONL after the header and displayed in the web viewer as a collapsible
section with a model badge.
- Add SystemPromptEntry with Content, Model, and Provider fields
- Capture current system prompt and model at share time
- Display in web viewer with collapsible UI and model badge
- Update documentation for /share command
- 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
- InstallWithInclude wrote manifest twice via two different code paths,
with the first write missing Include; unify into shared install() method
that writes the manifest once with all fields including Include
- Update() now reads the existing manifest entry to preserve Include and
Installed timestamp instead of constructing a fresh entry from scratch
- Filter out _test.go files in findExtensionsInDir, findExtensionsInRepo,
and ScanForExtensions to prevent Yaegi from loading test files
- Narrow examples/ traversal so only recognized extension directories
(extensions/, ext/, *-ext/, *-extensions/) are scanned, not arbitrary
subdirs like examples/sdk/ that import pkg/kit
- Send sendChatAction("typing") every 4s while agent is processing,
started on AgentStart and stopped on AgentEnd/SessionShutdown
- configPath() now checks project-local .kit/ first, then falls back
to ~/.config/kit/kit-telegram.json for cross-project portability
- 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
Move reasoning tag detection from the provider and UI layers into the agent layer. This prevents raw XML tags from leaking into text streams while ensuring structured reasoning events are emitted correctly for all callers.
- 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 full-width bordered container with rounded border and primary color
- Add max height constraint to prevent terminal overflow
- Improve selection highlighting with inverted colors matching PopupList style
- Change cursor indicator from › to > for consistency
- Add separator lines between header, content, and footer
- Add footer showing current filter mode
Steering messages (Ctrl+S during agent work) now carry file attachments
just like queued messages do. Previously, pasted images were silently
dropped when steering.
Changes:
- Add SteerMessage struct with Text and Files fields
- Update steer channel from chan string to chan SteerMessage
- Add SteerWithFiles methods through the stack (UI, app, SDK)
- Update PrepareStep to include files in injected user messages
- Add full-width bordered container with rounded border and primary color
- Add max height constraint to prevent terminal overflow
- Improve selection highlighting with inverted colors matching PopupList style
- Change cursor indicator from › to > for consistency
- Use MutedBorder for tree lines and Success color for active marker
- Update search display format to match PopupList (
Herald's codeBlockWithLineNumbers() hardcodes PaddingTop(1) and
PaddingBottom(1), adding invisible blank lines with background color
above and below the code content. These padding lines occupy line
indices in the rendered item but are visually indistinguishable from
empty space, causing mouse click coordinates to map to the wrong
content line (consistently 1 row off in tool output blocks).
Strip the padding lines after CodeBlock rendering since the Compose
separator above and Figure caption below already provide adequate
visual spacing.
Add dedicated renderGrepBody function for the grep tool, replacing the
previous behavior of routing it through renderBashBody. The grep tool now:
- Shows a caption with total match count (e.g., '8 matches' or '1 match')
- Displays truncation info when matches exceed maxLsLines
- Uses consistent Figure component styling with ls, read, find, and bash tools
- Uses 'match/matches' terminology appropriate for grep results
Add dedicated renderFindBody function for the find tool, replacing the
previous behavior of routing it through renderBashBody. The find tool now:
- Shows a caption with total result count (e.g., '12 results')
- Displays truncation info when results exceed maxLsLines
- Uses consistent Figure component styling with ls, read, and bash tools
Apply the same Figure component pattern to the ls tool for consistency
with read and bash tools. The caption now appears below the directory
listing and shows the count of hidden entries when truncated.
Replace inline truncation hints and exit code labels with herald's
Figure component. Captions now appear below content and show:
- read: filename • lines X-Y of Z • offset=N to continue
- bash: N more lines • exit code N
This provides consistent visual grouping and cleaner metadata
display for tool output blocks.
- Replace detachedWithCancel (goroutine-based) with context.WithoutCancel
+ valuesContext; the old goroutine would fire immediately if the parent
was already cancelled/deadline-exceeded, causing 'failed after 0s'
- Kit.Subagent() pre-flight: if the incoming ctx is already done, reset
to context.Background() before applying the subagent timeout
- Both Subagent() error paths now return a non-nil *SubagentResult with
Elapsed set, so the tool response always shows accurate timing
- Narrow viperInitMu scope in Kit.New(): snapshot viper state + call
BuildProviderConfig under the lock, then release before SetupAgent /
MCP loading; parallel subagent spawns no longer serialise on viper I/O
- AgentSetupOptions gains ProviderConfig + scalar fields so SetupAgent
can skip viper reads when a pre-built config is supplied
- Add subagent_test.go covering the fixed context detachment behaviour
- 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
- Break scanner bar into individual LED segments with single-space gaps
- Center KIT text over the scanner bar (13-space indent for all lines)
- Maintain original 46-char total width for the scanner bar
- Add ValidateModelString() to ModelsRegistry for format, provider,
and model name validation with typo suggestions
- Validate model in Kit.Subagent() before expensive Kit.New() setup
- Remove silent fallback to parent model on creation failure
- Error propagates as tool result so calling agent can self-correct
- Add registry_test.go covering format, provider, and suggestion cases
- Add PopupList: generic themed popup with fuzzy search, scrolling,
keyboard navigation, and centered overlay rendering
- Refactor ModelSelectorComponent to delegate to PopupList instead
of implementing its own full-screen rendering and input handling
- Render /model selector as a centered overlay on top of the chat
view instead of replacing the entire screen
- PopupList accepts a pluggable FilterFunc for domain-specific
fuzzy matching (model selector wires its own scoring)
- Add 11 tests for PopupList covering navigation, search, selection,
cancellation, filtering, rendering, and edge cases
- Wire fantasy's OnReasoningEnd callback through the full event chain:
agent → SDK (ReasoningCompleteEvent) → app → TUI
- Freeze reasoning duration in both StreamComponent and
StreamingMessageItem as soon as reasoning ends, not when the
next assistant text chunk arrives
- Fix accent color on duration label in render.ReasoningBlock to
match the live streaming style (VeryMuted prefix + Accent duration)
- Refactor go-edit-lint to collect edited .go files during the agent
turn via OnToolResult, then run gopls + golangci-lint once in
OnAgentEnd instead of after every individual edit/write call
- Use ctx.SendMessage() to inject diagnostics as a follow-up prompt
when issues are found, replacing the old tool-result rewriting
- Show a green 'all clean' block when no issues are detected
- Fix StopReason docs in skills/kit-extensions/SKILL.md: the value is
'error' on failure, 'completed' when the LLM returns empty, or the
raw provider value (e.g. 'stop', 'end_turn') passed through — not
the previously documented 'completed'/'cancelled'/'error' enum
Remove 8 unused exports from clipboard package:
- CopyToClipboardWithMessage, IsClipboardSupported
- ToastMsg, ToastType, ToastInfo, ToastSuccess, ToastWarning, ToastError
These were remnants of a toast notification feature that was never
wired up. No callers exist anywhere in the codebase.
The getItemAndLineAtY() method was using item.Height() which returns 0
for reasoning blocks (StreamingMessageItem with role='reasoning') because
their render cache is intentionally never populated (they include a live
duration timer).
This caused all items below a reasoning block to have incorrect Y
coordinates — clicking on the reasoning text would highlight the
assistant text below it instead.
Two fixes:
1. getItemAndLineAtY() now uses renderedHeight() which calls Render()
and counts lines — matching exactly what View() does. This is the
single source of truth for item height during hit-testing.
2. StreamingMessageItem.Height() now falls back to Render(0) when
cachedRender is empty, fixing the same issue for other callers
(GotoBottom, ScrollBy, clampOffset, etc.).
- Add ForkToNewSession method to create new session with history up to target
- Add NewTreeSelectorForFork showing only user messages (flat list)
- Update performFork to create and switch to new session file
- Update /fork command description in docs and help text
Previously /fork just branched within the same session file like /tree.
Now /fork creates a completely new session file with parent_session reference,
matching Pi's behavior exactly.
When loading session history, some assistant messages contain text parts
with only whitespace (e.g., single space ' '). These were being rendered
as empty message blocks, causing extra vertical spacing in the UI.
Fix by trimming whitespace from message content before checking if it's
non-empty in renderSessionHistory().
Changes:
- Apply strings.TrimSpace() to user message content before rendering
- Apply strings.TrimSpace() to assistant message content before rendering
This prevents empty/whitespace-only message blocks from being added to
the scrollback when resuming sessions.
Extract pure rendering functions into internal/ui/render/blocks.go
to eliminate code duplication between streaming and historical
message rendering paths.
Changes:
- Create render package with UserBlock, AssistantBlock, ReasoningBlock,
SystemBlock, ErrorBlock, and ToolBlock functions
- Update MessageRenderer methods to use shared render functions
- Update StreamingMessageItem to use shared render functions
- Reduce ~77 lines of duplicated code across message_items.go and messages.go
All existing tests pass, no functional changes.
- Change border from MutedBorder to Primary for visibility
- Add full-width background styles for all popup items
- Use inverse colors for selected item (primary bg, background fg)
- Add background to scroll indicators and footer
- Add bottom margin for visual depth/shadow effect
Auto-scroll was being disabled when users manually scrolled (mouse wheel,
PgUp, etc.) but never re-enabled. Now it reactivates when submitting a
new message so the conversation view jumps to the bottom to show the
latest content.
Add a dedicated /feature-request prompt that guides users through creating
well-formed feature requests using the GitHub feature_request template.
The prompt focuses on:
- Problem-first description
- Clear motivation and use cases
- Optional proposed implementation
- Conventional commit-style titles (feat: ...)
Usage: /feature-request <description of the feature>
The file-issue prompt now references the structured GitHub issue templates
(bug_report, feature_request, documentation) and guides users to use the
--template flag with gh issue create for consistent issue formatting.
Add structured GitHub issue templates for:
- Bug reports (with reproduction steps, code, component)
- Feature requests (with motivation and proposed implementation)
- Documentation issues
Also add a /file-issue kit prompt for quickly filing issues from the TUI.
The templates enforce conventional commit-style titles and include
checklists to ensure issues are well-formed before submission.
Remove 'j' and 'k' keybindings from model, session, and tree selectors
to allow typing those characters for fuzzy filtering. Navigation now
uses only arrow keys (↑/↓) which matches the existing help text.
Fix /resume, /model, and /tree selectors to render in the alternate
screen buffer instead of terminal scrollback. All three selector
components now set AltScreen=true on their tea.View returns.
- Remove scrollbackBuf, appendScrollback(), drainScrollback() and all
call sites — the entire terminal scrollback pipeline was dead code
since the alt screen migration
- Remove StreamComponent.render(), renderCache, renderDirty,
scrollbackFlushedLines, viewContent(), and ConsumeOverflow() body —
rendering is now handled by StreamingMessageItem in the ScrollList
- Remove SetHeight and ConsumeOverflow from streamComponentIface since
height is managed by ScrollList and overflow is a no-op
- Remove redundant AltScreen/MouseMode/ReportFocus/KeyboardEnhancements
boilerplate from 6 child View() methods — parent already sets these
- Convert two orphan appendScrollback calls (extension default text,
shell command output) to proper ScrollList message items
- Update ~30 stale comments referencing tea.Println and scrollback buffer
- 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 a horizontal rule (────) with blank lines above and below to
visually separate the startup info from the conversation history.
The separator uses theme.Border color and spans 80 characters, providing
a clear visual break between startup messages and the chat content.
This makes it easier to distinguish where the conversation starts when
scrolling back through history.
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.
Enhanced clampOffset() to detect when the viewport has scrolled past the
bottom of the content (would show empty space) and automatically reposition
to show the last line of content at the bottom of the viewport.
This prevents the 'floating' effect where multiple PgDn or scroll down
operations would push content off the top while showing blank space below.
The clamping logic:
1. Calculates total content height
2. If content fits in viewport, forces position to top
3. Otherwise, checks if remaining content < viewport height
4. If so, repositions to show exactly the last line at viewport bottom
Also updated clampOffset to use rendered height calculation (handles
non-cached items like reasoning blocks) instead of cached Height().
Root cause: GotoBottom() was calculating heights using Height() which returns
0 for non-cached items. Reasoning blocks never cache renders due to live
duration updates, causing incorrect scroll calculations during reasoning →
assistant transitions.
Fix: Calculate heights directly from rendered strings instead of relying on
cached Height() values. This ensures accurate scroll positioning for all
message types.
Changes:
- ScrollList.GotoBottom(): Render items and calculate height from string
- ScrollList.AtBottom(): Same pattern for bottom detection
- appendStreamingChunk(): Call GotoBottom() directly for existing messages
- refreshContent(): Remove redundant GotoBottom() (handled by SetItems)
Tested with 'explore this repo' prompt - autoscroll now works correctly
throughout reasoning and assistant streaming phases.
- Use max() instead of if statement for min value
- Use strings.SplitSeq for efficient iteration
- Use range over int instead of explicit loop counter
- Remove unused functions:
- InputComponent.renderPopup()
- AppModel.renderStream()
- AppModel.renderStreamingBashOutput()
- AppModel.printCompactResult()
flushStreamContent() was creating a new StyledMessageItem when tool calls
started, but we already had a StreamingMessageItem with the same content.
Now we:
- Mark the existing StreamingMessageItem as complete
- Only create a new message as fallback if no streaming item exists
This fixes text duplication when assistant messages precede tool calls.
- Limit streaming bash output to 20 lines max during live display
- Remove streaming bash item when tool completes
- Replace with truncated tool result block
- Expand background color to full terminal width with proper indentation
- Matches renderBashBody styling (lineIndent + width)
This prevents long-running commands from growing the UI forever while
still showing live output up to a reasonable height.
- Add StreamingBashOutputItem to message_items.go
- Update ToolOutputEvent handler to append chunks to bash item in ScrollList
- Remove old renderStreamingBashOutput() that broke layout
- Bash output now streams inline with messages instead of separate section
- Auto-scrolls to bottom during streaming
- Marks bash item complete on ToolResultEvent
Fixes layout breaking when bash commands produce streaming output.
- Add StreamCallback parameter to compaction.Compact() for streaming text deltas
- Update generateSummary() to use fantasy.Agent.Stream() when callback provided
- Fix compactSplitTurn() to stream both history and turn prefix summaries
- Add SDK event subscription in CompactConversation() goroutine
- Update UI to handle streaming compaction like regular assistant messages
- Compaction summaries now stream word-by-word instead of appearing all at once
Fixes issue where compaction would show incomplete context (e.g. only 'nce')
by ensuring both history summary and turn prefix are streamed to the UI.
- Move popup rendering from inline (below input) to centered overlay
- Add RenderPopupCentered() method to InputComponent
- Implement overlayContent() helper for line-by-line merging
- Popup now appears in center of screen above all content
- Prevents overflow issues when typing / at bottom of terminal
- Detect role transition in appendStreamingChunk (reasoning → assistant)
- Mark reasoning StreamingMessageItem as complete when assistant text starts
- Duration counter now freezes immediately when reasoning ends
- Add live duration counter that updates during reasoning streaming
- Store startTime and finalDuration for proper counter behavior
- Create StreamingMessageItem that accumulates chunks and re-renders
- Update StreamChunkEvent/ReasoningChunkEvent to append to StreamingMessageItem
- Enable live streaming display within ScrollList (iteratr-style)
- Mark streaming items as complete on ResponseCompleteEvent
- Reasoning and assistant text now stream in real-time in the viewport
Disable the mouse selection and keyboard copy features while keeping
all the supporting code infrastructure:
- Comment out MouseClickMsg, MouseMotionMsg, MouseReleaseMsg handlers
- Comment out keyboard shortcuts (c/y keys) for copying
- Keep all ScrollList selection tracking code
- Keep clipboard utilities (clipboard.go)
- Keep highlighting functions in scrolllist.go
This allows the features to be easily re-enabled later while keeping
the codebase clean for now.
Implement visual feedback for text selection in the scrollback:
- Add isLineInSelection() to check if a line is within the current selection
- Add applyHighlight() using the theme's Highlight color for selected lines
- Add applyFocusIndicator() using MutedBorder for focused items
- Update View() to apply highlighting during rendering
- Add getItemAndLineAtY() for precise mouse position tracking
- Track both item index and line index within item for selection
Selection highlighting uses the user's selected theme colors for
consistent visual feedback across all themes.
Implement mouse selection and keyboard copy functionality following
crush's patterns:
- Add clipboard.go with dual-write clipboard support (OSC 52 + system)
- Add CopySelection tracking to ScrollList for text selection
- Implement HandleMouseDown/HandleMouseDrag/HandleMouseUp methods
- Add keyboard shortcuts (c/y) for copying messages
- Mouse click+drag to select text, auto-copy on release
- Toast notifications for copy feedback
Note: Full text extraction from selection requires additional work to
properly extract raw text from styled message content.
Add SetSessionModel method to the ACP agent, allowing clients to change
the active LLM model for a session at runtime. The method looks up the
session in the registry and delegates to kit.SetModel().
Verified with smoke test: session/set_model now returns success instead
of 'Method not found' error.
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
Remove charm.land/fantasy from the public API surface of pkg/kit by
replacing the four type aliases with concrete Kit-owned structs:
- LLMMessage {Role LLMMessageRole, Content string}
- LLMUsage {InputTokens, OutputTokens, TotalTokens, ...}
- LLMResponse {Content, FinishReason, Usage}
- LLMFilePart {Filename, Data []byte, MediaType}
Add LLMMessageRole type with user/assistant/system/tool constants.
Introduce pkg/kit/llm_convert.go as the single boundary layer where
Kit types convert to/from fantasy types internally. All callers in
pkg/kit, pkg/kit/compaction.go, pkg/kit/extensions_bridge.go, and
internal/app/app.go cross through this layer.
ContextPrepareHook.Messages and ContextPrepareResult.Messages change
from []fantasy.Message to []LLMMessage. extensions_bridge.go drops
its fantasy and strings imports entirely.
internal/app/app_test.go switches &fantasy.Usage{} to &kit.LLMUsage{}.
Add seven new tests in types_test.go covering concrete construction,
role constants, JSON snake_case tags, and round-trip conversion.
- Remove always-zero queueLen variable: len() was measured after
clearing the queue, so it was unconditionally 0 and the variable
was dead code
- Emit QueueUpdatedEvent{Length: 0} explicitly to make intent clear
- Also emit QueueUpdatedEvent when a second batch is pulled mid-loop;
previously the queue was silently cleared without notifying the UI,
leaving queuedMessages stuck in the displayed-queued state forever
/new-prompt <description> scaffolds a new .kit/prompts/ template.
Explains the file format, argument substitution syntax, naming
conventions, and writing guidelines.
Provides a /commit-push slash command that reviews git status and diff,
stages all changes, writes a conventional commit message, commits, and
pushes to the current branch.
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
When using /resume to resume a session, reasoning/thinking content
was not being displayed even though it was saved in the session file.
Changes:
- Add RenderReasoningBlock to Renderer interface
- Implement RenderReasoningBlock for MessageRenderer with muted italic
styling matching live streaming output
- Implement RenderReasoningBlock for CompactRenderer with same styling
- Update renderSessionHistory to render reasoning content before
assistant message text
Fixes: reasoning blocks now populate correctly when resuming sessions
The Qwen model outputs thinking content wrapped in tags
(not <thinking>). Updated parseThinkingTags to detect and handle
both formats:
- <thinking>...</thinking> (long format)
- (short format)
Also removed the hasProperReasoningEvents logic that was preventing
thinking tag parsing from working correctly. Now both ReasoningDeltaEvent
(from models with proper reasoning APIs) and thinking tags in text
(from models like Qwen) are handled together, matching the TUI behavior.
Track whether a model sends proper ReasoningDeltaEvent events. If so,
skip parsing <thinking> tags from text to avoid sending reasoning content
twice (once as proper reasoning, once parsed from text).
Also reset the tracking state at the start of each new prompt turn.
Parse <thinking>...</thinking> tags from models (Qwen, DeepSeek) that
wrap reasoning content in XML-style tags instead of using proper
reasoning events.
When text chunks contain thinking tags:
- Extract content between tags and send as reasoning/thought updates
- Send content outside tags as regular message text
- Track state across chunks to handle streaming properly
This mirrors the TUI's thinking tag parsing behavior.
- 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
Previously, when streaming text grew taller than the allocated view
height, the top (older) lines were silently discarded by viewContent().
This meant users could not scroll up to see them.
Now, overflow lines are emitted directly via tea.Println so they land
in the terminal's real scrollback buffer — matching the diagram where
completed text lives in the red scrollback region and the green viewable
area always shows the most recent streaming lines + input/footer.
Key changes:
- StreamComponent: add scrollbackFlushedLines counter and ConsumeOverflow()
method that returns newly overflowed lines and advances the pointer
- StreamComponent.Reset(): zero the counter between steps
- StreamComponent.GetRenderedContent(): skip already-flushed lines so
the end-of-step flush doesn't re-emit content already in scrollback
- AppModel.Update(): call ConsumeOverflow() each cycle and emit overflow
directly via tea.Println (not appendScrollback, to avoid triggering
drainScrollback's auto-flush guard while streaming is active)
- streamComponentIface: add ConsumeOverflow() to interface
- model_test.go: add stub ConsumeOverflow() to test double
- children_test.go: add 7 unit tests covering ConsumeOverflow and the
updated GetRenderedContent skip-flushed-lines behaviour
When users run `/theme <name>`, the alert colors (Tip, Note, Warning, etc.)
now update correctly. Previously, MessageRenderer and StreamComponent cached
herald.Typography instances that weren't refreshed after theme changes.
Changes:
- Added UpdateTheme() method to Renderer interface
- Implemented UpdateTheme() for MessageRenderer to recreate herald typography
- Added no-op UpdateTheme() stub for CompactRenderer (fetches colors fresh)
- Implemented UpdateTheme() for StreamComponent reasoning block renderer
- Modified handleThemeCommand() to notify all renderers of theme changes
This ensures newly rendered messages use the current theme's alert colors.
- 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
Models like Qwen and DeepSeek wrap reasoning content in ... XML-like
tags within the regular content field. This was causing the reasoning
text to appear twice - once as a reasoning block and once as regular text.
Changes:
1. Provider hooks (providers.go):
- Extract reasoning from tags and emit proper reasoning events
- Use openai provider directly with custom ExtraContentFunc and
StreamExtraFunc hooks to parse thinking content
2. Stream filtering (stream.go):
- Filter out all text content between and tags at the
streaming level to prevent duplicate rendering
- Track state with inThinkTag flag across stream chunks
3. Message conversion (content.go):
- Strip any remaining tags from text content when converting
from fantasy messages
The regex patterns use string concatenation to avoid XML tag corruption:
regexp.MustCompile( + + + + + + + )
Fixes duplicate reasoning text when using custom provider with models
that wrap thinking in tags.
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
- Replace glamour-based markdown rendering with herald/herald-md
- Update go.mod and go.sum with new dependencies
- Refactor styles.go to use Typography cache instead of TermRenderer
- Update enhanced_styles.go for compatibility
- Update btca.config.jsonc configuration
- Update github.com/indaco/herald v0.9.0 -> v0.10.0
- Update charm.land/bubbles/v2 v2.0.0 -> v2.1.0
- Update AWS SDK v2 packages
- Update google.golang.org/genai v1.51.0 -> v1.52.0
- Update various other dependencies
refactor(ui): use herald.CodeBlock for Read tool output
- Replace manual renderCodeBlock() with herald.CodeBlock()
- Add WithCodeLineNumberOffset() support for correct line numbers
- Extract language hint from file extension for syntax highlighting
- Preserve existing syntax highlighting via WithCodeFormatter()
- Remove unused codeLine struct and renderCodeBlock function
Add two test files that auto-discover and validate every single-file
extension in examples/extensions/:
- all_extensions_load_test.go: Verifies all 32 extensions load into the
Yaegi interpreter without errors (syntax, imports, Init signature).
- all_extensions_sanity_test.go: Six generalized sanity checks:
- Lifecycle: SessionStart → SessionShutdown round-trip
- CommandSanity: non-empty names/descriptions, no spaces/leading slash,
non-nil Execute, no duplicates
- ToolSanity: non-empty names/descriptions, at least one executor,
valid JSON parameters, no duplicates
- ZeroValueEvents: all 22 event types fired as zero-value structs
- WidgetSanity: non-empty IDs, consistent keys, valid placements
- IdempotentLifecycle: repeated SessionStart/SessionShutdown
Shared extensionFiles() helper auto-discovers extensions so new files
are automatically covered.
performFork() called ClearMessages() after Branch(targetID), but
ClearMessages() calls TreeSession.ResetLeaf() which sets leafID back
to empty — immediately undoing the branch. The in-memory message store
was also never reloaded from the tree session after branching, so the
LLM had zero context.
Add ReloadMessagesFromTree() which clears the store and reloads it
from the tree session's current branch without resetting the leaf
pointer. Use it in performFork() instead of ClearMessages().
The issue was that cache control persisted across turns in conversation
history, causing accumulation beyond Anthropic's 4-block limit.
Changes:
- Count existing cache blocks in message history before adding new ones
- Only add new cache blocks up to the 4-block limit
- Remove tool caching (was adding 1 block per turn)
- Skip messages that already have cache control set
Tested with 5 sequential messages - no errors, proper cache metrics.
Crush's proven 4-block strategy:
1. Last system message (if present)
2. Last 2 conversation messages
3. Last tool definition
This stays exactly at Anthropic's 4-block limit without exceeding it.
Previous implementation could exceed the limit in certain edge cases.
Now matches Crush's battle-tested approach.
Anthropic API enforces a maximum of 4 blocks with cache_control per request.
The previous implementation could exceed this limit when combining:
- System message caching
- Recent message caching
- Tool definition caching
Changes:
- Add explicit cache block counting (max 4)
- Remove tool cache control to stay under limit
- Prioritize: system message first, then recent messages
- Work backwards from end to cache most recent context first
Fixes: bad request error 'A maximum of 4 blocks with cache_control may be provided'
- 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
- Upgrade golangci-lint to v2.11.4
- Fix errcheck warnings for os.Setenv/os.Unsetenv in tests
- Use maps.Copy instead of manual loop (modernize lint)
- Add maps import for maps.Copy
Remove internal monologue comments that don't add value for readers:
- Remove lengthy explanations of type conflicts that are now resolved
- Remove 'NOTE:' and 'TODO:' comments documenting implementation history
- Remove obvious test comments that just restate what the code does
- Keep only meaningful comments that explain design intent
The code is now cleaner and easier to read without the self-referential
commentary that was useful during development but not for maintenance.
Implements automatic prompt caching to reduce API costs by 60-90% for
repeated prompts with the same context.
Architecture:
- Provider-level caching for OpenAI (PromptCacheKey)
- Message-level caching for Anthropic (avoids type conflicts)
- Model family detection enables caching regardless of provider
Key Changes:
- Add ModelInfo.Family with SupportsCaching() and CacheType() methods
- Add ProviderConfig.DisableCaching for opt-out
- Implement message-level cache control in agent (like Crush)
- Last system message gets cache control
- Last 2 messages get cache control
- Last tool gets cache control
- Auto-disable caching when thinking is enabled (type conflict avoidance)
- Add KIT_DISABLE_CACHE environment variable for global opt-out
Tested with opencode/claude-sonnet-4-6 showing cacheRead/cacheWrite
tokens in debug output, confirming 60-90% cost savings.
Closes cost optimization for multi-turn conversations.
Rename public SDK symbols to use generic LLM terminology instead of
exposing the internal dependency name (charm.land/fantasy):
Public API renames (with deprecated wrappers for backward compat):
- ConvertToFantasyMessages() → ConvertToLLMMessages()
- ConvertFromFantasyMessage() → ConvertFromLLMMessage()
- GetFantasyProviders() → GetLLMProviders()
New type alias:
- LLMFilePart = fantasy.FilePart (eliminates need for direct fantasy import)
- PromptResultWithFiles() signature now uses LLMFilePart
Internal renames (with deprecated wrappers):
- ModelsRegistry.GetFantasyProviders() → GetLLMProviders()
- TreeManager.GetFantasyMessages() → GetLLMMessages()
- TreeManager.AppendFantasyMessage() → AppendLLMMessage()
- TreeManager.AddFantasyMessages() → AddLLMMessages()
- Message.ToFantasyMessages() → ToLLMMessages()
- FromFantasyMessage() → FromLLMMessage()
- npmToFantasyProvider → npmToLLMProvider
- isProviderFantasySupported() → isProviderLLMSupported()
All internal callers migrated to new names. ~30 comments updated
to remove Fantasy references across pkg/kit/, internal/agent/,
internal/models/, internal/message/, internal/session/.
Documentation updates:
- AGENTS.md: added Public SDK rules section (no dependency leakage,
naming conventions, deprecation pattern)
- README.md: removed Fantasy references
- pkg/kit/README.md: full rewrite with current API surface
- skills/kit-sdk/SKILL.md: updated examples and type references
- www/pages/providers.md, www/pages/cli/commands.md: updated
- Add StepUsageEvent and SteerConsumedEvent to event types table
- Add new Extension API section documenting kit.Extensions() sub-API
- Add extension_api.go to Key Files reference list
- Fix Close() error handling in README SDK example
- 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.
kit.go
- Extract iterBranchMessages helper to eliminate ~15 lines of duplicated
branch-fetch/type-assert boilerplate between GetSessionMessages and
GetStructuredMessages
- Move skillCache from package-level global to per-Kit field; avoids
cross-contamination when multiple Kit instances exist in same process
skills.go
- Remove globalSkillCache var and skillCache type definition
- Update DiscoverSkillsForExtension and ClearSkillCache to use m.skillCache
- Remove unused sync import
sessions.go
- Use m.treeSession.EntryID instead of local getEntryID duplicate
- Remove local getEntryID function (was missing LabelEntry, SessionInfoEntry,
CompactionEntry types that internal/session.TreeManager.EntryID handles)
internal/session/tree_manager.go
- Export entryID -> EntryID so pkg/kit can use it directly
- Update all internal callers to use EntryID
config.go
- Add sync comment for defaultSystemPrompt noting it should be kept in sync
with CLI default in cmd/root.go
hooks_test.go
- Add newEmptyHookedTool helper for tests that need hookedTool with empty
hook registries
- Update TestHookedTool_Passthrough and TestHookedTool_InfoDelegates to use
helper (saves ~6 lines of boilerplate each)
- Merge TestHookRegistry_HasHooks into TestHookRegistry_Unregister (was
testing same behavior, now just one initial state assertion added)
All changes tested with opencode/kimi-k2.5 exploring the repo in tmux.
6 files changed, 69 insertions(+), 98 deletions(-)
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.
events.go
- Delete subagentListenerSet (verbatim duplicate of eventBus); reuse
*eventBus in SubscribeSubagent and getSubagentListenerSet
hooks.go
- Add early-exit in run() when hooks slice is empty, making all
hasHooks() guard call sites in kit.go and compaction.go redundant
kit.go
- Remove four if m.X.hasHooks() { m.X.run(...) } outer guards
(beforeTurn, contextPrepare, afterTurn x2); run() now short-circuits
- Replace goto drained with an idiomatic return inside default: branch
- Replace stdlib log.Printf with charmlog.Debug (charmbracelet/log),
consistent with the rest of the codebase; remove "log" import
config.go
- Collapse single-element configNames := []string{".kit"} loop into a
direct viper.SetConfigName call (removes slice, for, break, flag)
auth.go
- Fix GetOpenAIAPIKey: it documented OPENAI_API_KEY env var fallback but
never called os.Getenv; now it does
compaction.go
- Extract persistAndEmitCompaction helper; eliminates duplicated
AppendCompaction + events.emit block in compactInternal and
applyCustomCompaction
- Replace fmt.Errorf("%s", reason) with errors.New(reason)
- Name the 16384 magic number as const defaultReserveTokens
skills.go
- Fix broken double-checked lock in DiscoverSkillsForExtension: the
read-unlock -> write-lock gap had a TOCTOU race; replaced with a
single write-lock covering the check and load
- Remove dead nil guard in convertSkills (convertSkill never returns nil)
- Rename convertSkills parameter skills->skillList to avoid shadowing
the skills package import
extensions_bridge.go
- Delete taskMutex struct (sync.Mutex wrapper with map passed as param);
replace with inline var taskMu sync.Mutex at the use site
- Simplify AgentEnd double-if into a single combined := declaration
template_bridge.go
- Fix RenderTemplate: use varRegex.ReplaceAllStringFunc instead of
two-pass strings.ReplaceAll; handles arbitrary whitespace in {{var}}
- Remove dead isFlag function and simplify ParseArguments guard
(the outer !HasPrefix guard made isFlag always return false)
- Cache matchModelPattern compiled regexps in a sync.Map to avoid
repeated regexp.Compile on hot streaming paths
pkg/extensions/test/mock.go
- Remove dead local StatusBarEntry type (duplicate of extensions type,
never referenced)
- Change make([]T, 0) to nil for nine slice fields in NewMockContext
pkg/extensions/test/harness.go
- Remove MustLoad (no callers outside the package)
- Remove extPath field (assigned but never read)
- Remove redundant os.Stat in LoadFile (os.ReadFile already errors)
events_test.go
- Add five missing event types to TestEventTypes table
(Compaction, ReasoningDelta, ToolOutput, StepUsage, SteerConsumed)
- Expand TestEventOrdering from 11 to 16 events with the same types
- Add a got < 0 assertion to TestEventBusConcurrentSubscribeEmit so the
test can actually fail rather than only logging
## 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
- Extract isShellTool() helper in tool_renderers.go to eliminate
duplicated shell tool matching logic
- Replace bannedCommands slice with compiled regex in bash.go for
cleaner security validation
- Extract pathSet helper type in loader.go for reusable path
deduplication
- Consolidate ac()/acOr() helpers in themes.go for better organization
- Total reduction: ~34 lines across 4 files
All tests pass (go test -race ./...) and build succeeds.
These methods have been deprecated since the narrow-accessor and event-
subscriber APIs were introduced. No callers exist in this repository.
- pkg/kit/kit.go: remove GetExtRunner(), GetBufferedLogger(), GetAgent(),
and PromptWithCallbacks(); update Subscribe() doc comment which still
mentioned PromptWithCallbacks; tighten section header comment
- pkg/kit/README.md: replace PromptWithCallbacks example with the
OnToolCall/OnToolResult/OnStreaming subscriber pattern; remove method
from the quick-reference list
- README.md: same example migration in the SDK section
- www/pages/sdk/callbacks.md: remove the PromptWithCallbacks section
entirely; the event-based monitoring section that followed it is now
the lead content
- www/pages/sdk/overview.md: remove PromptWithCallbacks row from the
prompt-variant table
- skills/kit-sdk/SKILL.md: remove the deprecated legacy callback snippet
- agent: remove unused currentToolName variable and its compiler-suppressor
'_ = currentToolName'; currentToolArgs is the field actually used by
OnToolResult callbacks
- tools/connection_pool: collapse double-nested identical if guard into a
single check (copy-paste artifact)
- tools/mcp_test: replace hand-rolled contains() helper with strings.Contains;
add 'strings' import and delete the redundant function
- config: fix tilde path expansion (filepath.Join result was discarded)
- config: remove dead comment '// base := GetConfigPath()'
- auth: extract oauthTokenExpired/oauthTokenNeedsRefresh helpers to
eliminate copy-paste duplication across AnthropicCredentials and
OpenAICredentials
- ui/messages: remove dead RenderToolCallMessage on MessageRenderer
(not part of Renderer interface, never called)
- ui/compact_renderer: remove dead RenderToolCallMessage on CompactRenderer
(symmetric duplicate, never called)
- ui/enhanced_styles: remove dead CreateGradientText wrapper
(one-liner over ApplyGradient, never called)
- ui/fuzzy: fix fuzzyCharacterMatch to use rune iteration instead of
byte indexing (was silently wrong for multi-byte Unicode input)
- ui/file_suggestions: remove duplicate fuzzyCharMatch; call the now-
correct shared fuzzyCharacterMatch instead; drop unused utf8 import
- app: replace TODO comment with descriptive note (batch file attachment
limitation is intentional, not a pending action item)
This commit fixes several issues with token usage tracking:
1. Fix InputTokens-only validation bug - now checks any token field > 0
to handle OpenAI-compatible providers where cached prompts result in
InputTokens=0 while OutputTokens>0
2. Remove per-step context token updates from recordStepUsage() - context
fill is now set once at turn completion via updateUsageFromTurnResult
using FinalUsage.InputTokens, preventing display jumps during multi-step
tool calls
3. Track maximum context seen in SetContextTokens() - prevents the status
bar from showing decreasing token counts when FinalUsage.InputTokens
reflects only the last step's input
4. Add comprehensive debug logging for token tracking at key points:
- StepUsageEvent emission
- recordStepUsage processing
- updateUsageFromTurnResult processing
5. Update tests to reflect new behavior:
- TestRecordStepUsage_updatesTracker: no longer expects context updates
- TestUpdateUsageFromTurnResult_contextTokensUsesInputOnly: verifies
InputTokens-only tracking
All tests pass. Token tracking now correctly accumulates costs and shows
monotonically increasing context size.
- /new command now properly resets usageTracker stats when starting fresh session
- Remove EstimateAndUpdateUsage fallback in updateUsageFromTurnResult()
- Remove EstimateAndUpdateUsage fallback in UpdateUsageFromResponse()
- Only use actual API-reported tokens for cost tracking (following opencode pattern)
- Estimation is inaccurate and should never be used for billing
Fixes issues with kimi-k2.5 and opencode token tracking where:
1. /new didn't reset token count/cost
2. Tokens never updated correctly due to estimation fallback
Change thinking content from H6 to Italic for more subdued,
secondary visual appearance. Makes reasoning text less prominent
than main assistant responses.
- Change AlertNote label from "You" to "Info" for system/extension messages
- Update RenderUserMessage to use custom styling with "You" label
- This separates user messages ("You") from info messages ("Info")
Update RenderSystemMessage to use r.ty.Note() instead of r.ty.P()
for visual consistency with other herald-based message rendering.
This affects extension PrintInfo output and system messages.
- 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
The herald-based CodeBlock implementation didn't match the custom
styling we had for line numbers and gutters. Restoring the original
renderReadBody and renderCodeBlock functions with:
- Custom line number gutter styling
- Chroma syntax highlighting
- Truncation handling with footer preservation
- 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
Extensions were being loaded automatically by SetupAgent but the context
was never initialized unless the SDK user explicitly called
SetExtensionContext. This left extensions with a zero-value Context where
all function fields are nil.
Now kit.New() automatically calls SetExtensionContext with minimal defaults
(CWD, Model, Interactive=false) when extensions are loaded. SDK users can
still call SetExtensionContext to override with richer implementations
(TUI callbacks, prompts, etc.).
Combined with the normalizeContext() safety net in the runner, extensions
are now guaranteed to work in SDK mode without explicit context wiring.
Extensions running via the SDK (without a fully-wired SetExtensionContext
call) would panic with 'reflect.Value.Call: call of nil function' when
calling any ctx method like ctx.PrintBlock().
normalizeContext() now replaces every nil function field in Context with
a safe no-op stub before storing it in the runner, so extension handlers
can never crash on a missing callback regardless of how Kit is embedded.
The StreamComponent was truncating content to fit the viewport height before
caching it in renderCache. This caused GetRenderedContent() to return truncated
content when flushing to scrollback.
Changes:
- render() now caches FULL content without height clamping
- New viewContent() helper applies height clamping only for display
- View() calls both: render() for full content, viewContent() for visible slice
This follows the Pi TUI pattern: full buffer in memory, viewport slicing only
at display time. Long assistant messages are now fully preserved in scrollback.
Remove the StepUsageEvent handler from subscribeSDKEvents. It was
calling UpdateUsage() for every individual tool-calling step as it
streamed, then updateUsageFromTurnResult() called UpdateUsage() again
with TotalUsage (fantasy's own aggregate of all steps). A turn with N
tool calls was counting every token N+1 times.
Fix updateUsageFromTurnResult to use a single, clean code path:
- UpdateUsage() called exactly once per turn using TotalUsage
- SetContextTokens() uses FinalUsage.InputTokens only (not +OutputTokens)
since input tokens of the last call = actual context window fill;
output tokens are the response length, not context occupancy
- Estimate fallback no longer early-returns before SetContextTokens
Verified with opencode/kimi-k2.5: cost accumulates linearly across
simple and multi-step tool-calling turns with no double-counting.
anthropic/claude-sonnet-4-6 correctly shows $0.00 for OAuth sessions.
- Update all Go dependencies to latest versions
- Remove internal/app/usage_test.go (import cycle)
- Add sanitizeToolCallID function to fix message tests
- All tests pass with race detection
Empty sessions (no messages) are now automatically cleaned up:
1. On shutdown: When kit exits cleanly, if the current session has no
messages, the session file is deleted.
2. On /resume: When listing sessions for the resume picker, any empty
session files are deleted and not shown in the list.
This prevents accumulation of orphaned empty session files when users
start sessions but don't send any messages.
Changes:
- internal/session/tree_manager.go: add IsEmpty() helper
- internal/app/app.go: delete empty session on Close()
- internal/session/store.go: filter and delete empty sessions in listSessionsInDir()
Change /new behavior to match Pi:
- Create a completely new session file instead of just resetting the leaf
- Previous session is closed and saved (accessible via /resume)
- New session starts with 0 entries, 0 messages - clean slate
- Update help text to reflect new behavior
Key fix: SwitchTreeSession now updates the kit SDK's tree session
reference so messages are persisted to the correct file.
Files changed:
- internal/app/app.go: update kit SDK session reference
- internal/ui/model.go: create new session file on /new
- internal/ui/model_test.go: add SwitchTreeSession stub
- Remove unused modelFamily variable in createOpenAICodexProvider
- Remove dead spark handling code (spark is rejected early with error)
- Simplify buildCodexProviderOptions to only handle regular codex models
- Remove redundant comments and simplify code structure
- Net reduction: 31 lines of code
Spark models are not accessible via ChatGPT OAuth and return Cloudflare
'Forbidden' errors. Add early detection and helpful error message directing
users to regular Codex models like 'openai/gpt-5.3-codex' instead.
Different Codex model families use different API formats:
- gpt-codex-spark: uses standard ProviderOptions (not Responses API)
- gpt-codex, gpt-codex-mini: uses ResponsesProviderOptions
- Add detectCodexModelFamily() to determine model family from name
- Use standard ProviderOptions for spark models
- Use ResponsesProviderOptions for regular codex models
- Conditionally use WithUseResponsesAPI() based on model family
Note: gpt-5.3-codex-spark still gets Cloudflare forbidden error,
may need additional headers or different endpoint.
When using /model command to switch models, the system prompt was not being
passed to the new provider config. This caused OpenAI Codex to fail with
"Instructions are required" error.
- Load system prompt using config.LoadSystemPrompt() in SetModel
- Pass SystemPrompt to ProviderConfig when building model config
- This ensures Codex OAuth gets the instructions field it requires
The Codex API doesn't support the max_output_tokens parameter, which was causing
"Unsupported parameter: max_output_tokens" errors.
- Add SkipMaxOutputTokens flag to ProviderResult
- Set flag when creating Codex OAuth provider
- Check flag in agent setup to skip WithMaxOutputTokens option
- This matches pi's behavior of not sending max_tokens to Codex API
- Upgrade charm.land/fantasy from v0.16.0 to v0.17.1
- Add buildCodexProviderOptions() to pass system prompt as 'instructions'
- The Codex API requires instructions as a top-level field, not as system message
- Set Store=false to prevent server-side conversation storage
- Use ResponsesProviderOptions.Instructions for system prompt
- Change base URL to /backend-api/codex for correct endpoint path
- Add browser-like User-Agent to avoid Cloudflare blocking
- Add Accept, Accept-Language, Cache-Control headers
- Match pi client headers more closely
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 displaying streaming bash output, show the initial command as a
muted header ($ <command>) before the output lines. This helps users
understand what command is currently executing.
Changes:
- Add streamingBashCommand field to AppModel
- Extract command from ToolCallStartedEvent for bash tools
- Render $ <command> header in renderStreamingBashOutput
- Clear command on ToolResultEvent when tool completes
- Add tests for command extraction and cleanup
Update all documentation to include the new OnSubagentStart, OnSubagentChunk,
OnSubagentEnd lifecycle events for monitoring subagents spawned by the main agent:
README.md:
- Update lifecycle events list (20 → 23 events)
- Add subagent-monitor.go to examples list
www/pages/extensions/capabilities.md:
- Update event count (20 → 23)
- Add 3 new subagent events to lifecycle table
- Add 'Monitoring subagents spawned by the main agent' section with
complete event handler documentation and struct definitions
www/pages/extensions/examples.md:
- Add subagent-monitor.go to Multi-agent section
- Add subagent-monitor_test.go to Development section
www/pages/advanced/subagents.md:
- Add 'Monitoring subagents from extensions' section with complete
code example and event struct documentation
- Cross-reference subagent-monitor.go example
.agents/skills/kit-extensions/SKILL.md:
- Update lifecycle event count (18 → 21)
- Add Subagent Events section with full handler documentation
- Add event struct definitions (SubagentStartEvent, SubagentChunkEvent,
SubagentEndEvent)
- Add 'Pattern: Monitoring Subagents with Widgets' complete example
with Yaegi-safe design notes
When a steer message is consumed mid-turn via PrepareStep, no new
SpinnerEvent{Show: true} fires within that turn, so the message was
stuck in pendingUserPrints indefinitely and never rendered.
Branch the SteerConsumedEvent handler on m.state:
- stateWorking (mid-turn): flush live stream content, then print the
steering user messages to scrollback immediately via drainScrollback.
- idle/post-turn: keep the existing pendingUserPrints deferral so the
SpinnerEvent{Show: true} for the next turn orders things correctly.
Models from the opencode provider (like claude-opus-4-6 and gpt-5.3-codex)
have provider overrides in the models database that specify different npm
packages than the provider's default. The code was ignoring these overrides
and routing all models through openaicompat, causing "bad request" errors.
Changes:
- Added Provider field to modelsDBModel to capture model-specific overrides
- Added ProviderNPM field to ModelInfo registry struct
- Updated autoRouteProvider() to check for model-specific provider overrides
- Fixed URL path handling for anthropic provider (strip /v1 suffix to avoid
double /v1/v1 paths when using third-party anthropic-compatible APIs)
Fixes routing for:
- opencode/claude-opus-4-6 -> @ai-sdk/anthropic
- opencode/gpt-5.3-codex -> @ai-sdk/openai
Previously, token usage and costs were only updated at the end of a complete
turn. For long-running multi-step tool-calling conversations, this meant the
status bar showed stale (or zero) costs during the entire interaction.
Now, after each complete step (tool call + result), the usage tracker is
updated with the actual token counts from that step. This provides real-time
cost accumulation visible in the status bar.
Changes:
- Add StepUsageHandler type and onStepUsage parameter to agent
- Emit StepUsageEvent from kit layer after each step completes
- Handle StepUsageEvent in app layer to update UsageTracker
- Add EventStepUsage constant and StepUsageEvent struct to events
The step usage is additive - each step's tokens are added to the running
session totals, just like the final turn usage was before.
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
Add width and count truncation to renderStreamingBashOutput to prevent
long-running commands from blowing up the TUI layout:
- Per-line width truncation via truncateLine() (ANSI-aware, matches final
bash tool renderer behavior)
- Display cap at maxBashLines (20) showing the tail (latest output)
- Truncation hint '...(N more lines above)' when lines are hidden
The buffer still accumulates up to 50 lines for context, but only the
last 20 are rendered during streaming. This is consistent with how the
final bash tool result is displayed.
Add maxLsLines (20) constant and truncate Ls output in the TUI to
prevent large directory listings from blowing up the layout. Shows a
'...(N more entries)' hint when truncated, consistent with all other
core tool renderers (Edit, Read, Write, Bash, Subagent).
- makeTheme() and fileConfigToTheme() now compute DiffInsertBg, DiffDeleteBg,
DiffEqualBg, DiffMissingBg, CodeBg, GutterBg, and WriteBg by blending the
theme's own Background with its Success/Error colors, so every theme gets
properly tinted diff backgrounds.
- Added color derivation helpers: parseHexColor, blendHex, deriveDiffBg.
- File-based themes still allow explicit diff color overrides; derived colors
are used only as fallbacks.
- formatToolParams() now skips body-content keys (content, old_text, new_text,
etc.) from the header line regardless of value length, preventing raw
unformatted code from appearing above the formatted body.
- Use process group isolation (Setpgid) so the entire process tree is
killed on timeout/cancellation, not just the direct child
- Set cmd.Cancel to kill the process group (-pgid) with SIGKILL
- Set cmd.WaitDelay (500ms grace period) to force-close pipes when
grandchild processes hold them open after the direct child exits
- Convert buffered path from cmd.Run() to explicit pipes + cmd.Start()
+ cmd.Wait() so WaitDelay can properly force-close pipe handles
- Reorder streaming path: cmd.Wait() before wg.Wait() so the WaitDelay
timer starts when the child exits, not after pipes close
- Add mutex for thread-safe chunk collection in streaming mode
- Add comprehensive tests for timeout, background processes, context
cancellation, and both buffered/streaming paths
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
Previously, pressing ESC twice to cancel rolled back the entire tree
session to the pre-turn state, discarding the user message, completed
tool call/result pairs, and any streamed response. Content that had
already rendered in the TUI would vanish from the session history.
Now the cancellation path uses the same logic as the non-cancellation
error path: the user message (already persisted before generation) and
any completed step messages (fully-paired tool_use + tool_result from
OnStepFinish) are preserved. Only the in-progress pending message or
tool call is discarded.
This ensures that if a message has rendered in the TUI, it stays in
the history and session.
Background agent that checks BTC/ETH prices every 30 minutes via the
CoinGecko API and sends desktop notifications through notify-send.
Demonstrates long-running autonomous agents with the Kit SDK.
Relocate SDK usage examples to the top-level examples directory alongside
extension examples for better discoverability. Add README for the SDK
examples directory.
- Update lifecycle events count from 18 to 20
- Add OnToolOutput event documentation for streaming tool output
- Add OnCustomEvent to lifecycle events list
- Fix go-edit-lint.go reference (it's project-local, not in examples/)
- Add neon-theme.go to examples list
- Add project-local extension example section
Add Background(theme.MutedBorder) to all text elements in reasoning blocks: contentStyle, hintStyle, and footer styles. Previously these only specified foreground colors, causing them to inherit the terminal's default background instead of matching the box background.
Extensions can now subscribe to streaming tool output events using
OnToolOutput(), giving them the same power as the internal TUI to
observe and react to tool execution in real-time.
Changes:
- Add ToolOutputEvent struct to extensions API
- Add ToolOutput constant to EventType
- Add OnToolOutput() handler registration method
- Add event bridging from kit to extensions runner
- Export ToolOutputEvent in Yaegi symbols
- Add OnToolOutput() to public SDK (pkg/kit)
Example usage in an extension:
api.OnToolOutput(func(e ext.ToolOutputEvent, ctx ext.Context) {
ctx.PrintInfo(fmt.Sprintf("%s: %s", e.ToolName, e.Chunk))
})
Display streaming bash output in the TUI stream region as it arrives.
Changes:
- Add streaming bash output rendering to renderStream()
- Style stdout with CodeBg, stderr with Error color
- Add streamingMu mutex for thread-safe buffer access
- Clear buffers on ToolResultEvent
- Add ToolOutputEvent to event system (pkg/kit, internal/app)
- Add ToolOutputHandler callback in agent
- Implement streaming mode in bash tool with pipes
- Add tests for accumulation and clearing
The streaming output appears in real-time below the LLM streaming text
while bash commands are executing, with proper synchronization to
prevent race conditions between Update and Render methods.
Running golangci-lint on a single file caused false positives due to
missing package context. Now it analyzes the entire package (./...)
while gopls still provides fast, targeted feedback on the edited file.
Replace bufio.Scanner with bufio.Reader in OpenTreeSession to avoid
64KB line length limit. Scanner silently truncates long lines which can
corrupt session data. Reader handles arbitrary line lengths properly.
- Add go-edit-lint extension that runs gopls and golangci-lint on Go file edits
- Show TUI PrintBlock only when diagnostics are found
- Add explicit prompt to LLM to fix issues when found
- Unignore .kit/extensions/ directory in .gitignore
- Color-coded borders: yellow for single tool issues, red for both
Instead of displaying 'Finished without output' or '(no output)' messages
when the LLM returns empty content, both RenderAssistantMessage functions
now return empty UIMessages with zero height. This removes confusing
placeholder messages from the scrollback.
Changes:
- internal/ui/messages.go: Return empty message when content is whitespace-only
- internal/ui/compact_renderer.go: Same behavior for compact mode
- Fix gofmt formatting in loader.go and loader_test.go
- Modernize strings.Index to strings.Cut (template.go:162,231)
- Use min() builtin instead of manual if check (template.go:273)
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
When the assistant returns empty or whitespace-only content after a tool
call, the TUI was showing a 'Finished without output' message block. This
was confusing because the tool output was already displayed.
Now we simply skip rendering the assistant message block entirely when
there's no meaningful content to display.
Changes:
- printAssistantMessage: check strings.TrimSpace(text) != instead of text !=
- ResponseCompleteEvent handler: same trim check before printing
When the user presses ESC twice to cancel during a tool call, the entire
turn is now rolled back instead of persisting partial progress. This
ensures that tool_use and tool_result messages are always sent to the API
as matched pairs, avoiding errors from orphaned tool calls.
Changes:
- Save pre-turn leaf ID before appending user messages
- On context cancellation (double-ESC), branch back to pre-turn leaf
- On other errors (API failures), still persist partial progress
- Update app.go comments to reflect new behavior
Two bugs fixed in internal/core/edit.go:
1. fuzzyMatch/mapFuzzyIndex returned wrong byte positions when trailing
whitespace was stripped during normalization. The old rune-counting
approach assumed 1:1 mapping between original and normalized strings,
but whitespace trimming changes string length. This caused the
replacement splice to cut at wrong boundaries, corrupting files.
Fix: replaced with normalizeWithMap() that builds an explicit
byte-position mapping during normalization. Also added ambiguity
guard — multiple fuzzy matches now return no-match instead of
silently picking the wrong one.
2. generateDiff marked the entire rest of the file as changed.
The old code used countNewlines(old[changeIdx:]) to compute the
diff range, which counted ALL newlines from the change point to EOF.
Fix: replaced hand-rolled diff with udiff.Unified() (already a
dependency) for correct standard unified diff output.
Added internal/core/edit_test.go with 33 tests covering fuzzyMatch
position mapping, normalizeWithMap correctness, generateDiff output,
and end-to-end executeEdit scenarios including corruption regression
tests.
Rework the compaction system with several improvements modelled after
pi's approach:
Compaction engine (internal/compaction):
- Tool result truncation: cap tool result text at 2000 chars during
serialisation to keep summarisation requests within token budgets
- Serialize tool calls and reasoning parts (previously only text)
- Split turn handling: when a single turn exceeds the keep budget,
summarise the turn prefix separately and merge with history summary
- Cumulative file tracking: extract read/modified files from tool calls
(read, write, edit, grep, find, ls) and carry forward across
compactions via PreviousCompaction parameter
- Add IsSplitTurn, findTurnStart helpers and CutPoint, ReadFiles,
ModifiedFiles fields to CompactionResult
Session tree (internal/session):
- New CompactionEntry type records summary, first-kept-entry-id, token
stats, and file lists without deleting old messages
- BuildContext skips entries before the compaction boundary and injects
the summary as a system message
- GetContextEntryIDs maps fantasy message indices to entry IDs for
cut-point resolution
- GetLastCompaction retrieves prior file tracking state
Non-destructive compaction (pkg/kit):
- Compact now appends a CompactionEntry instead of clearing and
rewriting the session — old messages remain on disk for history
- Extension hook (BeforeCompact) can now provide a custom Summary that
replaces the LLM-generated one, in addition to cancelling
UI (internal/ui):
- Tree selector renders CompactionEntry nodes with info styling
Events & hooks (pkg/kit):
- CompactionEvent includes ReadFiles and ModifiedFiles
- BeforeCompactResult gains Summary field for extension-provided summaries
- Bridge updated to forward custom summaries from extensions
The spawn_subagent tool was inheriting the parent's context deadline,
causing subagents to be killed prematurely (e.g. after ~120s instead of
the intended 5-minute default).
The parent LLM generation loop's context carries its own deadline which,
via Go's context.WithTimeout semantics (takes the minimum of parent
deadline and new timeout), would always win over the subagent's longer
timeout.
Add a detachedContext type that preserves context values (spawner func,
etc.) and propagates parent cancellation (Ctrl-C) but strips the
deadline. Applied only in the internal tool handler (executeSubagent) so
the public Kit.Subagent() SDK method continues to honor caller-provided
context deadlines.
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
Use ANSI-aware truncation (charmbracelet/x/ansi) to prevent ugly line
wrapping in all tool renderers (Bash, Read, Write, Ls, Edit, Subagent).
- Replace byte-length padRight/truncateLine with xansi.StringWidth and
xansi.Truncate which correctly handle ANSI escape codes and wide chars
- Bash: truncate lines to fit panel width instead of allowing 3x wrapping
- Read/Write: truncate syntax-highlighted lines before lipgloss renders
- Ls: truncate entries before styling
- Compact renderers: use ANSI-aware truncation consistently
A single very long line (e.g. minified JSON, base64 blob) could wrap
into hundreds of visual rows in the TUI even when within the line-count
and byte-count limits.
Core layer (truncate.go):
- Add defaultMaxLineLen (2000 chars) per-line cap
- Apply truncateLongLines() in both TruncateTail and truncateHead
before line/byte truncation
- Append '[N chars truncated]' marker to capped lines
UI layer:
- Cap lines in renderBashBody() to width*3 chars before rendering
- Cap lines in shell display handler (model.go) similarly
Add comprehensive tests in truncate_test.go.
- Add /share slash command that uploads session JSONL to GitHub Gist
via the gh CLI and prints a shareable viewer URL
- Add session viewer SPA at www/public/session/index.html served at
go-kit.dev/session/#GIST_ID
- Viewer supports all message types: text, reasoning/thinking blocks,
tool calls (bash, read, write, edit, grep, find, ls, spawn_subagent),
images, model changes, branch summaries, and labels
- Tool-specific rendering with syntax highlighting, diffs, collapsible
output, and status badges
- Also supports ?url= query param for loading from any JSONL URL
- Dark theme matching Kit brand colors
When a user resumes or imports a session, all conversation messages are
now rendered into the TUI scrollback buffer, giving visual context of
the prior conversation. This includes:
- User messages with original text
- Assistant responses with model name from the session
- Tool calls with name, args, and full output/error status
The implementation does a two-pass walk over the session branch:
1. Builds a toolCallID → {name, args} map from assistant messages
2. Renders each message entry using the existing print helpers
Wired into both the /resume session picker (SessionSelectedMsg handler)
and the /import command handler.
- Document theme persistence in themes.md and README.md
- Document session commands (/resume, /export, /import, /name) in commands.md and sessions.md
- Document prompt history (up/down arrows) in commands.md
- Document SubscribeSubagent API in sdk/callbacks.md and advanced/subagents.md
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 Kit.SubscribeSubagent(toolCallID, listener) which lets SDK consumers
opt into real-time events from LLM-initiated subagents. Listeners are
keyed by the spawn_subagent tool call ID, which is available in the
ToolCallEvent before the subagent starts.
The typical pattern is:
kit.OnToolCall(func(e kit.ToolCallEvent) {
if e.ToolName == "spawn_subagent" {
kit.SubscribeSubagent(e.ToolCallID, func(child kit.Event) {
// real-time subagent events
})
}
})
Implementation:
- Thread toolCallID through SubagentSpawnFunc so generate() knows which
tool call triggered the spawn
- Add subagentListenerSet (per-tool-call event bus) stored in a sync.Map
on the Kit struct, keyed by toolCallID
- In generate(), wire OnEvent to dispatch to registered listeners only
when SubscribeSubagent has been called for that tool call
- Listeners are cleaned up automatically when the subagent completes
- No listeners registered = no OnEvent callback = no overhead (the
default TUI path)
The core tool spawner in generate() was unconditionally setting OnEvent
to re-emit every child event onto the parent Kit's event bus. This caused
subagent tool calls, streaming text, reasoning, and responses to surface
in the TUI as if they were the parent's own events.
Remove the OnEvent callback from the core tool spawner. The spawn_subagent
tool is a blocking call that returns a summary result — it doesn't need
real-time event streaming. SDK consumers who need real-time subagent events
can call Kit.Subagent() directly with their own OnEvent callback. The
extension and ACP paths are unaffected as they bridge to their own
callbacks independently.
When the user's login shell is nushell, fish, or another non-bash shell,
the SHELL environment variable leaks through to child processes. This
causes tools like tmux to use the wrong shell for pane commands, leading
to failures (e.g. nushell rejects 'sleep 30' because it requires
'sleep 30sec').
Override SHELL to point to the resolved bash binary path in both the
bash tool (internal/core/bash.go) and the TUI shell command handler
(internal/ui/model.go) so child processes always use bash.
When pressing ESC twice to cancel an agent turn, completed tool calls
and their results were being discarded along with the in-progress text.
Only the streaming text should be discarded.
The root cause was a chain of two issues:
1. Agent layer (internal/agent/agent.go): Fantasy's Stream() returns
nil on error, discarding all accumulated step data. Fixed by tracking
completed step messages via the OnStepFinish callback and returning
a partial GenerateWithLoopResult alongside the error.
2. App layer (internal/app/app.go): The in-memory message store was
never synced from the tree session after cancellation. Fixed by
reloading the store from the tree session (which the SDK's runTurn
already persists partial progress to).
The existing partial-persistence code in pkg/kit/kit.go runTurn() was
correct but was dead code because the agent layer always returned nil
on error. It now receives the partial result and persists completed
step messages to the tree session as intended.
1. Batch queued messages into single agent turn
- Add PromptResultWithMessages() to SDK for batch submission
- Rewrite drainQueue() to collect all queued items and submit together
- This prevents the agent from processing queued messages sequentially
2. Persist tool messages on cancellation
- When generation is cancelled (double-ESC), persist any completed
tool calls and results to the session before returning
- Prevents the agent from re-doing work when user continues
Both issues caused the agent to lose context:
- Batching: Multiple queued messages now submitted as one turn
- Cancellation: Tool results from cancelled turns are preserved
Move the extension testing package from internal/extensions/test to
pkg/extensions/test to make it publicly importable by external extension
authors.
Changes:
- Moved test package files to pkg/extensions/test/
- Updated all imports from internal/ to pkg/ path:
- README.md
- examples/extensions/tool-logger_test.go
- examples/extensions/extension_test_template.go
- skills/kit-extensions/SKILL.md
- www/pages/extensions/testing.md
- pkg/extensions/test/README.md
- pkg/extensions/test/harness.go
The test package is now available for external import as:
github.com/mark3labs/kit/pkg/extensions/test
All tests pass with race detector.
Add missing PromptMultiSelect example to Interactive Prompts section.
Replace the minimal subprocess pattern with comprehensive SpawnSubagent
documentation including blocking/background modes, all SubagentConfig
fields, SubagentResult fields, SubagentEvent types, and handle methods.
Add Themes section to Context API reference with RegisterTheme,
SetTheme, ListThemes examples and ThemeColorConfig field reference.
Add Custom Theme with Slash Command pattern to Common Patterns.
Remove mistakenly committed .agents/skills copy.
Add Themes section to Context API reference with RegisterTheme,
SetTheme, ListThemes examples and ThemeColorConfig field reference.
Add Custom Theme with Slash Command pattern to Common Patterns.
Add comprehensive testing documentation to the kit-extensions skill:
- Add code example showing basic test structure with LoadFile(), Emit(), and assertions
- Document key testing patterns (LoadFile vs LoadString, event emission, assertions)
- List 25+ assertion helpers available in test package
- Reference tool-logger_test.go as complete example with 14 test cases
- Add link to internal/extensions/test/ in Key Files section
- Maintain existing CLI testing commands section
The skill now provides complete guidance for testing extensions alongside development.
Add comprehensive documentation for the extension testing package:
- README.md: Add 'Testing Extensions' section with basic usage and links
- www/pages/extensions/testing.md: Complete testing guide with:
- Overview and basic usage
- Common testing patterns (handlers, tools, widgets, etc.)
- Available assertions reference table
- Advanced usage (mock context access, multiple extensions)
- Best practices and limitations
- Links to complete examples
- www/pages/extensions/examples.md: Add test examples and template references
- www/pages/extensions/overview.md: Link to testing documentation
All documentation cross-references existing test files and examples.
Remove the test/ directory ignore rule from .gitignore since
there is no root-level test directory in the project.
The internal/extensions/test/ package remains tracked as expected.
Add comprehensive testing utilities for Kit extensions:
- internal/extensions/test/: New test package with:
- harness.go: Test harness for loading extensions into Yaegi
- mock.go: Mock context that records all context interactions
- assert.go: 20+ assertion helpers (AssertBlocked, AssertWidgetSet, etc.)
- harness_test.go: 18 comprehensive test examples
- README.md: Complete documentation with usage examples
- internal/extensions/test_api.go: Helper function for creating test API objects
- examples/extensions/tool-logger_test.go: 14 tests demonstrating real extension testing
- examples/extensions/extension_test_template.go: Copy-and-paste template for extension authors
- .gitignore: Allow internal/extensions/test/ directory
All 93 tests pass including race detector.
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.
Add a theme registry that discovers themes from three sources (in
precedence order): built-in presets, user dir (~/.config/kit/themes/),
and project-local dir (.kit/themes/). Later sources override earlier
ones with the same name.
Ship 22 built-in presets ported from the OpenCode theme collection:
amoled, ayu, catppuccin, dracula, everforest, flexoki, github,
gruvbox, kanagawa, kitt, material, matrix, monokai, nord, one-dark,
rose-pine, solarized, synthwave, tokyonight, vercel, vesper, zenburn.
Add /theme slash command with tab-completion for listing available
themes and switching at runtime. Also fix .yml extension handling
in config.FilepathOr.
Replace the Catppuccin color palette with a Knight Rider KITT-inspired
theme — scanner reds, amber dashboard glows, and dark cockpit tones.
No blues or bright greens; the entire palette stays in the warm
red/amber/gray family.
Unify the theme system by folding the standalone MarkdownTheme config
into the main Theme struct, eliminating the separate config path.
Replace all hardcoded lipgloss.Color() calls across input, overlay,
and CLI components with semantic theme references so every color
responds to theme customization.
Scaffold Tome docs site in www/ with 17 pages covering installation,
configuration, CLI reference, extensions, sessions, Go SDK, and advanced
usage. Custom Knight Rider theme (cipher base + red accent, dark mode,
Space Grotesk fonts). GitHub Pages workflow deploys via Bun on push to
master.
Shell commands (! and !!) now display the KITT spinner animation while
running, matching the behavior of LLM agent steps and compaction.
On shellCommandMsg: transition to stateWorking and start the spinner.
On shellCommandResultMsg: stop the spinner and return to stateInput.
The KITT spinner animation would sometimes run at 2x (or higher) speed
because multiple concurrent tick loops could accumulate in the Bubble Tea
command queue.
The problematic sequence: SpinnerEvent{Show:true} starts tick loop A,
SpinnerEvent{Show:false} sets spinning=false but tick A is still
in-flight, then ToolExecutionEvent restarts spinning and starts tick
loop B. When the stale tick A fires, spinning is true again so it
continues — now two loops advance spinnerFrame simultaneously.
Fix: add a generation counter (uint64) to StreamComponent. Each
streamSpinnerTickMsg carries the generation it was created for. The tick
handler only processes ticks matching the current generation — stale
ticks from previous start/stop cycles are silently discarded.
Generation is bumped on every spinner start, stop, and Reset() to
ensure at most one tick loop is ever active.
deriveExtensionName was using the full directory path (e.g.
examples/extensions/kit-telegram) to derive the display name for
main.go extensions, producing verbose names like 'Examples Extensions
Kit Telegram Extension'. Now uses filepath.Base(dir) so only the
immediate parent directory is used, giving 'Kit Telegram Extension'.
Also fix TestLoadExtensions_SkipsBadFiles which was flaky when
globally-installed git packages existed — isolate the test from the
host environment by overriding XDG_CONFIG_HOME, XDG_DATA_HOME, and
the working directory.
- Copy Telegram relay extension from ../kit-telegram
- Add README with quickstart, commands, API reference, and architecture
- Update examples/extensions/README.md with Integrations section and details
- Replace custom multi-select with huh.NewMultiSelect for kit install --select
- Replace raw bufio prompts in cmd/auth.go with huh.NewConfirm and huh.NewInput
- Fix extension discovery to use opinionated conventions (only scan root,
extensions/, ext/, examples/extensions/ directories, skip cmd/internal/pkg/)
- Fix loader to use same convention-based scanning for installed git repos
- Fix errcheck lint warning in loader.go
Prevent layout corruption and visual breakage when the terminal is
resized to narrow or short dimensions:
- Status bar: progressively drops middle/right sections instead of
wrapping to multiple lines (broke height calculation)
- Autocomplete popup: guard against negative widths, truncate names
before rendering to prevent text wrapping inside fixed-width columns,
adapt to name-only mode at very narrow widths
- Tree selector: use full height minus chrome instead of halved height,
guard against negative width in node truncation
- Model selector: rune-aware name truncation preserving provider tags,
width-adaptive entry rendering
- Overlay dialog: clamp dimensions to terminal bounds instead of using
fixed minimums that could exceed the terminal
- Input hint, popup footer, and all help text: tiered adaptive variants
for different terminal widths
- Queued messages: measure actual rendered height instead of fixed
5-line-per-message estimate
tea.Println inserts above BubbleTea's managed region, but after
StepCompleteEvent the previous response stays in the stream component
(managed region). Any subsequent print (tool results, shell commands,
slash output, errors) would appear above that response — out of order.
Introduce a scrollback buffer: all print helpers now buffer rendered
content via appendScrollback(). At the end of each Update cycle,
drainScrollback() combines everything into a single tea.Println. If
the stream component has unflushed content it is auto-prepended, so
new messages always appear below the previous assistant response.
Swap user/assistant border colors (user=blue, assistant=mauve), remove
per-message timestamps and username labels, simplify system messages to
borderless muted text with diamond prefix, change tool name color from
peach to blue, and redesign thinking blocks with surface background,
line truncation, and duration footer.
- Change newline keybinding from alt+enter to shift+enter across all
input components (main input, slash command input, prompt overlay)
- Skip markdown rendering for plain-text user messages so newlines are
preserved without extra paragraph spacing from glamour
- Fix scrollback ordering: defer queued user message printing to
SpinnerEvent where previous stream content is guaranteed complete,
combining flush + user message into a single tea.Println call
The spawner closure in generate() called m.Subagent() without setting
OnEvent, so child events (tool calls, text streaming, reasoning deltas)
were silently discarded. Wire OnEvent to re-emit on the parent's bus,
matching the behavior already present in the extension SpawnSubagent path.
If the requested model fails (bad name, unsupported provider), fall
back to the parent's model instead of returning a hard error. The
original prompt is prepended with a note so the agent knows which
model is actually running and can adjust future calls.
When spawn_subagent is called with a model name like 'claude-haiku'
(no provider prefix), prepend the parent's provider instead of letting
ParseModelString guess. Only full 'provider/model' strings bypass this.
Add SubagentEvent type to extension API and OnEvent field to
SubagentConfig so extensions can watch subagent tool calls, text
chunks, reasoning deltas, and turn lifecycle events in real time.
The SDK's Kit.Subagent() already had OnEvent via kit.SubagentConfig.
This wires it through to the extension layer with a concrete
SubagentEvent struct (Yaegi-safe) and bridges SDK events to it
in both cmd/root.go and the ACP server.
The subagent_session_id was already attached to the fantasy response
metadata by internal/core/subagent.go but ToolResultMetadata had no
field for it, so json.Unmarshal silently dropped it. Add the field
so SDK consumers can detect subagent tools and load their sessions.
Subagents now run as child Kit instances in the same process instead of
spawning a kit binary subprocess. This removes the binary dependency,
eliminates JSON serialization overhead, and enables SDK-only consumers
to use subagents without installing the kit CLI.
- Add Kit.Subagent() method for in-process subagent execution
- Add SubagentConfig/SubagentResult types to the SDK
- Add context-based SubagentSpawnFunc injection so core spawn_subagent
tool calls back to Kit.Subagent() without an import cycle
- Add SubagentTools() bundle (all core tools minus spawn_subagent)
- Add viperInitMu for thread-safe concurrent kit.New() calls
- Wire extension ctx.SpawnSubagent and ACP server to use in-process
- Child Kit gets parent's model as fallback, in-memory or persisted
session, and no extensions (preventing recursive loading)
Thread ToolCallID, ToolKind, ParsedArgs, FileDiff metadata, StopReason,
SessionID, and StructuredMessages across the SDK event bus, extension
wrapper, app bridge, hooks, and ACP server layers.
- Gap 1: ToolCallID from Fantasy's ToolCallContent threaded end-to-end
- Gap 2: ToolKind via static lookup (execute/edit/read/search/agent)
- Gap 3+4: FileDiffInfo with DiffBlocks via fantasy.ToolResponse.Metadata
- Gap 5: StopReason from Fantasy FinishReason on TurnEndEvent/TurnResult
- Gap 6: Subagent sessions now opt-out (NoSession); SessionID in JSON output
- Gap 7: GetStructuredMessages() returns typed ContentParts
- Gap 8: ParsedArgs map[string]any on tool events for convenience
Edit/write tools attach structured diff metadata. ACP server uses real
ToolCallIDs. Extension and SDK events kept in sync with matching fields.
Extensions were loaded but non-functional in ACP because
SetExtensionContext was never called. Wire a headless context with
no-op TUI stubs, functional data/model/tool APIs, and emit
SessionStart so extension lifecycle hooks fire during ACP sessions.
Adds an extension that starts language servers on demand and surfaces
diagnostics after file edits, following crush's LSP integration pattern.
Hooks into the edit tool lifecycle to diff pre/post diagnostics, display
a persistent widget, and expose lsp_diagnostics/lsp_hover tools plus
/lsp and /lsp-check slash commands.
Remove the early ValidateEnvironment gate from CreateProvider that only
checked env vars and --provider-api-key, blocking stored OAuth credentials
from working. Each provider creation function already handles its own auth
resolution with clear error messages.
Update ValidateEnvironment to also check stored Anthropic credentials so
the model selector UI correctly shows Anthropic models for OAuth users.
Add automatic token refresh in oauthTransport so long-lived ACP sessions
survive token renewals. Surface actionable auth error messages in ACP
session creation.
Fix pre-existing staticcheck SA5011 warnings in test files.
Streaming chunks now accumulate in a pending buffer and flush on a 16ms
tick (~60fps) instead of triggering a full markdown re-render on every
chunk. Between flushes, View() returns a cached string — no markdown
parsing, no lipgloss styling, no terminal escape sequence churn. This is
especially impactful for inline rendering (no alt screen) where each
frame requires cursor repositioning across the full view height.
- Mark read-only core tools as parallel-safe (read, grep, find, ls)
- Mark spawn_subagent as parallel-safe for concurrent task delegation
- Update UI to track multiple active tools during parallel execution
- Display 'Running: tool1, tool2, ...' in spinner for concurrent tools
- Add test for parallel tool execution scenarios
Fantasy already supports parallel execution via ToolInfo.Parallel field.
Tools marked parallel run concurrently (up to 5 at a time).
- Add custom renderer for spawn_subagent tool showing status + 3-line preview
- Pass toolArgs through ToolExecutionEvent to show task in spinner
- Display 'Subagent: <task>' during execution instead of generic message
- Compact mode shows concise one-line status summary
Implement 4-phase subagent system enabling LLM and extensions to spawn,
manage, and orchestrate child Kit instances for parallel task execution.
- Phase 1: SDK API with SpawnSubagent() for extensions
- Phase 2: spawn_subagent core tool for LLM usage
- Phase 3: Session hierarchy with ParentSessionID tracking
- Phase 4: Provider pooling for concurrent model access
New files:
- internal/extensions/subagent.go: SpawnSubagent implementation
- internal/core/subagent.go: Core tool definition
- internal/models/pool.go: Provider pool for concurrency
- examples/extensions/subagent-test.go: Test extension
- openspec/subagent-support.md: Design specification
Anthropic rejects requests with both temperature and top_p set.
When both are configured (typically from defaults), clear top_p
so temperature takes precedence.
splitPromptAndHistory was extracting only text from the last user
message, discarding FilePart data (clipboard images). The fix extracts
both text and file parts, passing files via AgentStreamCall.Files and
AgentCall.Files so Fantasy includes them in the API request.
Also preserves file parts when BeforeTurn hooks or skill expansion
replace the user message text in runTurn.
Add multimodal image support so users can paste clipboard images into
prompts alongside text. Images are read from the system clipboard via
platform-specific tools and sent as fantasy.FilePart to the LLM API.
- New internal/clipboard package with platform-specific image readers:
Linux: xclip (X11) with wl-paste (Wayland) fallback
macOS: osascript with AppKit NSPasteboard
Magic byte detection for PNG/JPEG/GIF/WebP/BMP/TIFF
- New ImageContent type in message model with full serialization and
Fantasy bridge support (ImageContent <-> fantasy.FilePart)
- InputComponent handles Ctrl+V (paste image), Ctrl+U (clear images),
shows attachment indicator, and carries images through submitMsg
- App layer queue upgraded from []string to []queueItem to carry files
alongside prompts through the drain loop
- Kit SDK gains PromptResultWithFiles() for multimodal user messages
- AppController interface extended with RunWithFiles()
Add extended thinking/reasoning support for Anthropic and OpenAI models:
- ThinkingLevel type (off/minimal/low/medium/high) with token budgets
- Stream reasoning deltas via OnReasoningDelta through SDK→TUI event pipeline
- Render thinking blocks in StreamComponent (muted italic, collapsible)
- ctrl+t toggles thinking visibility, shift+tab cycles thinking level
- /thinking slash command with tab-completion for level names
- --thinking-level CLI flag and config file support
- Map ThinkingLevel to OpenAI ReasoningEffort for Responses API
- Auto-bump Anthropic max_tokens when thinking budget exceeds it
- Fix ResponseCompleteEvent prematurely resetting stream in streaming mode
- Status bar displays current thinking level
Enable fantasy's Responses API path (WithUseResponsesAPI) for the OpenAI
provider so that models like gpt-5.3-codex, codex-mini-latest, o3, o4-mini,
and other Responses-only models work correctly.
- Enable WithUseResponsesAPI on both createOpenAIProvider and
createAutoRoutedOpenAIProvider
- Build provider options for reasoning models (reasoning_summary, encrypted
reasoning content) matching crush's coordinator behaviour
- Thread ProviderOptions from provider creation through to the fantasy agent
in NewAgent, SetModel, and the SDK Complete path
- Pass generation parameters (Temperature, MaxTokens, TopP, TopK) to the
fantasy agent for all providers (previously only Ollama)
- Fix extension tool schema for Responses API: parse Parameters JSON Schema
string into fantasy ToolInfo format, ensure Required is never nil (OpenAI
rejects null, expects empty array)
Add /model command that allows switching LLM models mid-session.
When invoked without arguments, opens a full-screen selector overlay
showing only models with configured API keys, with inline fuzzy search,
cursor navigation, and current model indicator. When invoked with an
argument (e.g. /model anthropic/claude-haiku-4-5), switches directly.
Also upgrades all Go dependencies to latest versions.
Match the tool result renderer behavior — show first 20 lines
with a '...(N more lines)' hint. Full output still goes to
context (with TruncateTail limits) for ! commands.
! runs a shell command with output included in LLM context.
!! runs a shell command with output excluded from LLM context.
Adds AddContextMessage to AppController for injecting messages
without triggering an LLM turn.
Drop the --prompt/-p flag entirely. Non-interactive mode is now
triggered by passing positional arguments:
kit "Explain this"
kit @file.go "Review this" --json
kit @a.go @b.go --quiet
Updated extension examples (kit-kit.go, subagent-widget.go) to pass
the prompt as a positional arg. Updated AGENTS.md and README.md.
Positional args are now the main way to run non-interactive mode:
kit "Explain this codebase"
kit @code.ts @test.ts "Review these files"
kit @go.mod "What module?" --quiet
--prompt is hidden but still works for subprocess compat (extensions
spawn kit with --prompt internally). Updated --quiet/--json/--no-exit
error messages to reference the new positional arg pattern.
Enables: kit @code.ts @test.ts "Review these files"
Positional args starting with @ are treated as file attachments —
their content is read and prepended to the prompt. Remaining
positional args are joined as the prompt text. Works alongside
--prompt flag (files prepended, extra text appended).
Type @ in the input to trigger a fuzzy file picker popup. Files are
discovered via git ls-files (with os.ReadDir fallback), scored by
fuzzy match, and displayed in the existing autocomplete popup.
Tab/Enter inserts the selected path; directories keep the popup open
for drilling. On submit, @file tokens are expanded into XML-wrapped
file content before being sent to the agent. No CWD restriction —
supports ~/, ../, and absolute paths.
Before-hook callbacks (OnBeforeSessionSwitch, OnBeforeFork) were called
synchronously inside BubbleTea's Update(), so extensions that used
blocking prompts (ctx.PromptConfirm) would deadlock — the channel read
waited for Update() to process the PromptRequestEvent, but Update()
was blocked on that same channel read.
Run hooks in dedicated goroutines and deliver results via SendEvent,
matching the pattern already used by extension slash commands.
- ctx.SuspendTUI(callback): releases terminal for interactive subprocesses
(vim, shell, htop), automatically restores TUI when callback returns.
Uses BubbleTea v2 ReleaseTerminal/RestoreTerminal.
- api.RegisterMessageRenderer(config) + ctx.RenderMessage(name, content):
named render functions for branded/styled extension output. Renderers
receive content and terminal width, return ANSI-styled strings.
- ctx.ReloadExtensions(): hot-reloads all extensions from disk. Emits
SessionShutdown to old extensions, reloads source, emits SessionStart
to new. Event handlers, commands, renderers, shortcuts update immediately.
TUI command list refreshes via WidgetUpdateEvent. Extension tools are
NOT updated (baked into agent at creation, documented limitation).
New example extensions: interactive-shell.go, branded-output.go, dev-reload.go
- RegisterShortcut(ShortcutDef, handler) for global keyboard shortcuts
that fire across all non-modal app states (after ctrl+c, before
component dispatch). Handlers run in goroutines for safe blocking calls.
- ToolContext with IsCancelled/OnProgress for rich tool execution;
ExecuteWithContext on ToolDef takes priority over simple Execute.
- Source field on ToolCallEvent (currently "llm", forward-compatible
with future user-initiated tool calls).
- Fix missing //go:build ignore on context-inject.go.
- Update plan-mode.go to register ctrl+alt+p shortcut.
Add three new extension events that allow extensions to gate destructive
session operations and compaction:
- OnBeforeFork: fires before branching in the tree selector; handler can
cancel with reason (e.g. dirty-repo guard)
- OnBeforeSessionSwitch: fires before /new resets the session branch;
handler can cancel with reason
- OnBeforeCompact: fires before context compaction (auto or manual);
handler receives token stats and IsAutomatic flag, can cancel
Includes SDK hook registry (beforeCompact), extension bridge, UI
callbacks threaded through AppModelOptions, and two example extensions:
- confirm-destructive.go: git dirty check + fork confirmation
- compact-notify.go: compaction notification + auto-compact gating
Extensions can now register an OnContextPrepare handler that fires after
the context window is built from the session tree and before messages are
sent to the LLM. Handlers receive ContextMessage entries with positional
indices and can filter, reorder, or inject messages. Original messages
referenced by index preserve tool calls, reasoning, and other complex
parts. New context-inject example extension demonstrates injecting a
local .kit/context.md file as an ephemeral system message every turn.
Extensions can now provide a Complete function on CommandDef that supplies
argument suggestions. When the user types a command name followed by a space,
the input popup switches to argument-completion mode, calling Complete with
the partial text and displaying matching suggestions.
Implement Phase 1 extension API gaps identified in the pi-mono gap analysis:
- Gap 1: Session Management API (GetMessages, GetSessionPath) — read-only
access to conversation history from extensions
- Gap 2: Session Persistence (AppendEntry, GetEntries) — custom extension
data survives across session restarts via new ExtensionDataEntry type
- Gap 10: SetEditorText — extensions can pre-fill the input editor
- Gap M3: Keyed Status Bar (SetStatus, RemoveStatus) — multiple extensions
can place independent entries in the TUI status bar, ordered by priority
Add a --json flag that outputs structured JSON (response, model, usage,
messages with typed parts) when used with --prompt. Update kit-kit and
subagent-widget extensions to use --json for cleaner subprocess output
parsing instead of raw text heuristics.
- Add ctx.SetUIVisibility() to toggle built-in TUI chrome (startup
message, status bar, separator, input hint) from extensions
- Add ctx.GetContextStats() returning accurate API-reported token counts
instead of text-based heuristic; fix event ordering so extension
handlers see up-to-date conversation state
- Add compact tool body renderers for compact mode: Read/Edit/Write/Ls
show one-line summaries, Bash shows first 3 lines instead of full
20-line syntax-highlighted output
- Add minimal.go example extension using UIVisibility + GetContextStats
Ls output is a plain file list with no line numbers, so the empty
gutter column was wasted space. Give ls its own renderer that shows
a clean list with just the code background.
Port of Pi Pi (meta-agent with parallel expert subprocesses) to Kit's
extension system. Includes expert grid widget, query_experts tool,
custom footer, tool renderer, and orchestrator system prompt injection.
Also updates AGENTS.md with Yaegi gotchas, BubbleTea patterns, testing
recipes, and extension architecture notes.
Fixes golangci-lint issues: modernize min/max in overlay.go, replace
deprecated GetExtRunner() with new GetExtensionContext() SDK method,
remove broken --model flag from expert subprocess.
The interceptor now stays installed for the entire vim session, handling
both normal and insert modes. Esc switches from insert back to normal,
and /vim toggles the entire interceptor on/off.
Extensions can now intercept key events and wrap the editor's rendered
output via ctx.SetEditor/ctx.ResetEditor, enabling vim-like modal
editing, custom key bindings, and visual decorators.
Key fixes during development:
- Yaegi requires closure wrappers for struct function fields (bare
function references return zero values across the interpreter boundary)
- SetEditor/ResetEditor use async NotifyWidgetUpdate to avoid deadlocking
BubbleTea's event loop when called from HandleKey callbacks
- distributeHeight now uses renderInput() to account for interceptor
Render wrapper in height calculations
Add ctx.ShowOverlay() API that displays modal dialogs with optional
scrollable content, markdown rendering, action buttons, and configurable
positioning. Follows the same channel-based blocking pattern as prompts,
with full Yaegi compatibility via concrete structs.
Extensions can now override how tool calls are displayed in the TUI via
API.RegisterToolRenderer(). Supports custom display name, border color,
background color, header parameter formatting, body rendering, and
optional markdown processing of custom body output.
Extensions can now place persistent header (above stream) and footer
(below status bar) regions via ctx.SetHeader/SetFooter. Single-instance
per slot, reuses WidgetContent/WidgetStyle types and WidgetUpdateEvent
for notifications. Includes thread-safe Runner storage, SDK methods,
UI rendering with height distribution, and example extension.
Extensions can now show modal prompts to the user via ctx.PromptSelect,
ctx.PromptConfirm, and ctx.PromptInput. Prompts render inline below the
separator (replacing the input area) and use channel-based sync so the
extension blocks until the user responds. Extension slash commands run in
dedicated goroutines to avoid stalling BubbleTea's Cmd scheduler.
Add a declarative widget system that lets extensions place persistent
content above or below the input area. Widgets survive across agent
turns and are updated via ctx.SetWidget/ctx.RemoveWidget from any
event handler.
All types are concrete structs (Yaegi-safe, no interfaces cross the
interpreter boundary). Widget state lives on the Runner with mutex
protection, and WidgetUpdateEvent triggers BubbleTea re-renders.
Issue ClearScreen after tea.Println in flushStreamContent() to force a
full terminal redraw when the view height shrinks. Mirrors pi's
clearOnShrink mechanism — bubbletea's inline renderer doesn't clear
rows below the managed region when the frame gets shorter.
On step completion, keep stream content visible in the view instead of
immediately flushing to scrollback and resetting. The flush+reset in the
same frame shrinks the view height, and bubbletea's inline renderer
leaves orphaned blank lines at the bottom for the rows it no longer
manages.
The deferred flush happens when the next step starts (SpinnerEvent),
right before new content begins streaming.
Merge Context, Skills, and tool counts into one KIT System block
instead of separate styled sections. Add separate MCP and extension
tool counts to Agent, only displaying each when > 0.
- Add default system prompt (Pi-style) listing core tools and guidelines
- Load AGENTS.md into system prompt as project context if present
- Discover skills from .agents/skills/ (standardized) and .kit/skills/
- Change FormatForPrompt to metadata-only XML (agent reads on demand)
- Compose system prompt: base + AGENTS.md + skills metadata + date/cwd
- Show [Context] and [Skills] sections in startup banner
- Add /skill:name command expansion in runTurn (re-reads file, wraps in <skill> block)
- Add /skill:name to /help with available skill names
The app layer now exclusively uses the SDK path (kit.PromptResult).
- Remove AgentRunner interface and Agent field from Options
- Remove executeStepLegacy and updateUsage (SDK handles both)
- Remove Extensions field from Options (SDK owns extension lifecycle)
- Simplify StepCompleteEvent to carry ResponseText string only
- Add PromptFunc test hook to Options for stub-based testing
- Remove dead wrappers from cmd/setup.go (SetupAgent, BuildProviderConfig)
- Update all tests to use stubPrompt/PromptFunc instead of stubAgent
Delete the entire scripting feature (cmd/script.go, tests, examples/scripts/,
examples/hooks/) and clean up all supporting code: ArgsSubstituter, MergeConfigs,
Config.Prompt/NoExit fields, scriptMCPConfig, and HasScriptArgs. Env substitution
(EnvSubstituter, HasEnvVars) is retained as it's used by config loading and hooks.
-2171 lines across 21 files.
cmd/root.go now creates a *Kit via kit.New() instead of manually calling
SetupAgent/InitTreeSession. The app's executeStep() delegates to
kit.PromptResult() which handles session persistence, hooks, extension
events, auto-compaction, and the generation loop — eliminating 150+ lines
of duplicated orchestration.
Key changes:
- Add MCPConfig, ShowSpinner, SpinnerFunc, UseBufferedLogger, Debug to
kit.Options so the CLI can pass through its flags
- Add TurnResult struct and PromptResult() method for rich return data
(usage stats, conversation messages)
- Bridge extension observation events (AgentStart/End, MessageStart/
Update/End) through SDK EventBus so extensions work without app-layer
emission
- Add subscribeSDKEvents() in app layer to convert SDK events to tea.Msg
for TUI rendering
- Kit.Close() emits SessionShutdown and closes tree session
- Legacy executeStepLegacy() retained for test stubs and script mode
Delete legacy session files (manager.go, session.go) and unused
ParseModelName() — all orphaned after Plans 00-09. Add Plan 10 to
close all deferred items: app uses kit.New(), executeStep() delegates
to kit.PromptResult(), extension observation events route through SDK
EventBus.
- Add ToolOption/WithWorkDir functional options pattern to internal/core
- Update all 7 tool constructors to accept ...ToolOption and resolve
paths relative to the configured working directory
- Create pkg/kit/tools.go with public exports: individual constructors,
bundles (AllTools, CodingTools, ReadOnlyTools), and WithWorkDir
- Add CoreTools field to AgentConfig/AgentCreationOptions so callers
can inject custom tool sets instead of hardcoding core.AllTools()
- Add Tools field to kit.Options and GetTools() to kit.Kit
- Fully backward compatible: no-arg calls use os.Getwd() as before
Restructure the codebase so the CLI app consumes the SDK rather than
the SDK wrapping CLI internals. This eliminates the circular dependency
(sdk -> cmd -> sdk) and establishes pkg/kit as the canonical API.
Key changes:
- Create pkg/kit/ with InitConfig, SetupAgent, BuildProviderConfig
extracted from cmd/root.go and cmd/setup.go as parameterized functions
- Move sdk/kit.go -> pkg/kit/kit.go (remove cmd import, use local calls)
- Move sdk/types.go -> pkg/kit/types.go
- Move main.go -> cmd/kit/main.go (standard Go project layout)
- cmd/root.go and cmd/setup.go now delegate to pkg/kit, injecting
CLI-specific state (quietFlag) via the Quiet field on AgentSetupOptions
- Add setSDKDefaults() for cobra-free SDK usage (viper defaults)
- Fix .gitignore: kit -> /kit (was blocking cmd/kit/ and pkg/kit/)
- Update .goreleaser.yaml, Taskfile.yml, AGENTS.md, contribute/build.sh,
README.md for new cmd/kit entrypoint and pkg/kit import paths
- Add plans/ with 10 detailed SDK revamp plans and Taskfile.yml
- Delete sdk/ directory entirely
os/exec lives in yaegi's stdlib/unrestricted package, not the default
stdlib.Symbols. Without it, extensions importing os/exec fail with
'unable to find source'. This is needed for the /run command example
and any extension that spawns subprocesses.
SendMessage lets extensions inject messages into the conversation and
trigger new agent turns, enabling async patterns like background
subagent execution. It delegates to app.Run() which handles queueing.
CommandDef.Execute now receives Context so commands can access
SendMessage, Print*, and session metadata. The UI layer wraps the
call via runner.GetContext() at the boundary.
Also fixes all 20+ golangci-lint issues across the codebase:
errcheck, modernize (min/max/slices.Contains/SplitSeq/range-over-int),
staticcheck (error string casing), and unused code removal.
Extension-registered slash commands are now fully end-to-end:
- Commands appear in autocomplete popup (category: Extensions)
- Commands appear in /help under Extensions section
- Commands dispatch via handleExtensionCommand with argument support
- Command names normalized to /prefix at the cmd layer boundary
Extension-registered tools now show in /tools:
- Agent.GetTools() includes extraTools from extensions
- Previously only core + MCP tools were returned
Also adds RegisterTool and RegisterCommand examples to the
kit extensions init template, and adds .kit/ to .gitignore.
Implement pi-style JSONL append-only session management with tree branching:
- TreeManager with id/parent_id tree structure, leaf pointer, and context
building that walks leaf-to-root for LLM messages
- Auto-discovery by cwd in ~/.kit/sessions/ with session listing
- /tree TUI overlay with ASCII art rendering, filter modes, and navigation
- /fork, /new, /name, /session slash commands for tree operations
- --continue, --resume, --no-session CLI flags
- Default auto-creates a tree session per working directory
Core tools previously rendered two separate blocks: an 'Executing tool'
header block on ToolCallStartedEvent, then a separate result block on
ToolResultEvent. This merges them into a single unified block that
renders only when the result arrives.
The unified block shows a status icon (checkmark/cross), a friendly
tool display name, inline parameters, and the output body. Border
color indicates success (green) or error (red).
Remove the entire internal/builtin package (bash, fetch, todo, http, fs
servers) and all inprocess/builtin transport support from config and
connection pool.
Add internal/core package with 7 direct fantasy.AgentTool implementations
matching pi's coding agent: bash, read, write, edit, grep, find, ls.
These execute in-process with zero MCP/JSON serialization overhead.
Add internal/message package with crush-inspired custom content blocks:
ContentPart interface with TextContent, ReasoningContent, ToolCall,
ToolResult, and Finish types. Messages carry heterogeneous Parts slices
with type-tagged JSON serialization for persistence and a ToFantasyMessages
bridge for LLM provider integration.
Core tools are always registered on the agent. External MCP servers remain
supported for additional tools, but MCP loading failures are now non-fatal
since core tools guarantee a working baseline.
Extract shared functions into cmd/setup.go (BuildProviderConfig, SetupAgent,
BuildAppOptions, CollectAgentMetadata, DisplayDebugConfig, SetupCLIForNonInteractive)
eliminating triplicated config/agent/app assembly from root.go, script.go, and
the SDK.
Move the event handler from cmd/script.go into internal/ui/event_handler.go as
CLIEventHandler, shared by both script and --prompt modes. Fix streaming in
non-interactive modes: chunks are now printed to stdout immediately as they
arrive (fmt.Print) instead of being silently buffered until step completion.
The --prompt path switches from RunOnce (no intermediate display) to
RunOnceWithDisplay with CLIEventHandler, gaining streaming, tool call display,
and spinner support that was previously exclusive to script mode.
- Fix context percentage: use FinalResponse.Usage (last API call) instead of
TotalUsage (sum of all tool-calling steps) which overstated context fill level
- Fix token count: display current context window tokens, not cumulative session
total, so the number and percentage tell a consistent story
- Fix script mode double-counting: app.updateUsage already updates the shared
tracker before sending StepCompleteEvent, so remove redundant
UpdateUsageFromResponse call
- Add sticky usage display in TUI: render in View() layout between stream and
separator instead of tea.Println so it updates in place
- Add usage display for non-interactive --prompt mode (non-quiet)
- Add SetContextTokens to UsageUpdater interface for separating billing tokens
(TotalUsage) from context utilization (FinalResponse.Usage)
Tool results were displayed as nested JSON (Fantasy wrapper + MCP content
structure). Now extractToolResultText unwraps both layers at the source,
so all UI paths receive clean text. Removes the redundant extraction in
script mode since the agent layer handles it.
- Accumulate stream chunks in a buffer and flush through
DisplayAssistantMessageWithModel at boundaries (tool calls, step
complete), mirroring the TUI's StreamComponent accumulate-and-flush
strategy. Text accompanying tool calls now renders identically to
solo assistant responses.
- Fix example-script.sh: add missing --- frontmatter delimiters and
convert legacy command/args format to new type+command list format
so Viper YAML parsing works correctly.
- Fix env-substitution-script.sh: add missing execute permission.
Script mode had a duplicated agentic loop (runAgenticLoop/runAgenticStep)
that was copied from root.go during the Bubble Tea refactor but left with
broken streaming display and missing hooks integration. The streaming
callback silently accumulated chunks without rendering, and the final
response was skipped because it assumed streaming had already shown it.
- Refactor app.executeStep to accept a generic eventFn callback instead
of a *tea.Program, decoupling the agent step from Bubble Tea
- Add app.RunOnceWithDisplay for non-TUI callers that need intermediate
display events (spinner, tool calls, streaming chunks)
- Replace ~300 lines of duplicated code in script.go with a lightweight
scriptEventHandler that routes app events to CLI display methods
- Fix agent.GenerateWithLoopAndStreaming to use the streaming path when
any callbacks are provided (fantasy only exposes callbacks on Stream)
- Fix CLI displayContainer to match TUI output (remove extra padding)
- Remove premature usage display during CLI setup
Streaming callbacks were not checking ctx.Err(), so the fantasy library
kept processing after the user cancelled. Additionally, context.Canceled
was surfaced as a StepErrorEvent, printing an error instead of silently
ending the turn.
Add ctx.Err() checks to all streaming callbacks so the fantasy stream
loop breaks immediately on cancel. Introduce StepCancelledEvent so the
TUI flushes partial content and returns to input without an error message.
Use three-phase rendering for blocks with background color:
1) content with bg + horizontal padding, 2) Place() for vertical
padding with WithWhitespaceStyle, 3) border applied last so it
extends the full block height. Pass background color through to
glamour markdown renderer so inline text inherits the highlight.
Switch Highlight to Catppuccin Mantle for a subtler recessed look.
Lipgloss PaddingTop/PaddingBottom adds raw newlines that don't receive
background color styling. Prepend/append newlines to the content string
instead so they are part of the styled content and get the background.
All content blocks now span the full container width by default.
Removed PlaceHorizontal floating so user and agent messages no longer
stagger left/right. The align option now only determines which border
gets the accent color (left for agent, right for user).
Remove right padding from content blocks and align input components
with message content by matching the border(1) + paddingLeft(2)
pattern used by renderContentBlock.
Replace inline text+badge with proper bordered content blocks matching
the overall message styling. Each queued message is rendered with a
right-aligned block, muted border, and a QUEUED badge below the text.
Modernize range-over-int loops, use tagged switch, replace min/max
if-blocks with builtins, remove unused funcs/fields, delete dead
streaming_display.go, and fix ineffectual assignment in tests.
Spinner now uses theme.Primary/Muted/VeryMuted/MutedBorder instead of
hardcoded red. Removed 'Thinking...' label and message parameter from
NewSpinner/ShowSpinner/SpinnerFunc. Spinner keeps running alongside
streaming text and only hides on step complete via Reset().
Fix a deadlock where submitting a message while the agent is streaming
locks the app. App.Run() and App.ClearQueue() were calling prog.Send()
synchronously from within Bubble Tea's Update() loop, blocking when the
internal event channel was full from streaming events.
Run() now returns the queue depth instead of sending events, and
ClearQueue() no longer sends events. The UI layer updates state directly.
Additionally, queued messages are now rendered with a "queued" badge
between the separator and input, anchored in the managed region until
the agent picks them up. Previously they were printed to scrollback
immediately and only a count badge was shown.
tea.Println from Init() gets pushed above the fold because the View
fills the terminal immediately. Print startup info to stdout before
program.Run() so messages are visible when the TUI appears.
tea.Batch runs commands concurrently, so multiple tea.Println calls
would either race (dropping messages) or render on one line. Use
tea.Sequence to emit each startup message one at a time.
Multiple tea.Println commands in a tea.Batch can race, causing the
model-loaded message to be dropped. Combine all startup info into a
single system message so only one tea.Println is needed.
The Bubble Tea refactor stopped calling SetupCLI for interactive mode,
which dropped the model-loaded, loading-message, and tool-count info
that used to appear on startup. Emit those messages from AppModel.Init()
via tea.Println to match the old behaviour.
The Bubble Tea refactor only wired /quit, /clear, and /clear-queue in
InputComponent; the remaining commands (/help, /tools, /servers, /usage,
/reset-usage) fell through as submitMsg and were forwarded to app.Run()
as regular prompts.
Intercept all recognized slash commands in AppModel.Update before they
reach the app layer, and add print helpers that emit formatted output
via tea.Println. Also create a UsageTracker for interactive mode so
/usage and /reset-usage work correctly.
Strip the three-layer permission system (hook-based blocking, user approval
dialogs, PostToolUse output suppression) so every tool call executes
unconditionally. This removes ~1100 lines across 14 files including the
--approve-tool-run and --no-hooks CLI flags, ToolApprovalHandler/Func types,
PreToolUse/UserPromptSubmit/PostToolUse/Stop hook firing, HookBlockedEvent,
ToolApprovalNeededEvent, the ApprovalComponent UI, and all associated tests.
The bubbletea refactor was accumulating all messages in StreamComponent
and only printing Response.Content.Text() on step completion, causing
user messages, tool calls, and tool results to be missing from output.
Now only agent streaming text stays live in StreamComponent. Everything
else is printed immediately to scrollback:
- submitMsg: prints rendered user message
- ToolCallStartedEvent: flushes stream text, prints tool call
- ToolResultEvent: prints tool result, restarts spinner
- HookBlockedEvent: prints blocked notice
- ResponseCompleteEvent: prints assistant text (non-streaming mode)
- StepCompleteEvent: flushes remaining stream text
Remove methods that belonged to the old interactive loop and have no
external callers: HandleSlashCommand, SlashCommandResult, IsSlashCommand,
DisplayHelp, DisplayTools, DisplayServers, ClearMessages, UpdateUsage,
DisplayUsageStats, ResetUsageStats. Slash command dispatch is now owned
by InputComponent in the Bubble Tea TUI; usage tracking is handled
directly by the app layer.
Retain all display methods still needed by non-interactive paths
(cmd/root.go session replay, cmd/script.go, factory.go). Also drop the
now-unused "strings" import. cli.go shrinks from 509 to 329 lines.
Introduce AgentRunner interface in options.go so tests can supply stub
agents without a real LLM (backward-compatible; *agent.Agent satisfies it).
Fix three pre-existing deadlock bugs in app.go where sendEvent() was called
while a.mu was held (Run, ClearQueue, drainQueue); the sendEvent comment
explicitly forbids this.
Add app_test.go with 15 tests covering: single/queued Run, FIFO drain
ordering, CancelCurrentStep, cancel-during-approval (ctx unblocks
ToolApprovalFunc), ClearQueue, Close (WaitGroup, idempotent, root-ctx
cancel), step error recovery, ClearMessages, and QueueLength.
- Move knightRiderFrames() from spinner.go to stream.go (its natural owner
as StreamComponent is now the primary TUI inline spinner)
- Remove unused dotFrames, dotFPS vars and NewThemedSpinner() constructor
- Remove color field from Spinner struct and simplify run() accordingly
- spinner.go shrinks from 150 to 76 lines; Spinner struct retained for
script.go and cli.go non-TUI paths
Remove runAgenticStep, runAgenticLoop, runInteractiveLoop, addMessagesToHistory,
replaceMessagesHistory, AgenticLoopConfig, executeStopHook, runNonInteractiveMode,
and runInteractiveMode from root.go. Functions still needed by the script command
are moved to script.go.
Delete GetPrompt, StartStreamingMessage, UpdateStreamingMessage,
GetToolApproval, and finishStreaming from CLI — these each spun up
their own tea.NewProgram and are superseded by the unified AppModel
TUI. Also remove the streamProgram/streamDone fields they relied on.
Update dead-code stubs in cmd/root.go to keep the build clean until
the legacy agentic-loop functions are deleted in TAS-28.
Add SetHeight() to StreamComponent and wire distributeHeight() to call
it so the stream region is clamped to the available terminal rows after
subtracting the fixed separator (1) and input (5) lines. Also propagate
initial dimensions at NewAppModel construction time.
After StepCompleteEvent resets state to stateInput, the next queued step fires
SpinnerEvent{Show: true} before the first output arrives. Without this guard the
parent stayed in stateInput while the agent was running. Now any SpinnerEvent
with Show=true drives the state machine back to stateWorking, ensuring the TUI
correctly reflects the active state for both fresh and queued steps.
Add a shutdown guard in drainQueue() that checks a.closed and
rootCtx.Err() before dequeuing the next prompt. Previously, calling
Close() mid-drain would cancel the root context but the loop would
still attempt to run subsequent queued prompts against that cancelled
context. Now the loop clears the queue and exits cleanly on shutdown.
Add UsageUpdater interface to app/options.go to avoid circular imports,
wire *ui.UsageTracker via Options.UsageTracker in cmd/root.go, and call
updateUsage() after each successful step in both runPrompt() and RunOnce().
executeStep() now accepts a prompt string instead of a pre-built message slice.
It calls store.Add() for the user message before the agent runs (persisting it
immediately, even on cancellation), builds the full msgs slice via store.GetAll(),
and calls store.Replace() on success with the full updated conversation.
runPrompt() and RunOnce() are simplified accordingly — all history mutations
are now owned by executeStep(), the single code path for both interactive and
non-interactive execution.
Fire UserPromptSubmit in Run() before queuing, PreToolUse/PostToolUse in
executeStep() callbacks, and Stop hook in runPrompt()/RunOnce() after each
step. Emit HookBlockedEvent when a hook blocks a prompt or tool call.
SetupCLI() and its associated debug/CLI display logic now only run when
--prompt is set (non-interactive mode). Interactive mode uses the Bubble
Tea AppModel exclusively and never allocates a legacy CLI.
- Move SetupCLI call inside promptFlag != "" guard
- Remove cli parameter from runInteractiveModeBubbleTea (was unused _)
- Remove cli parameter from runNonInteractiveModeApp (no longer needed)
- Session history display remains guarded by cli != nil, so it correctly
skips in interactive mode
Replace the legacy runNonInteractiveMode path (which used runAgenticLoop
and the old CLI spinner/streaming display) with runNonInteractiveModeApp,
which delegates directly to appInstance.RunOnce(ctx, prompt, os.Stdout).
RunOnce never creates a tea.Program, so no intermediate spinner or
tool-call output is produced — satisfying both the normal and --quiet
non-interactive cases with the same codepath.
When --no-exit is set, runNonInteractiveModeApp hands off to the
interactive BubbleTea TUI via runInteractiveModeBubbleTea after the
single step completes.
Replace the old SetupCLI/runInteractiveLoop flow with a single
tea.NewProgram(AppModel) + appInstance.SetProgram() call. NewAppModel
now constructs InputComponent and StreamComponent inline so the parent
model is fully wired on construction.
TAS-18: Implements internal/app/app.go with the App orchestrator that satisfies
the ui.AppController interface. Wires all 7 agent callbacks to program.Send()
events, handles interactive approval via ToolApprovalNeededEvent channel, and
drains the prompt queue sequentially in a single background goroutine.
Refactors runNormalMode() in cmd/root.go to construct app.New() after session
messages are loaded, defer appInstance.Close(), and wire ToolApprovalFunc per
mode (AutoApproveFunc for non-interactive, nil for interactive TUI path).
Wraps []fantasy.Message with Add, Replace, GetAll, Clear, and Len methods.
Bridges to session.Manager for best-effort on-disk persistence on every mutation.
Refactors tool approval from the standalone ToolApprovalInput (which called
tea.Quit) into ApprovalComponent that returns approvalResultMsg{Approved: bool}
via a tea.Cmd, letting AppModel own the program lifecycle. Wires the
ToolApprovalNeededEvent handler in model.go to construct and display the
component.
Implements TAS-16: refactors the stream display into a self-contained
StreamComponent with a phase state machine (idle/spinner/streaming).
Reuses knightRiderFrames() for the KITT-style spinner, accumulates
streaming chunks, and renders tool call/result events inline.
Reset() clears all state between agent steps.
Replaces the standalone SlashCommandInput quit-on-submit pattern with a
proper child component (InputComponent) that returns submitMsg as a tea.Cmd.
Slash commands are executed against AppController (/clear, /clear-queue) or
return tea.Quit (/quit) — no os.Exit(). Also registers /clear-queue in the
slash command registry.
Handle /quit slash command (and aliases /q, /exit) in submitMsg handler by
returning tea.Quit before forwarding to the app layer. Add TODO comment in
cmd/root.go marking where defer appInstance.Close() belongs once app.App
exists (TAS-18).
Render step errors above the BT region via tea.Println() using the
existing RenderErrorMessage() renderers, mirroring the StepCompleteEvent
pattern. Reset stream state and transition back to stateInput on error.
Introduce the root Bubble Tea model (AppModel) and the three TUI-internal
message types needed by the unified architecture spec (TAS-10).
- internal/ui/events.go: submitMsg, approvalResultMsg, cancelTimerExpiredMsg
- internal/ui/model.go: AppModel with full stateInput/stateWorking/stateApproval
state machine, AppController interface for dependency inversion, child
component interfaces as stubs (TAS-15/16/17), double-tap ESC cancel with 2s
timer, routing of all 13 app-layer events, tea.Println on StepCompleteEvent,
stacked View() with separator and queue badge, WindowSizeMsg propagation.
Define all 13 event structs in internal/app/events.go that the app layer
sends to the TUI via program.Send(), including streaming, tool call, approval,
spinner, queue, and error events.
Replace micro-program pattern (3 separate tea.NewProgram calls) with a
single persistent program using child model composition. Introduces thick
app layer for agent orchestration, message queueing, and double-tap ESC
cancellation.
Drop the points spinner and message label in favor of a Knight Rider
scanner that bounces a red glow across 8 small squares. Frames are
pre-rendered with a 3-level intensity trail (#FF0000, #990000, #440000)
at 14 FPS.
Streaming text was flickering because displayContainer() used raw escape
sequences (\033[%dF) to move the cursor and fmt.Println to repaint on
every token. This bypassed Bubble Tea's synchronized output, cursor
hiding, and atomic flush.
Replace the manual approach with a dedicated tea.Program that runs for
the lifetime of each streaming response. BT's renderer now handles all
in-place updates flicker-free. One-shot messages (user, tool, system,
error) continue to use simple fmt.Println since they are never redrawn.
OpenAI strictly validates JSON Schema and rejects 'required': null (expects
an array). When MCP tools had no required fields, the nil []string serialized
as null. Initialize required as []string{} and sanitize nested schemas
recursively to remove null/invalid required fields from MCP server responses.
Replace catwalk dependency with direct models.dev integration (97 providers,
3039 models vs catwalk's 22/679). Auto-route @ai-sdk/openai-compatible
providers through fantasy's openaicompat using the api URL from models.dev,
eliminating the need for --provider-url. Add --all flag to 'mcphost models'
to show all providers vs just fantasy-compatible ones.
Fix all 74 golangci-lint issues: errcheck (53), staticcheck SA4006 (24),
SA9003 (2), ST1005 (5), ineffassign (3). Restructure styles.go color
handling into a colorScheme struct to eliminate SA4006 false positives
from new(x) syntax.
- Make model validation advisory: unknown models pass through to the
provider API with a stderr warning instead of blocking. Catwalk
metadata is used for cost tracking and suggestions when available.
- Add LookupModel() as the primary registry API (returns nil for
unknown models, no error).
- Add 'mcphost update-models' subcommand to refresh the model database
from a catwalk server (defaults to https://catwalk.charm.sh), a local
file, or reset to the embedded version. Supports ETag caching.
- Add disk cache layer at ~/.local/share/mcphost/providers.json;
registry loads cached data first, falls back to embedded.
- Add vercel provider support via fantasy.
- Add io.Closer plumbing to ProviderResult and Agent.Close() for
providers that hold resources.
- Delete dead ESC listener code and bubbletea/time imports from agent
- Remove internal/tokens/ package (empty stubs and trivial estimator)
- Inline token estimation into usage_tracker as unexported helper
- Remove unused EstimateAndUpdateUsageFromText dead method
- Remove 9 unsupported provider env var entries from registry
Switch the --model / -m flag format from colon-separated (provider:model)
to slash-separated (provider/model), e.g. anthropic/claude-sonnet-4-5-20250929
or ollama/qwen3:8b. The slash separator is cleaner since model names can
contain colons (ollama tags, bedrock ARNs).
Add centralized ParseModelString() in internal/models/providers.go that all
callers now use. The old colon format is still accepted with a deprecation
warning to stderr for backward compatibility.
Update default model to claude-sonnet-4-5-20250929.
Each spinner created a new tea.NewProgram which sent DECRQM queries for
synchronized output mode 2026. When the program exited and restored
cooked terminal mode, the terminal's DECRPM response leaked as visible
^[[?2026;2$y characters. Replace Bubble Tea spinner with a simple
goroutine animation loop writing directly to stderr via lipgloss.
Add support for using Claude models via Google Cloud Vertex AI through
the `google-vertex-anthropic` provider. This enables users who have
Claude access through their Google Cloud account to use mcphost with
Vertex AI authentication.
Changes:
- Add `google-vertex-anthropic` provider case and createVertexAnthropicProvider()
- Support multiple env var names for project/region to match eino-claude:
- Project: ANTHROPIC_VERTEX_PROJECT_ID, GOOGLE_CLOUD_PROJECT, GCLOUD_PROJECT
- Region: CLOUD_ML_REGION (defaults to "global" if not set)
- Upgrade eino from v0.5.11 to v0.7.11 (required by eino-claude v0.1.12)
- Migrate schema API from OpenAPI v3 to JSON Schema (eino v0.7.11 change)
Usage:
# Authenticate with Google Cloud
gcloud auth application-default login
# Set required environment variables
export ANTHROPIC_VERTEX_PROJECT_ID="your-project-id"
export CLOUD_ML_REGION="us-east5" # or use default "global"
# Run mcphost
mcphost --model google-vertex-anthropic:claude-sonnet-4@20250514
Reference: https://docs.anthropic.com/en/docs/claude-code/google-vertex-ai🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix: convert JSON Schema draft-07 exclusive bounds to draft-04 format
Chrome DevTools MCP and other MCP servers use JSON Schema draft-07 where
exclusiveMinimum/exclusiveMaximum are numeric values representing the
actual bounds. However, kin-openapi (OpenAPI 3.0) expects these fields
as booleans that modify the minimum/maximum values (draft-04 format).
This fix recursively processes input schemas to convert:
- exclusiveMinimum: N → minimum: N, exclusiveMinimum: true
- exclusiveMaximum: N → maximum: N, exclusiveMaximum: true
Handles nested schemas in properties, items, additionalProperties,
and schema composition keywords (allOf, anyOf, oneOf, not).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test: add table-driven tests for JSON Schema draft conversion
Adds comprehensive tests for convertExclusiveBoundsToBoolean():
- Simple exclusiveMinimum/exclusiveMaximum conversion
- Both bounds together
- Already boolean values (draft-04 style, unchanged)
- No exclusive bounds (unchanged)
- Nested properties
- Array items
- allOf composition
- additionalProperties
- Real-world Chrome DevTools MCP schema example
- Invalid JSON handling (returns unchanged)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds a new CLI option, `--approve-tool-run` (or via config setting),
that when enabled, prompts the user to approve a tool's execution
before it runs.
This option is disabled by default to maintain existing behavior.
Replace fmt.Printf calls in connection pool health check routines with debug logger calls. Health check messages are now only displayed when the --debug flag is enabled, providing a cleaner terminal output during normal operation while maintaining diagnostic information for troubleshooting.
* feat: add SDK package for programmatic MCPHost usage
- Export InitConfig and LoadConfigWithEnvSubstitution from cmd package
- Create sdk package with MCPHost type for programmatic access
- Add Options struct for configuration overrides
- Implement Prompt and PromptWithCallbacks methods
- Add session management (load, save, clear)
- Create type helpers for Message and ToolCall
- Add comprehensive SDK documentation in README
- Include basic and scripting examples
- Add unit tests for SDK functionality
The SDK reuses all existing internal packages and maintains identical
behavior to the CLI, including config loading, environment variables,
and defaults.
* docs: add SDK section to main README with link to detailed documentation
* fix tests
* update CI
* feat: add MCP connection pool with health check and retry mechanism
* feat: add proactive connection health check before tool calls
- Add GetConnectionWithHealthCheck method to perform health check before reusing connections
- Add performHealthCheck method to test connection health with ListTools call
- Modify InvokableRun to use health-checked connections
- This prevents first tool call failure when connection is broken
- Improves user experience by ensuring connections are healthy before tool execution
---------
Co-authored-by: 茂勋 <shancangchen.csc@alibaba-inc.com>
* feat: add --tls-skip-verify flag for self-signed certificates
Adds support for skipping TLS certificate verification when connecting to
providers with self-signed certificates. This is particularly useful for
local Ollama instances secured with HTTPS.
- Add --tls-skip-verify command-line flag with security warnings
- Update ProviderConfig to include TLSSkipVerify field
- Modify HTTP client creation for all providers (Ollama, OpenAI, Anthropic, Google, Azure)
- Create helper functions for TLS-aware HTTP client creation
- Add comprehensive unit tests for TLS skip verify functionality
- Update documentation with usage examples and security warnings
Fixes#113🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: opencode <noreply@opencode.ai>
* feat: add TLS skip verify support to script mode
- Add TLSSkipVerify field to Config struct for script frontmatter
- Update script parsing to handle tls-skip-verify in YAML frontmatter
- Pass TLS configuration to model creation in script mode
- Add example script demonstrating TLS skip verify usage
- Update script examples documentation
This allows scripts to specify tls-skip-verify: true in their frontmatter
to connect to providers with self-signed certificates.
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: opencode <noreply@opencode.ai>
---------
Co-authored-by: opencode <noreply@opencode.ai>
* feat: enhance hooks with LLM feedback capabilities
- Add new HookOutput fields for LLM interaction (feedback, context, systemPrompt, modifyInput/Output)
- Implement Continue functionality to gracefully stop sessions from hooks
- Implement SuppressOutput to hide tool results from user display
- Add UserPromptSubmit context injection to provide additional context to LLM
- Update mergeHookOutputs to handle new fields
- Add comprehensive unit tests for new hook output processing
- Create example Python hook demonstrating LLM feedback features
This enhancement allows hooks to:
- Provide feedback and context that reaches the LLM
- Modify tool inputs/outputs before processing
- Control session flow with Continue field
- Suppress output display while still sending to LLM
- Inject system prompts and context for better LLM responses
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: opencode <noreply@opencode.ai>
* fix: make tool blocking visible to LLM
When a PreToolUse hook blocks a tool execution, the LLM now receives an error message
indicating the tool was blocked, allowing it to adapt its approach.
Changes:
- Track when tools are blocked by PreToolUse hooks
- Replace tool execution results with error messages when blocked
- Add test to verify blocking functionality
- Add ToolBlockChecker type for future enhancements
This ensures the LLM is aware when its tool calls are blocked by security policies
and can respond appropriately rather than being unaware of the block.
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: opencode <noreply@opencode.ai>
* refactor: remove unimplemented LLM feedback fields
Removed the following unimplemented fields from HookOutput:
- Feedback
- Context
- SystemPrompt
- ModifyInput
- ModifyOutput
These fields were added speculatively but not fully implemented.
Keeping only the working functionality:
- Continue/StopReason for session control
- SuppressOutput for hiding tool results
- Decision/Reason for blocking tools
The critical tool blocking visibility feature remains intact.
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: opencode <noreply@opencode.ai>
---------
Co-authored-by: opencode <noreply@opencode.ai>
* Add comprehensive hooks system for MCPHost lifecycle events
Implements a flexible hooks system based on Anthropic Claude Code specification:
- **Hook Events**: PreToolUse, PostToolUse, UserPromptSubmit, Stop
- **Hook Types**: Command execution with JSON input/output
- **Configuration**: XDG-compliant with layered config support
- **Security**: Command validation, timeout controls, safe execution
- **Common Fields**: Consistent session ID, timestamps, model info across all hooks
Key features:
- Hooks receive JSON via stdin and can control flow via stdout
- Pattern matching for tool-specific hooks (regex support)
- Enhanced Stop hook with agent response and metadata
- Centralized session management with consistent IDs
- Built-in examples for logging, validation, and monitoring
This enables users to:
- Log and audit all tool usage and prompts
- Implement custom security policies
- Track usage metrics and model performance
- Integrate with external systems
- Build custom workflows around MCPHost
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: opencode <noreply@opencode.ai>
* Enable hooks in script mode
Previously, hooks were only initialized and executed in normal mode but not
in script mode. This was because script mode had its own execution path that
bypassed the hook initialization code.
This fix:
- Adds hook initialization to runScriptMode function
- Creates hook executor with proper session ID and model info
- Passes the hook executor to runAgenticLoop
Now hooks work consistently across all execution modes (normal, script, and
interactive), ensuring uniform behavior for logging, validation, and monitoring.
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: opencode <noreply@opencode.ai>
* Remove unnecessary hooks.local.yml pattern
The .local.yml pattern adds unnecessary complexity. Users who want project-specific
hooks that aren't committed to git can simply add .mcphost/ to their .gitignore.
This simplifies the hooks configuration loading and makes it clearer that:
- Global user hooks go in ~/.config/mcphost/hooks.yml
- Project-specific hooks go in .mcphost/hooks.yml
- Git ignore management is left to the user
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: opencode <noreply@opencode.ai>
* Fix hooks test isolation and add --no-hooks flag
- Fix TestLoadHooksConfig by setting temporary XDG_CONFIG_HOME to prevent loading global hooks
- Add --no-hooks flag to disable all hooks execution across all modes
- Update README with documentation for the new flag
- Add test to verify hooks loading behavior
This allows users to temporarily disable hooks for security or debugging purposes.
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: opencode <noreply@opencode.ai>
---------
Co-authored-by: opencode <noreply@opencode.ai>
* Add builtin HTTP functionality
* Add GitHub Actions CI workflow
- Runs tests with race detection on push to main and PRs
- Verifies code generation is up to date
- Uses Go version from go.mod file
- Supports manual workflow dispatch
* Remove verify-codegen job from CI workflow
Simplifies CI to only run tests with race detection
* draft: rewrite single message when streaming (not full terminal)
* having the spinner align better with dots in compact mode
* fix user messages
* handle usage display
* fix formatting
* bash highlighting
---------
Co-authored-by: Nate Woods <big.nate.w@gmail.com>
This commit addresses issue #92 by extracting duplicated code between
normal mode (cmd/root.go) and script mode (cmd/script.go) into reusable
factory functions and utilities.
## Changes Made
### New Factory Files
- **internal/agent/factory.go**: Agent creation factory with spinner support
- `CreateAgent()` function with configurable options
- `ParseModelName()` utility for model string parsing
- Spinner function injection to avoid import cycles
- **internal/ui/factory.go**: CLI setup factory with standard configuration
- `SetupCLI()` function for consistent CLI initialization
- Usage tracking setup for supported providers
- Model info and tool count display
- **internal/config/merger.go**: Config loading and merging utilities
- `LoadAndValidateConfig()` for standard config loading
- `MergeConfigs()` for script frontmatter merging
### Updated Command Files
- **cmd/root.go**: Refactored to use new factories
- Replaced ~50 lines of agent creation logic
- Replaced ~30 lines of CLI setup logic
- Replaced ~20 lines of config loading logic
- Added agentUIAdapter to handle interface compatibility
- **cmd/script.go**: Refactored to use new factories
- Same factory usage as normal mode for consistency
- Maintained script-specific behavior (no spinners)
- Improved config merging with frontmatter
## Benefits
- **Reduced code duplication**: ~33 lines of duplicated code eliminated
- **Single source of truth**: Agent creation and CLI setup logic centralized
- **Consistent behavior**: Both modes now use identical underlying logic
- **Easier maintenance**: Changes apply to both modes automatically
- **Better testability**: Factory functions can be unit tested independently
- **Cleaner command files**: Focus on mode-specific logic only
## Testing
- All existing tests pass
- Build verification successful
- Both normal and script modes tested for basic functionality
- Code formatting and linting checks passed
🤖 Generated with [opencode](https://opencode.ai)
Co-authored-by: opencode <noreply@opencode.ai>
Fixes#89: Tools created without input properties were causing OpenAI function
calling validation errors with "object schema missing properties" message.
The issue occurred when MCP tools had no input parameters, resulting in OpenAPI
schemas with Type="object" but Properties=nil, which violates OpenAI's function
calling schema requirements.
Changes:
- Add schema validation fix in loadServerTools to ensure object schemas have
empty properties map when Properties is nil
- Add comprehensive regression test TestIssue89_ObjectSchemaMissingProperties
- Add additional test coverage for tools without properties
The fix ensures backward compatibility while resolving the validation error.
Users no longer need the workaround of adding dummy parameters to their tools.
🤖 Generated with [opencode](https://opencode.ai)
Co-authored-by: opencode <noreply@opencode.ai>
* Model generation params
* Standardize model URL
* standardize more flags and update max steps check
* fix system prompt handling
* standardize api key handling
* update readme
* fix: tool result handling in anthropic
* fix: set the role outside the loop
* fix: role mapping in implementation of google provider and anthropic provider
* fix: simplify role mapping logic and update role constants
* support system prompt for ollama
* support system prompt for openai
* support system prompt for google gemini
* fixed setting for google gemini system-prompt
* support system prompt for anthropic
* edit README
* edit commments
* fix system-prompt flag desc
* modifed var name for systemPromp
Previously when multiple tool calls were requested by the LLM we got the
error `An assistant message with 'tool_calls' must be followed by tool
messages responding to each 'tool_call_id'.` although the calls were
processed.
Apparently each response needs to be its own message, rather than its
own block.
Closes#7
* Add support of config for SSE servers. Add Authorization header support
* Change the format of JSON for SSE servers to be similar to other tools
---------
Co-authored-by: Roman Gelembjuk <Roman Gelembjuk>
* add google/gemini support
* fix user/model role issues, use chat session
* properly maintain history
* update gemini/google provider, pipe context through runPrompt, minor fixes
* minor formatting fix in usage tooltip
* update readme to showcase Google/Gemini support
---------
Co-authored-by: Ed Zynda <ezynda3@users.noreply.github.com>
description: Open a GitHub PR for the current branch using the repo's PR template
---
Open a GitHub pull request for the current branch, filling out the repository's PR template with a description grounded in the actual commits and diff.
## Steps
1.**Verify the branch is pushed**:
-`git status -sb` and `git log @{u}..HEAD --oneline 2>/dev/null` — if there is no upstream or unpushed commits, run `git push -u origin "$(git branch --show-current)"` first
- If the working tree is dirty, stop and tell the user to commit first (suggest `/commit-push`)
2.**Gather context**:
-`git log origin/main..HEAD --oneline` — list of commits going into the PR
-`git diff origin/main...HEAD --stat` then `git diff origin/main...HEAD` — read the actual changes
- Identify the linked issue (from commit messages, branch name, or extra user input: $@) — capture as `Fixes #N` if applicable
3.**Locate the PR template**:
- Check `.github/pull_request_template.md`, `.github/PULL_REQUEST_TEMPLATE.md`, or `docs/pull_request_template.md`
- If none exists, use a minimal `## Description` / `## Type of Change` / `## Checklist` structure
4.**Draft the PR body** by filling out the template:
- **Description**: 1–3 short paragraphs explaining *what* changed and *why*, grounded in the diff. Include a brief before/after example for new APIs when useful.
- **Fixes #N**: only if there is a real linked issue
- **Type of Change**: tick the single most accurate box with `[x]` (leave others as `[ ]`)
- **Checklist**: tick items that are genuinely true (style, self-review, tests added, docs updated)
- **Additional Information**: bullet list of added / modified files and any backward-compatibility notes
- Remove template sections explicitly marked "remove if not applicable" (e.g. MCP Spec Compliance) when they don't apply
5.**Write the body to a temp file**: `/tmp/pr-body-<branch-or-issue>.md` — never inline a long body via `--body`, always use `--body-file`
6.**Choose the title**: prefer the subject of the primary commit if it already follows Conventional Commits; otherwise craft one in the same style (`<type>(<scope>): <imperative summary>`, ≤72 chars)
7.**Create the PR**:
```
gh pr create \
--title "<title>" \
--body-file /tmp/pr-body-<...>.md \
--base main \
--head "$(git branch --show-current)"
```
Use the repo's actual default branch if it isn't `main` (`gh repo view --json defaultBranchRef -q .defaultBranchRef.name`)
8. **Report the PR URL** returned by `gh` and stop
## Guidelines
- Read the diff and commit messages — do **not** invent features that aren't in the code
- One PR per logical change; if the branch contains unrelated commits, surface that and ask before continuing
- Keep the description focused on reviewer-relevant information (what / why), not a replay of the diff
- Only check checklist boxes that are actually satisfied; leave the rest unchecked rather than lying
- If `gh` is not authenticated (`gh auth status` fails), stop and tell the user
- Mention it was created with the feature_request template
## Guidelines
- Focus on the *problem* first, then the solution
- Include concrete examples of how the feature would be used
- Consider edge cases and mention them
- If proposing API changes, show before/after code
- Check if similar features exist in related tools (mention them for reference)
- Align with Kit's philosophy: TUI-first, extension-based, keyboard-driven
## Example
User: `/feature-request I want to be able to customize tool border colors dynamically`
You:
1. Title: `feat: dynamic border colors for tool results based on status`
2. Body:
- **Feature Description**: Allow `ToolRenderConfig` to accept a function that determines border color based on tool result content or status, enabling dynamic visual feedback.
- **Motivation**: When running multiple tools, it's hard to distinguish file reads (blue), shell commands (green), and errors (red) without custom colors per result.
- **Proposed Implementation**: Add `BorderColorFunc` callback that receives `(result string, isError bool)` and returns a color string.
description: Implement the fix/feature/docs change requested by a GitHub issue
---
Resolve GitHub issue #$1 by reading it, classifying it, and producing the appropriate code or doc change. **Stop once the working tree contains the change** — committing, pushing, and opening a PR are handled by `/commit-push` and `/create-pr`.
- **Frontmatter**: only `description` is recognised; keep it under ~80 chars
- **Body**: plain markdown; the full text is submitted as the user's message when the template fires
- **Required args**: kit infers required positional args from the highest `$N` it finds *outside* backtick/tilde code fences — a stray `$2` in active prose means kit will refuse to run without 2 arguments
## Argument placeholders
kit performs shell-style substitution before sending the prompt to the model:
-`$1`, `$2`, … — positional arguments (1-indexed)
-`${1}`, `${2}`, … — same, brace form (use when followed by digits/letters: `${1}_suffix`)
-`$@` — all arguments joined by spaces (zero or more, optional)
-`$+` — all arguments, **at least one required**
-`$ARGUMENTS` / `${ARGUMENTS}` — alias for `$@`
-`${@:N}` — args from the Nth onwards (1-indexed, bash-style)
Anything inside triple-backtick fences, `~~~` fences, or single-backtick `inline` code spans is **left untouched** so example code samples don't get corrupted. That means:
- An inline-coded `gh issue view $1` stays literal `$1` in the model's input ❌
- The same command without backticks: gh issue view $1 → expands to `gh issue view 42` ✓
**Rule of thumb:** if you want a placeholder to substitute, keep it outside backticks and fences. If you want a literal `$1` in the output (e.g. teaching the user shell syntax), put it inside backticks.
### Workarounds for "I want it to look like code AND substitute"
1.**Drop the backticks** around just the placeholder portion — the rest can still read as a command line in prose
2.**Use a 4-space-indented code block** instead of a triple-backtick fence — kit only skips backtick/tilde fences, so indentation-style code blocks still get substitution:
git push -u origin "$(git branch --show-current)"
gh pr create --title "fix: ... (#$1)" --base main
3.**Bind once, reference loosely**: put `Issue: $1` at the top in prose, then leave the backticked examples literal — the model will substitute mentally
## Steps
1.**Understand the workflow** the user described in $@ — ask a clarifying question if the intent is ambiguous
2.**Choose a filename**: short, lowercase, hyphen-separated, descriptive (e.g. `code-review.md`)
3.**Write the description**: one sentence, imperative, fits in autocomplete
4.**Decide on arguments**:
- No args needed → omit placeholders entirely
- One required value (issue number, PR url, file path) → use `$1`
- Free-form trailing context → end with a single `$@` line
- Multiple distinct values → use `$1`, `$2`, … and document each at the top
5.**Draft the body**:
- Open with a single sentence stating the goal, weaving in `$1`/`$@` where the value belongs
- Use `## Steps` for multi-step workflows; use plain prose for simple prompts
- Be specific: name commands, flags, and file paths where relevant
- **Audit every backtick and code fence**: any `$N` or `$@` inside them will not expand — was that intentional? If not, apply one of the workarounds above
6.**Write the file** to `.kit/prompts/<slug>.md`
7.**Verify substitution** by mentally (or actually) replacing `$1`/`$@` with a sample value and confirming every reference resolves — and that the prompt's *own* example snippets don't accidentally bump the required-arg count (wrap illustrative `$N` examples in triple-backtick fences, not 4-space indentation, so `RequiredArgs()` ignores them)
8.**Confirm** by showing the final file content and the slash command that activates it (e.g. `/code-review 42`)
## Guidelines
- Keep prompts action-oriented — they should tell kit *what to do*, not just *what to think about*
- Prefer concrete steps over vague instructions
- A prompt that does one thing well beats one that tries to cover every edge case
- If the workflow already exists as a prompt, suggest extending it instead of duplicating
- When in doubt about substitution behaviour, write the file and run `/<slug> testvalue` once to confirm — wrong placement of backticks is the #1 failure mode
description: Audit and update project documentation (README and docs site) for a recent change
---
Review recent code changes, identify all documentation surfaces that should
mention them, and update each one — grounded in the actual diff, not guesses.
## Steps
1.**Identify the change**:
- If the user input ($@) names a commit / PR / branch / topic, use that as the focus
- Otherwise inspect `git log origin/main..HEAD --oneline` and `git diff origin/main...HEAD --stat` to discover what shipped on the current branch
- Read the actual diff (`git diff origin/main...HEAD`) — never document features that aren't in the code
2.**Inventory the doc surfaces**:
-`README.md` at the repo root
- Any docs site (commonly `www/`, `docs/`, `site/`) — list its pages and identify the one(s) most thematically related to the change
- Inline godoc / API reference comments on the new exported symbols
-`CHANGELOG.md` if the project keeps one
- Any `examples/` directory entries that demonstrate the affected area
3.**Audit each surface** with `grep`:
- Search for the names of related existing APIs (e.g. if you added `IterTools`, grep for `ListTools`) to find every page that already discusses the area
- Decide for each hit: does it need a cross-reference, a side-by-side comparison, or to stay untouched?
4.**Decide where new content lives**:
- Prefer extending an existing page over creating a new one
- For a docs site, place new sections near related content (check the page's `## Heading` outline first)
- Skip surfaces that genuinely don't apply (e.g. a server-focused README for a client-only change) and say so explicitly
5.**Draft the updates**:
- Lead with a one-sentence statement of what's new and why
- Show concrete code examples copied from real signatures — verify against the source files
- Include a comparison / "when to use which" table when adding an alternative to an existing API
- Note backwards-compatibility behaviour if relevant
6.**Verify the docs build** before committing:
- For vocs / docusaurus / mkdocs sites, run the local build command (e.g. `npx vocs build`, `mkdocs build`) and fix any MDX/markdown errors
- For godoc, run `go vet ./...` and `go doc <pkg> <Symbol>` to sanity-check rendering
7.**Report**:
- List every file changed and every file deliberately left alone (with a one-line reason)
- Suggest the next step (typically `/commit-push`) — do not auto-commit unless asked
## Guidelines
- Read the diff before writing anything — invented API names erode trust faster than missing docs
- One change per doc commit; keep doc updates separate from code changes when possible
- Match the existing voice and formatting of each surface (headings, code-fence languages, table styles)
- Prefer linking between pages over duplicating content
- **Decoupling pattern**: `cmd/root.go` has converter functions (e.g. `widgetProviderForUI()`) that bridge `internal/extensions/` types to `internal/ui/` types — the UI never imports extensions directly
- **Public SDK** (`pkg/kit/`): The public-facing Go SDK for embedding Kit as a library. See rules below.
## Public SDK (`pkg/kit/`) Rules
`pkg/kit/` is the **public API surface** consumed by external Go developers. All exported symbols, types, function names, and godoc comments in this package are part of the SDK contract.
- **Exported function/method names** — use generic terms (`LLM`, `Provider`, `Message`) instead of library names
- **Exported type names** — type aliases should use domain names (e.g. `LLMMessage`, not `FantasyMessage`)
- **Godoc comments** on exported symbols — these are visible in `go doc` output and pkg.go.dev
- **Struct field names and tags** on exported types
Using dependency types directly in **function bodies** (private implementation) is fine — that's invisible to SDK consumers.
### Naming Conventions for SDK Symbols
- Type aliases re-exporting dependency types: use `LLM*` prefix (e.g. `LLMMessage`, `LLMUsage`, `LLMResponse`)
- Conversion helpers: use `ConvertToLLM*` / `ConvertFromLLM*` (not the dependency name)
- Provider queries: use `GetLLMProviders` (not `GetFantasyProviders`)
- When wrapping internal methods, the `pkg/kit/` name should be dependency-agnostic even if the `internal/` method still uses the old name
### Deprecation Pattern
When renaming a public SDK symbol, keep the old name as a deprecated wrapper for one release cycle:
```go
// Deprecated: Use NewName instead.
funcOldName(){returnNewName()}
```
## Key Patterns
### Yaegi (Extension Interpreter) Gotchas
- **No interfaces across boundary**: All extension-facing API types must be concrete structs, never interfaces. Yaegi crashes on interface wrapper generation.
- **Function field bug**: Named function references assigned to struct fields return zero values across the interpreter boundary. Always use anonymous closure literals:
- **Symbol exports**: Every new type exposed to extensions must be added to `internal/extensions/symbols.go`
### BubbleTea Integration
- **No `prog.Send()` from inside `Update()`**: Calling `prog.Send()` synchronously within a BubbleTea `Update()` handler deadlocks the event loop. Use `go appInstance.NotifyWidgetUpdate()` (async goroutine) instead.
- **Height measurement**: `distributeHeight()` in `model.go` must measure using the same render path as `View()`. If an interceptor wraps rendering, measure with the wrapper too, or layout will mismatch.
- **Channel-based prompts**: Extension prompt calls (PromptSelect, etc.) block on a `chan PromptResponse`. Extension slash commands run in dedicated goroutines (not `tea.Cmd`) to avoid stalling BubbleTea's Cmd scheduler.
### Extension State Management
- **Thread-safe maps on Runner**: Widget/header/footer/editor state lives on the Runner with `sync.RWMutex`, queried by UI via callbacks
- **Context function fields**: The `Context` struct uses function fields (`Print func(string)`, `SetWidget func(WidgetConfig)`) wired by closures in `cmd/root.go`
- **Package-level vars in extensions**: Yaegi supports package-level variables captured in closures — this is how extensions maintain state across event callbacks
### Unicode in Widget Text
- Widget content renders through `lipgloss.Style.Render()` which preserves ANSI escape codes
- Use rune-based width calculations (`len([]rune(s))`) not byte length (`len(s)`) when aligning box-drawing characters or multi-byte symbols
## Testing
### Interactive TUI Testing with tmux
Use tmux to test Kit interactively without blocking the agent:
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">kit</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> auth</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> login</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [provider] --set-default </span><span style="color:#6A737D;--shiki-dark:#6A737D"># Set provider's default model as system default</span></span>
<p>Manage the local model database that maps provider names to API configurations.</p>
<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">kit</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> models</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [provider] </span><span style="color:#6A737D;--shiki-dark:#6A737D"># List available models (optionally filter by provider)</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">kit</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> models</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> --all</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # Show all providers (not just LLM-compatible)</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D"># Load all skills from a custom directory instead of the default locations</span></span>
<p>Skills are auto-discovered from <code>~/.config/kit/skills/</code>, <code>.kit/skills/</code>, and <code>.agents/skills/</code> by default. Use <code>--skills-dir</code> to override the project-local search root, or <code>--skill</code> to load files explicitly (which disables auto-discovery). <code>--no-skills</code> suppresses all skill loading regardless of other flags.</p>
<p>Use <strong>↑</strong> and <strong>↓</strong> arrow keys to navigate through previously submitted prompts. Kit keeps the last 100 entries. Consecutive duplicates are skipped.</p>
<p>Press <strong>ESC twice</strong> to cancel the current operation:</p>
<ul>
<li>During a tool call: rolls back the entire turn to maintain API message pairing</li>
<li>During streaming: stops the response generation</li>
</ul>
<p>This ensures that <code>tool_use</code> and <code>tool_result</code> messages are always sent to the API as matched pairs, avoiding errors from orphaned tool calls.</p>
<p>Press <strong>Ctrl+X e</strong> to open your <code>$VISUAL</code> or <code>$EDITOR</code> in a temporary file pre-populated with the current input text. On save and quit, the edited content replaces the input textarea. On error exit (e.g., <code>:cq</code> in Vim), the original input is preserved.</p>
<p>Press <strong>Ctrl+X s</strong> during streaming to inject a system-level instruction mid-turn. This allows you to steer the conversation direction without waiting for the model to finish:</p>
<ul>
<li>Works during streaming output</li>
<li>Sends a steering instruction as a system message</li>
<li>Model continues from the interruption point with the new guidance</li>
</ul>
<p>Example: While the model is writing code, press Ctrl+X s and type "Use async/await instead" to change the implementation approach.</p>
<p>Attach images to your next prompt straight from the clipboard:</p>
<ul>
<li>Copy an image (e.g. a screenshot) to the system clipboard, then press <strong>Ctrl+V</strong> in the input to attach it.</li>
<li>Press <strong>Ctrl+U</strong> to clear all pending image attachments.</li>
<li>Attachments are sent alongside your text when you submit, and cleared afterward.</li>
</ul>
<p>When a terminal supports color, Kit renders a small low-resolution <strong>thumbnail preview</strong> of each pending image directly in the input, below the <code>[N image(s) attached]</code> indicator, so you can confirm the right image was attached before sending.</p>
<p>The preview is drawn with Unicode half-block characters and ordinary terminal colors — not a graphics protocol — so it renders correctly inside terminal multiplexers like <strong>tmux</strong> and <strong>zellij</strong>. Thumbnails are capped to a small cell box for a glanceable, low-res look.</p>
<ul>
<li>Best fidelity needs a <strong>truecolor</strong> terminal (<code>COLORTERM=truecolor</code>); Kit degrades to 256-color where truecolor is unavailable.</li>
<li>On terminals with neither, the preview is skipped and the <code>[N image(s) attached]</code> text indicator is shown alone.</li>
</ul>
<p>You can also attach image files by referencing them with <code>@path/to/image.png</code> — binary files are auto-detected by MIME type. See <a href="/quick-start">Quick Start</a> for the <code>@</code> attachment syntax.</p>
<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D"># Load a specific template by name</span></span>
<p>Run Kit as an <a href="https://agentclientprotocol.com">ACP (Agent Client Protocol)</a> agent server. ACP-compatible clients communicate with Kit over JSON-RPC 2.0 on stdin/stdout.</p>
# Load all skills from a custom directory instead of the default locations
kit --skills-dir /path/to/skills "prompt"
# Disable all skill loading (auto-discovery and explicit)
kit --no-skills "prompt"
\`\`\`
Skills are auto-discovered from \`~/.config/kit/skills/\`, \`.kit/skills/\`, and \`.agents/skills/\` by default. Use \`--skills-dir\` to override the project-local search root, or \`--skill\` to load files explicitly (which disables auto-discovery). \`--no-skills\` suppresses all skill loading regardless of other flags.
## Interactive slash commands
These commands are available inside the Kit TUI during an interactive session:
| Command | Description |
|---------|-------------|
| \`/help\` | Show available commands |
| \`/tools\` | List available MCP tools |
| \`/servers\` | Show connected MCP servers |
| \`/model [name]\` | Switch model or open model selector |
| \`/theme [name]\` | Switch color theme or list available themes |
| \`/import <path>\` | Import a session from a JSONL file |
| \`/share\` | Upload session to GitHub Gist and get a shareable viewer URL |
| \`/quit\` | Exit Kit |
### Prompt history
Use **↑** and **↓** arrow keys to navigate through previously submitted prompts. Kit keeps the last 100 entries. Consecutive duplicates are skipped.
### Cancelling operations
Press **ESC twice** to cancel the current operation:
- During a tool call: rolls back the entire turn to maintain API message pairing
- During streaming: stops the response generation
This ensures that \`tool_use\` and \`tool_result\` messages are always sent to the API as matched pairs, avoiding errors from orphaned tool calls.
### External editor
Press **Ctrl+X e** to open your \`$VISUAL\` or \`$EDITOR\` in a temporary file pre-populated with the current input text. On save and quit, the edited content replaces the input textarea. On error exit (e.g., \`:cq\` in Vim), the original input is preserved.
### Mid-turn steering
Press **Ctrl+X s** during streaming to inject a system-level instruction mid-turn. This allows you to steer the conversation direction without waiting for the model to finish:
- Works during streaming output
- Sends a steering instruction as a system message
- Model continues from the interruption point with the new guidance
Example: While the model is writing code, press Ctrl+X s and type "Use async/await instead" to change the implementation approach.
### Image attachments
Attach images to your next prompt straight from the clipboard:
- Copy an image (e.g. a screenshot) to the system clipboard, then press **Ctrl+V** in the input to attach it.
- Press **Ctrl+U** to clear all pending image attachments.
- Attachments are sent alongside your text when you submit, and cleared afterward.
When a terminal supports color, Kit renders a small low-resolution **thumbnail preview** of each pending image directly in the input, below the \`[N image(s) attached]\` indicator, so you can confirm the right image was attached before sending.
The preview is drawn with Unicode half-block characters and ordinary terminal colors — not a graphics protocol — so it renders correctly inside terminal multiplexers like **tmux** and **zellij**. Thumbnails are capped to a small cell box for a glanceable, low-res look.
- Best fidelity needs a **truecolor** terminal (\`COLORTERM=truecolor\`); Kit degrades to 256-color where truecolor is unavailable.
- On terminals with neither, the preview is skipped and the \`[N image(s) attached]\` text indicator is shown alone.
You can also attach image files by referencing them with \`@path/to/image.png\` — binary files are auto-detected by MIME type. See [Quick Start](/quick-start) for the \`@\` attachment syntax.
## Prompt templates
### Creating templates
Templates use YAML frontmatter for metadata and support argument placeholders:
\`\`\`markdown
---
description: Review code for issues
---
Review the following code for bugs and security issues.
Focus on $1 specifically.
\`\`\`
Save to \`~/.kit/prompts/review.md\` or \`.kit/prompts/review.md\`.
### Using templates
Templates appear as slash commands:
\`\`\`
/review error handling
\`\`\`
### Argument placeholders
| Placeholder | Description |
|-------------|-------------|
| \`$1\`, \`$2\`, etc. | Individual arguments by position |
| \`$@\`, \`$ARGUMENTS\` | All arguments joined with spaces (zero or more) |
| \`$+\` | All arguments joined with spaces (one or more required) |
| \`\${@:N}\` | Arguments from position N onwards |
| \`\${@:N:L}\` | L arguments starting at position N |
Placeholders inside fenced code blocks (\`\`\`\`\`\`\`) and inline code spans are ignored, so documentation examples won't be substituted.
### CLI flags
\`\`\`bash
# Load a specific template by name
kit --prompt-template review
# Disable template loading
kit --no-prompt-templates
\`\`\`
## ACP server
Run Kit as an [ACP (Agent Client Protocol)](https://agentclientprotocol.com) agent server. ACP-compatible clients communicate with Kit over JSON-RPC 2.0 on stdin/stdout.
<p>The <code>llm.Provider</code> interface abstracts different LLM providers. Each provider implements message formatting, tool calling, and streaming for its specific API.</p>
<p>External tools are integrated via the Model Context Protocol (MCP). Kit acts as an MCP client, connecting to MCP servers configured in <code>.kit.yml</code>.</p>
<p>Extensions are Go source files interpreted at runtime by Yaegi. The <code>internal/extensions/</code> package manages loading, symbol export, and lifecycle dispatch. See the <a href="/extensions/overview">Extension System</a> docs for details.</p>
<p>The interactive terminal UI is built with <a href="https://github.com/charmbracelet/bubbletea">Bubble Tea v2</a>, using a parent-child model where <code>AppModel</code> manages child components (<code>InputComponent</code>, <code>StreamComponent</code>, etc.).</p>
<p><code>cmd/root.go</code> contains converter functions (e.g., <code>widgetProviderForUI()</code>) that bridge <code>internal/extensions/</code> types to <code>internal/ui/</code> types. The UI never imports the extensions package directly.</p>
<p>Contributions are welcome! Please see the <a href="https://github.com/mark3labs/kit/blob/master/contribute/contribute.md">contribution guide</a> for guidelines.</p>
internal/skills/ - Skill loading and system prompt composition
internal/tools/ - MCP tool integration
internal/ui/ - Bubble Tea TUI components
examples/extensions/ - Example extension files
npm/ - NPM package wrapper for distribution
\`\`\`
## Architecture overview
Kit is built around a few key architectural patterns:
### Multi-provider LLM support
The \`llm.Provider\` interface abstracts different LLM providers. Each provider implements message formatting, tool calling, and streaming for its specific API.
### MCP client-server model
External tools are integrated via the Model Context Protocol (MCP). Kit acts as an MCP client, connecting to MCP servers configured in \`.kit.yml\`.
### Extension system
Extensions are Go source files interpreted at runtime by Yaegi. The \`internal/extensions/\` package manages loading, symbol export, and lifecycle dispatch. See the [Extension System](/extensions/overview) docs for details.
### TUI architecture
The interactive terminal UI is built with [Bubble Tea v2](https://github.com/charmbracelet/bubbletea), using a parent-child model where \`AppModel\` manages child components (\`InputComponent\`, \`StreamComponent\`, etc.).
### Decoupling pattern
\`cmd/root.go\` contains converter functions (e.g., \`widgetProviderForUI()\`) that bridge \`internal/extensions/\` types to \`internal/ui/\` types. The UI never imports the extensions package directly.
## Contributing
Contributions are welcome! Please see the [contribution guide](https://github.com/mark3labs/kit/blob/master/contribute/contribute.md) for guidelines.
constt={frontmatter:{title:"Kit",description:"Kit is a powerful, extensible AI coding agent CLI with multi-provider support, built-in tools, and a rich extension system.",hidden:!1,toc:!1,draft:!1},html:`<div style="text-align: center; margin: 2rem 0;">
<p>See <a href="/providers">Providers</a> for the full list of supported providers and their configuration.</p>`,headings:[{depth:2,text:"Using npm / bun / pnpm",id:"using-npm--bun--pnpm"},{depth:2,text:"Using Go",id:"using-go"},{depth:2,text:"Building from source",id:"building-from-source"},{depth:2,text:"Verifying the installation",id:"verifying-the-installation"},{depth:2,text:"Setting up a provider",id:"setting-up-a-provider"}],raw:`
# Installation
## Using npm / bun / pnpm
\`\`\`bash
npm install -g @mark3labs/kit
\`\`\`
\`\`\`bash
bun install -g @mark3labs/kit
\`\`\`
\`\`\`bash
pnpm install -g @mark3labs/kit
\`\`\`
## Using Go
\`\`\`bash
go install github.com/mark3labs/kit/cmd/kit@latest
\`\`\`
## Building from source
\`\`\`bash
git clone https://github.com/mark3labs/kit.git
cd kit
go build -o kit ./cmd/kit
\`\`\`
## Verifying the installation
After installing, verify Kit is available:
\`\`\`bash
kit --help
\`\`\`
## Setting up a provider
Kit needs at least one LLM provider configured. Set an API key for your preferred provider:
\`\`\`bash
# Anthropic (default provider)
export ANTHROPIC_API_KEY="sk-..."
# OpenAI
export OPENAI_API_KEY="sk-..."
# Google Gemini
export GOOGLE_API_KEY="..."
\`\`\`
For OAuth-enabled providers like Anthropic, you can also authenticate interactively:
\`\`\`bash
kit auth login anthropic
\`\`\`
See [Providers](/providers) for the full list of supported providers and their configuration.
<p>Extensions are Go source files interpreted at runtime via <a href="https://github.com/traefik/yaegi">Yaegi</a>. They can add custom tools, slash commands, widgets, keyboard shortcuts, and intercept lifecycle events — all without recompiling Kit.</p>
<p>See <a href="/extensions/capabilities">Capabilities</a> for full details on each component type, and <a href="/extensions/testing">Testing</a> for writing tests for your extensions.</p>`,headings:[{depth:2,text:"Minimal extension",id:"minimal-extension"},{depth:2,text:"How extensions work",id:"how-extensions-work"},{depth:2,text:"Key concepts",id:"key-concepts"},{depth:3,text:"The API object",id:"the-api-object"},{depth:3,text:"The Context object",id:"the-context-object"}],raw:'\n# Extension System\n\nExtensions are Go source files interpreted at runtime via [Yaegi](https://github.com/traefik/yaegi). They can add custom tools, slash commands, widgets, keyboard shortcuts, and intercept lifecycle events — all without recompiling Kit.\n\n## Minimal extension\n\n```go\n//go:build ignore\n\npackage main\n\nimport "kit/ext"\n\nfunc Init(api ext.API) {\n api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {\n ctx.SetFooter(ext.HeaderFooterConfig{\n Content: ext.WidgetContent{Text: "Custom Footer"},\n })\n })\n}\n```\n\nRun it with:\n\n```bash\nkit -e examples/extensions/minimal.go\n```\n\n## How extensions work\n\n1. Kit discovers extension files from [auto-discovery paths](/extensions/loading) or explicit `-e` flags\n2. Each `.go` file is loaded into a Yaegi interpreter with access to the `kit/ext` package\n3. Kit calls the `Init(api ext.API)` function in each extension\n4. The extension registers callbacks, tools, commands, and UI components via the `api` and `ctx` objects\n\n## Key concepts\n\n### The `API` object\n\nPassed to `Init()`, the `API` object is used to register lifecycle event handlers and static components:\n\n- **Lifecycle handlers** — `api.OnSessionStart(...)`, `api.OnToolCall(...)`, etc.\n- **Tools** — `api.RegisterTool(ext.ToolDef{...})`\n- **Commands** — `api.RegisterCommand(ext.CommandDef{...})`\n- **Shortcuts** — `api.RegisterShortcut(ext.ShortcutDef{...}, handler)`\n- **Tool renderers** — `api.RegisterToolRenderer(ext.ToolRenderConfig{...})`\n- **Message renderers** — `api.RegisterMessageRenderer(ext.MessageRendererConfig{...})`\n- **Options** — `api.RegisterOption(ext.OptionDef{...})`\n\n### The `Context` object\n\nPassed to event handlers, the `Context` object provides runtime access to Kit\'s state and UI:\n\n- **Output** — `ctx.Print(...)`, `ctx.PrintInfo(...)`, `ctx.PrintError(...)`\n- **UI components** — `ctx.SetWidget(...)`, `ctx.SetHeader(...)`, `ctx.SetFooter(...)`, `ctx.SetStatus(...)`\n- **Editor** — `ctx.SetEditor(...)`, `ctx.ResetEditor()`\n- **Prompts** — `ctx.PromptSelect(...)`, `ctx.PromptConfirm(...)`, `ctx.PromptInput(...)`\n- **Overlays** — `ctx.ShowOverlay(...)`\n- **Messages** — `ctx.SendMessage(...)`, `ctx.GetMessages()`\n- **Model** — `ctx.SetModel(...)`, `ctx.GetAvailableModels()`\n- **Tools** — `ctx.GetAllTools()`, `ctx.SetActiveTools(...)`\n- **Context stats** — `ctx.GetContextStats()`\n- **Session data** — `ctx.AppendEntry(...)`, `ctx.GetEntries(...)` (append-only, in conversation tree)\n- **Session state** — `ctx.SetState(...)`, `ctx.GetState(...)`, `ctx.DeleteState(...)`, `ctx.ListState()` (last-write-wins, sidecar file)\n- **Subagents** — `ctx.SpawnSubagent(...)`\n- **LLM completion** — `ctx.Complete(...)`\n- **Custom events** — `ctx.EmitCustomEvent(...)`\n\nSee [Capabilities](/extensions/capabilities) for full details on each component type, and [Testing](/extensions/testing) for writing tests for your extensions.\n'};export{easdefault};
consts={frontmatter:{title:"Go SDK",description:"Embed Kit in your Go applications.",hidden:!1,toc:!0,draft:!1},html:`<h1 id="go-sdk"><a class="heading-anchor" aria-hidden="" tabindex="-1" href="#go-sdk"><span class="icon icon-link"></span></a>Go SDK</h1>
<p>The <code>pkg/kit</code> package lets you embed Kit as a library in your Go applications.</p>
<td>Quick programmatic setups; you only need the common fields. Streaming defaults on.</td>
</tr>
<tr>
<td><code>kit.New(ctx, *Options)</code></td>
<td>You need fields without a <code>With*</code> helper (<code>MCPConfig</code>, <code>InProcessMCPServers</code>, <code>SessionManager</code>, MCP task tuning, etc.), or you already hold an <code>Options</code> value.</td>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// a.GetThinkingLevel() == "medium"; b.GetThinkingLevel() is still "high"</span></span></code></pre>
<td>Non-image media result (e.g. <code>"audio/mpeg"</code>)</td>
</tr>
</tbody>
</table>
<p>Binary data (images, audio, etc.) in <code>ToolOutput.Data</code> is automatically forwarded to the LLM when <code>MediaType</code> is set. For advanced use, return a <code>kit.ToolOutput</code> struct directly with <code>Data</code>, <code>MediaType</code>, and <code>Metadata</code> fields.</p>
<p>Use <code>kit.NewParallelTool</code> for tools that are safe to run concurrently. Use <code>kit.ToolCallIDFromContext(ctx)</code> to retrieve the LLM-assigned call ID for logging or tracing.</p>
<p>Kit auto-discovers skills and <code>AGENTS.md</code>-style context files during <code>New()</code>,
but multi-tenant hosts (chatbots, web services, per-user agents) often need
to swap these <strong>after</strong> construction. The runtime mutators below recompose
the system prompt and apply it to the agent so the next turn picks up the
updated instructions — no restart, no file shuffling.</p>
<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// Add a programmatic skill — no file on disk required.</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8"> Description: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Respond in French and always greet the user."</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8"> Content: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Always reply in French. Open every response with 'Bonjour'."</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8"> Prompt: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Analyze the test files"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<p>See <a href="/sdk/options">Options</a>, <a href="/sdk/callbacks">Callbacks</a>, and <a href="/sdk/sessions">Sessions</a> for more details.</p>`,headings:[{depth:2,text:"Installation",id:"installation"},{depth:2,text:"Basic usage",id:"basic-usage"},{depth:2,text:"Functional options (NewAgent)",id:"functional-options-newagent"},{depth:3,text:"When to use which",id:"when-to-use-which"},{depth:2,text:"Per-instance config isolation",id:"per-instance-config-isolation"},{depth:2,text:"Multi-turn conversations",id:"multi-turn-conversations"},{depth:2,text:"Additional prompt methods",id:"additional-prompt-methods"},{depth:2,text:"Custom tools",id:"custom-tools"},{depth:2,text:"Generation & provider overrides",id:"generation--provider-overrides"},{depth:2,text:"Event system",id:"event-system"},{depth:2,text:"Model management",id:"model-management"},{depth:2,text:"Dynamic MCP servers",id:"dynamic-mcp-servers"},{depth:3,text:"In-process MCP servers",id:"in-process-mcp-servers"},{depth:2,text:"Runtime skills and context files",id:"runtime-skills-and-context-files"},{depth:2,text:"MCP prompts and resources",id:"mcp-prompts-and-resources"},{depth:2,text:"MCP tasks (long-running tools)",id:"mcp-tasks-long-running-tools"},{depth:2,text:"Context and compaction",id:"context-and-compaction"},{depth:2,text:"In-process subagents",id:"in-process-subagents"}],raw:`
# Go SDK
The \`pkg/kit\` package lets you embed Kit as a library in your Go applications.
## Installation
\`\`\`bash
go get github.com/mark3labs/kit/pkg/kit
\`\`\`
## Basic usage
\`\`\`go
package main
import (
"context"
"log"
kit "github.com/mark3labs/kit/pkg/kit"
)
func main() {
ctx := context.Background()
// Create Kit instance with default configuration
host, err := kit.New(ctx, nil)
if err != nil {
log.Fatal(err)
}
defer host.Close()
// Send a prompt
response, err := host.Prompt(ctx, "What is 2+2?")
if err != nil {
log.Fatal(err)
}
println(response)
}
\`\`\`
## Functional options (\`NewAgent\`)
For simple programmatic setups, \`kit.NewAgent\` offers an ergonomic
functional-options front door over \`kit.New\`. Streaming is **enabled by
default**; pass \`kit.WithStreaming(false)\` to opt out.
Options are applied in order, so later options override earlier ones. \`Option\`
is a plain \`func(*Options)\`, so you can define your own. For advanced
configuration not covered by the helpers (custom MCP config, in-process MCP
servers, session backends, MCP task tuning) construct an \`Options\` value
explicitly and call \`kit.New\`.
### When to use which
| Constructor | Use when |
|-------------|----------|
| \`kit.NewAgent(ctx, ...Option)\` | Quick programmatic setups; you only need the common fields. Streaming defaults on. |
| \`kit.New(ctx, *Options)\` | You need fields without a \`With*\` helper (\`MCPConfig\`, \`InProcessMCPServers\`, \`SessionManager\`, MCP task tuning, etc.), or you already hold an \`Options\` value. |
## Per-instance config isolation
Each \`kit.New\` / \`kit.NewAgent\` call owns an **isolated configuration store**,
so constructing multiple Kit instances in the same process is safe: setting the
model, thinking level, or generation parameters on one never affects another,
and runtime mutators (\`SetModel\`, \`SetThinkingLevel\`) only touch the owning
instance. This makes subagent spawning and multi-Kit embedding race-free with
no external synchronization required.
\`\`\`go
a, _ := kit.NewAgent(ctx, kit.WithThinkingLevel("low"))
return kit.TextResult("72°F, sunny in " + input.City), nil
},
)
host, _ := kit.New(ctx, &kit.Options{
ExtraTools: []kit.Tool{weatherTool},
})
\`\`\`
Struct tags control the schema:
- \`json:"name"\` — parameter name
- \`description:"..."\` — description shown to the LLM
- \`enum:"a,b,c"\` — restrict valid values
- \`omitempty\` — marks the parameter as optional
Return values:
| Helper | Description |
|--------|-------------|
| \`kit.TextResult(s)\` | Successful text result |
| \`kit.ErrorResult(s)\` | Error result (LLM sees it as a tool error) |
| \`kit.ImageResult(s, data, mediaType)\` | Image result with binary data (e.g. \`"image/png"\`) |
| \`kit.MediaResult(s, data, mediaType)\` | Non-image media result (e.g. \`"audio/mpeg"\`) |
Binary data (images, audio, etc.) in \`ToolOutput.Data\` is automatically forwarded to the LLM when \`MediaType\` is set. For advanced use, return a \`kit.ToolOutput\` struct directly with \`Data\`, \`MediaType\`, and \`Metadata\` fields.
Use \`kit.NewParallelTool\` for tools that are safe to run concurrently. Use \`kit.ToolCallIDFromContext(ctx)\` to retrieve the LLM-assigned call ID for logging or tracing.
## Generation & provider overrides
SDK consumers can configure generation parameters and provider endpoints
entirely in-code via \`Options\`, without touching \`.kit.yml\` or \`viper.Set()\`:
<p>The <code>custom/custom</code> model has zero cost, 262K context window, and supports reasoning. It routes through the <code>openaicompat</code> provider and accepts any OpenAI-compatible API endpoint.</p>
<p>Optionally set <code>CUSTOM_API_KEY</code> environment variable or use <code>--provider-api-key</code> for endpoints requiring authentication.</p>
The \`custom/custom\` model has zero cost, 262K context window, and supports reasoning. It routes through the \`openaicompat\` provider and accepts any OpenAI-compatible API endpoint.
Optionally set \`CUSTOM_API_KEY\` environment variable or use \`--provider-api-key\` for endpoints requiring authentication.
## Auto-routed providers
Any provider in the [models.dev](https://models.dev) database can be used with the
standard \`provider/model\` format, even without a dedicated native integration. Kit
auto-routes the request through the matching **wire protocol** — the actual API
shape the provider speaks — rather than requiring a per-provider code path:
| Wire protocol | npm package (models.dev) | Transport used |
consts={frontmatter:{title:"Quick Start",description:"Get up and running with Kit in minutes.",hidden:!1,toc:!0,draft:!1},html:`<h1 id="quick-start"><a class="heading-anchor" aria-hidden="" tabindex="-1" href="#quick-start"><span class="icon icon-link"></span></a>Quick Start</h1>
<p>Binary files (images, audio, PDFs) are automatically detected via MIME type and sent as multimodal attachments. You can also reference MCP resources:</p>
<h2 id="acp-server-mode"><a class="heading-anchor" aria-hidden="" tabindex="-1" href="#acp-server-mode"><span class="icon icon-link"></span></a>ACP server mode</h2>
<p>Kit can run as an <a href="https://agentclientprotocol.com">ACP (Agent Client Protocol)</a> agent server, enabling ACP-compatible clients (such as <a href="https://github.com/sst/opencode">OpenCode</a>) to drive Kit as a remote coding agent over stdio:</p>
<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D"># Start Kit as an ACP server (JSON-RPC 2.0 on stdin/stdout)</span></span>
<p>The ACP server exposes Kit's full capabilities — LLM execution, tool calls (bash, read, write, edit, grep, etc.), and session persistence — over the standard ACP protocol.</p>`,headings:[{depth:2,text:"Basic usage",id:"basic-usage"},{depth:2,text:"Non-interactive mode",id:"non-interactive-mode"},{depth:2,text:"Resuming sessions",id:"resuming-sessions"},{depth:2,text:"ACP server mode",id:"acp-server-mode"}],raw:`
# Quick Start
## Basic usage
Start an interactive session:
\`\`\`bash
kit
\`\`\`
Run a one-off prompt:
\`\`\`bash
kit "List files in src/"
\`\`\`
Attach files as context using the \`@\` prefix:
\`\`\`bash
kit @main.go @test.go "Review these files"
\`\`\`
Binary files (images, audio, PDFs) are automatically detected via MIME type and sent as multimodal attachments. You can also reference MCP resources:
\`\`\`bash
kit @mcp:myserver:file:///data/report.csv "Summarize this data"
\`\`\`
Use a specific model:
\`\`\`bash
kit --model anthropic/claude-sonnet-latest
\`\`\`
## Non-interactive mode
Kit can run as a non-interactive tool for scripting and automation.
Get JSON output:
\`\`\`bash
kit "Explain main.go" --json
\`\`\`
Quiet mode (final response only, no TUI):
\`\`\`bash
kit "Run tests" --quiet
\`\`\`
Ephemeral mode (no session file created):
\`\`\`bash
kit "Quick question" --no-session
\`\`\`
## Resuming sessions
Continue the most recent session for the current directory:
\`\`\`bash
kit --continue
# or
kit -c
\`\`\`
Pick from previous sessions interactively:
\`\`\`bash
kit --resume
# or
kit -r
\`\`\`
## ACP server mode
Kit can run as an [ACP (Agent Client Protocol)](https://agentclientprotocol.com) agent server, enabling ACP-compatible clients (such as [OpenCode](https://github.com/sst/opencode)) to drive Kit as a remote coding agent over stdio:
\`\`\`bash
# Start Kit as an ACP server (JSON-RPC 2.0 on stdin/stdout)
kit acp
# With debug logging to stderr
kit acp --debug
\`\`\`
The ACP server exposes Kit's full capabilities — LLM execution, tool calls (bash, read, write, edit, grep, etc.), and session persistence — over the standard ACP protocol.
<p>Path separators in the working directory are replaced with <code>--</code>. For example, <code>/home/user/project</code> becomes <code>home--user--project</code>.</p>
<p>Each line in the session file is a JSON entry representing a message, tool call, model change, or extension data. The tree structure allows branching from any message to explore alternate paths.</p>
<p>Kit automatically cleans up empty sessions on shutdown and when using <code>/resume</code>. A session is considered empty if it has no messages beyond the initial system prompt. This prevents cluttering your sessions directory with unused files.</p>
<p>To start fresh without creating a session file at all, use ephemeral mode:</p>
<p>The session picker supports search, scope/filter toggles (all sessions vs. current directory), and session deletion. You can also open it during a session with the <code>/resume</code> slash command.</p>
<h3 id="open-a-specific-session"><a class="heading-anchor" aria-hidden="" tabindex="-1" href="#open-a-specific-session"><span class="icon icon-link"></span></a>Open a specific session</h3>
<p>The <code>/share</code> command uploads your session JSONL to GitHub Gist (via the <code>gh</code> CLI) and prints a shareable viewer URL:</p>
<pre><code>/share
</code></pre>
<p>The shared session includes:</p>
<ul>
<li>The <strong>system prompt</strong> that was active during the conversation</li>
<li>The <strong>model</strong> used (e.g., <code>anthropic/claude-sonnet-4-5</code>)</li>
</ul>
<p>The viewer displays this information in a collapsible "System Prompt" section at the top of the session, with the model shown as a badge in the header.</p>
<p>The viewer is available at <code>https://go-kit.dev/session/#GIST_ID</code> and supports all message types including text, reasoning blocks, tool calls, images, and model changes.</p>
<p>You can also load any JSONL session via URL parameter: <code>https://go-kit.dev/session/?url=https://example.com/session.jsonl</code></p>
<p>Kit automatically saves your preferences across sessions to <code>~/.config/kit/preferences.yml</code>:</p>
<ul>
<li><strong>Theme</strong> — Set via <code>/theme <name></code></li>
<li><strong>Model</strong> — Set via <code>/model <name></code> or the model selector</li>
<li><strong>Thinking level</strong> — Set via <code>/thinking <level></code> or Shift+Tab cycling</li>
</ul>
<p>These preferences are restored on next launch. Precedence: CLI flag > config file > saved preference > default.</p>`,headings:[{depth:2,text:"Session storage",id:"session-storage"},{depth:2,text:"Compaction",id:"compaction"},{depth:2,text:"Auto-cleanup",id:"auto-cleanup"},{depth:2,text:"Resuming sessions",id:"resuming-sessions"},{depth:3,text:"Continue most recent",id:"continue-most-recent"},{depth:3,text:"Interactive picker",id:"interactive-picker"},{depth:3,text:"Open a specific session",id:"open-a-specific-session"},{depth:2,text:"Session commands",id:"session-commands"},{depth:2,text:"Ephemeral mode",id:"ephemeral-mode"},{depth:2,text:"Sharing sessions",id:"sharing-sessions"},{depth:2,text:"Preferences persistence",id:"preferences-persistence"}],raw:`
# Session Management
Kit uses a tree-based session model that supports branching and forking conversations.
## Session storage
Sessions are stored as JSONL (JSON Lines) files:
\`\`\`
~/.kit/sessions/<cwd-path>/<timestamp>_<id>.jsonl
\`\`\`
Path separators in the working directory are replaced with \`--\`. For example, \`/home/user/project\` becomes \`home--user--project\`.
Each line in the session file is a JSON entry representing a message, tool call, model change, or extension data. The tree structure allows branching from any message to explore alternate paths.
## Compaction
When conversations grow long, Kit can compact them to free up context window space. The compaction system:
- **Non-destructive**: Old messages remain on disk for history; only the LLM context is summarized
- **File tracking**: Tracks which files were read and modified across compactions
- **Split-turn handling**: Can summarize large single turns by splitting them
- **Tool result truncation**: Caps tool output during serialization to stay within token budgets
Use \`/compact [focus]\` to manually compact, or enable \`--auto-compact\` to compact automatically near the context limit.
## Auto-cleanup
Kit automatically cleans up empty sessions on shutdown and when using \`/resume\`. A session is considered empty if it has no messages beyond the initial system prompt. This prevents cluttering your sessions directory with unused files.
To start fresh without creating a session file at all, use ephemeral mode:
\`\`\`bash
kit --no-session
\`\`\`
## Resuming sessions
### Continue most recent
\`\`\`bash
kit --continue
kit -c
\`\`\`
### Interactive picker
Choose from previous sessions interactively:
\`\`\`bash
kit --resume
kit -r
\`\`\`
The session picker supports search, scope/filter toggles (all sessions vs. current directory), and session deletion. You can also open it during a session with the \`/resume\` slash command.
### Open a specific session
\`\`\`bash
kit --session path/to/session.jsonl
kit -s path/to/session.jsonl
\`\`\`
## Session commands
These slash commands are available during an interactive session:
| Command | Description |
|---------|-------------|
| \`/name [name]\` | Set or display the session's display name |
| \`/session\` | Show session info (path, ID, message count) |
| \`/resume\` | Open the session picker to switch sessions |
| \`/export [path]\` | Export session as JSONL (auto-generates path if omitted) |
| \`/import <path>\` | Import and switch to a session from a JSONL file |
| \`/share\` | Upload session to GitHub Gist and get a shareable viewer URL |
| \`/tree\` | Navigate the session tree |
| \`/fork\` | Fork to new session from an earlier message (creates new session file) |
| \`/new\` | Start a new session (creates new session file) |
## Ephemeral mode
Run without creating a session file:
\`\`\`bash
kit --no-session
\`\`\`
This is useful for one-off prompts, scripting, and subagent patterns where persistence isn't needed.
## Sharing sessions
The \`/share\` command uploads your session JSONL to GitHub Gist (via the \`gh\` CLI) and prints a shareable viewer URL:
\`\`\`
/share
\`\`\`
The shared session includes:
- The **system prompt** that was active during the conversation
- The **model** used (e.g., \`anthropic/claude-sonnet-4-5\`)
The viewer displays this information in a collapsible "System Prompt" section at the top of the session, with the model shown as a badge in the header.
The viewer is available at \`https://go-kit.dev/session/#GIST_ID\` and supports all message types including text, reasoning blocks, tool calls, images, and model changes.
You can also load any JSONL session via URL parameter: \`https://go-kit.dev/session/?url=https://example.com/session.jsonl\`
## Preferences persistence
Kit automatically saves your preferences across sessions to \`~/.config/kit/preferences.yml\`:
- **Theme** — Set via \`/theme <name>\`
- **Model** — Set via \`/model <name>\` or the model selector
- **Thinking level** — Set via \`/thinking <level>\` or Shift+Tab cycling
These preferences are restored on next launch. Precedence: CLI flag > config file > saved preference > default.
<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// Get the current session file path</span></span>
<p>Session behavior is configured at initialization:</p>
<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// Open a specific session file</span></span>
<p>Package-level functions for session discovery:</p>
<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// List sessions for a specific directory</span></span>
<p>For advanced use cases (databases, cloud storage, multi-user apps), implement the <code>SessionManager</code> interface to replace the default JSONL file backend:</p>
<p>The interface requires methods for message storage, branching, compaction, extension data, and lifecycle management. See the <a href="https://github.com/mark3labs/kit">SDK skill reference</a> for the complete interface definition.</p>
<p>When using a custom <code>SessionManager</code>, the <code>SessionPath</code>, <code>Continue</code>, and <code>NoSession</code> options are ignored — your manager handles its own storage and session selection.</p>`,headings:[{depth:2,text:"Automatic persistence",id:"automatic-persistence"},{depth:2,text:"Accessing session info",id:"accessing-session-info"},{depth:2,text:"Configuring sessions via Options",id:"configuring-sessions-via-options"},{depth:2,text:"Clearing history",id:"clearing-history"},{depth:2,text:"Tree-based sessions",id:"tree-based-sessions"},{depth:2,text:"Listing and managing sessions",id:"listing-and-managing-sessions"},{depth:2,text:"Custom session manager",id:"custom-session-manager"}],raw:`
# SDK Sessions
## Automatic persistence
By default, Kit automatically persists sessions to JSONL files. Multi-turn conversations retain context across calls:
\`\`\`go
host.Prompt(ctx, "My name is Alice")
response, _ := host.Prompt(ctx, "What's my name?")
// response: "Your name is Alice"
\`\`\`
## Accessing session info
\`\`\`go
// Get the current session file path
path := host.GetSessionPath()
// Get the session ID
id := host.GetSessionID()
// Get the current model string
model := host.GetModelString()
\`\`\`
## Configuring sessions via Options
Session behavior is configured at initialization:
\`\`\`go
// Open a specific session file
host, _ := kit.New(ctx, &kit.Options{
SessionPath: "./my-session.jsonl",
})
// Resume the most recent session for the current directory
host, _ := kit.New(ctx, &kit.Options{
Continue: true,
})
// Ephemeral mode (no file persistence)
host, _ := kit.New(ctx, &kit.Options{
NoSession: true,
})
// Custom session directory
host, _ := kit.New(ctx, &kit.Options{
SessionDir: "/custom/sessions/",
})
\`\`\`
## Clearing history
Clear the in-memory conversation history (does not delete the session file):
\`\`\`go
host.ClearSession()
\`\`\`
## Tree-based sessions
Kit's session model is tree-based, supporting branching. You can branch from any entry to explore alternate conversation paths:
For advanced use cases (databases, cloud storage, multi-user apps), implement the \`SessionManager\` interface to replace the default JSONL file backend:
\`\`\`go
host, _ := kit.New(ctx, &kit.Options{
SessionManager: myCustomSession,
})
\`\`\`
The interface requires methods for message storage, branching, compaction, extension data, and lifecycle management. See the [SDK skill reference](https://github.com/mark3labs/kit) for the complete interface definition.
When using a custom \`SessionManager\`, the \`SessionPath\`, \`Continue\`, and \`NoSession\` options are ignored — your manager handles its own storage and session selection.
system_prompt: "You are a test analysis expert.", // optional
timeout_seconds: 300 // optional, max 1800
)
</code></pre>
<p>Subagents run as separate in-process Kit instances with full tool access (except spawning further subagents, to prevent infinite recursion). They can run in parallel.</p>
<p>When the LLM (not the extension itself) spawns a subagent using the <code>subagent</code> tool, extensions can monitor its activity in real-time using three lifecycle event handlers:</p>
<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// Track active subagents and display their output</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8"> ToolCallID </span><span style="color:#D73A49;--shiki-dark:#F97583">string</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // Unique ID for this subagent invocation</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8"> Task </span><span style="color:#D73A49;--shiki-dark:#F97583">string</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // The task/prompt sent to subagent</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8"> Task: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Summarize the changes in this PR"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<p>Use <code>SubscribeSubagent</code> to receive real-time events from LLM-initiated subagents (i.e., when the model uses the <code>subagent</code> tool). Register inside an <code>OnToolCall</code> handler using the tool call ID:</p>
<p>The listener receives the same event types as <code>Subscribe()</code> (<code>ToolCallEvent</code>, <code>MessageUpdateEvent</code>, <code>ReasoningDeltaEvent</code>, etc.) but scoped to the child agent's activity. Listeners are cleaned up automatically when the subagent completes.</p>
<p>If no listeners are registered for a tool call, no event dispatching overhead is incurred.</p>`,headings:[{depth:2,text:"Subprocess pattern",id:"subprocess-pattern"},{depth:2,text:"Built-in subagent tool",id:"built-in-subagent-tool"},{depth:2,text:"Extension subagents",id:"extension-subagents"},{depth:3,text:"Monitoring subagents from extensions",id:"monitoring-subagents-from-extensions"},{depth:2,text:"Go SDK subagents",id:"go-sdk-subagents"},{depth:3,text:"Real-time subagent events",id:"real-time-subagent-events"}],raw:`
# Subagents
Kit supports multi-agent orchestration through both subprocess spawning and in-process subagents.
## Subprocess pattern
Spawn Kit as a subprocess for isolated agent execution:
system_prompt: "You are a test analysis expert.", // optional
timeout_seconds: 300 // optional, max 1800
)
\`\`\`
Subagents run as separate in-process Kit instances with full tool access (except spawning further subagents, to prevent infinite recursion). They can run in parallel.
## Extension subagents
Extensions can spawn subagents programmatically:
\`\`\`go
result := ctx.SpawnSubagent(ext.SubagentConfig{
Task: "Review this code for security issues",
Model: "anthropic/claude-sonnet-latest",
SystemPrompt: "You are a security auditor.",
})
\`\`\`
### Monitoring subagents from extensions
When the LLM (not the extension itself) spawns a subagent using the \`subagent\` tool, extensions can monitor its activity in real-time using three lifecycle event handlers:
\`\`\`go
// Track active subagents and display their output
Use \`SubscribeSubagent\` to receive real-time events from LLM-initiated subagents (i.e., when the model uses the \`subagent\` tool). Register inside an \`OnToolCall\` handler using the tool call ID:
The listener receives the same event types as \`Subscribe()\` (\`ToolCallEvent\`, \`MessageUpdateEvent\`, \`ReasoningDeltaEvent\`, etc.) but scoped to the child agent's activity. Listeners are cleaned up automatically when the subagent completes.
If no listeners are registered for a tool call, no event dispatching overhead is incurred.
consts={frontmatter:{title:"Testing Extensions",description:"Write unit tests for your Kit extensions using the test package.",hidden:!1,toc:!0,draft:!1},html:`<h1 id="testing-extensions"><a class="heading-anchor" aria-hidden="" tabindex="-1" href="#testing-extensions"><span class="icon icon-link"></span></a>Testing Extensions</h1>
<p>Kit provides a testing package (<code>github.com/mark3labs/kit/pkg/extensions/test</code>) that enables you to write unit tests for your extensions. Tests run outside the Yaegi interpreter but load your extension code into an isolated interpreter instance, allowing you to verify behavior without running the full Kit TUI.</p>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D"> // Now when your extension calls ctx.PromptSelect(), it gets this result</span></span>
</ul>`,headings:[{depth:2,text:"Overview",id:"overview"},{depth:2,text:"Installation",id:"installation"},{depth:2,text:"Basic Usage",id:"basic-usage"},{depth:3,text:"Testing an Extension File",id:"testing-an-extension-file"},{depth:3,text:"Testing Inline Extension Code",id:"testing-inline-extension-code"},{depth:2,text:"Common Testing Patterns",id:"common-testing-patterns"},{depth:3,text:"Testing Handler Registration",id:"testing-handler-registration"},{depth:3,text:"Testing Tool Registration",id:"testing-tool-registration"},{depth:3,text:"Testing Commands",id:"testing-commands"},{depth:3,text:"Testing Widgets",id:"testing-widgets"},{depth:3,text:"Testing Input Handling",id:"testing-input-handling"},{depth:3,text:"Testing Headers and Footers",id:"testing-headers-and-footers"},{depth:3,text:"Testing Status Bar",id:"testing-status-bar"},{depth:3,text:"Testing Print Output",id:"testing-print-output"},{depth:3,text:"Testing with Prompts",id:"testing-with-prompts"},{depth:3,text:"Testing Complete Session Flow",id:"testing-complete-session-flow"},{depth:2,text:"Available Assertions",id:"available-assertions"},{depth:3,text:"Event Results",id:"event-results"},{depth:3,text:"Context Interactions",id:"context-interactions"},{depth:3,text:"Registration",id:"registration"},{depth:3,text:"Messaging",id:"messaging"},{depth:2,text:"Helper Functions",id:"helper-functions"},{depth:2,text:"Advanced Usage",id:"advanced-usage"},{depth:3,text:"Accessing the Mock Context",id:"accessing-the-mock-context"},{depth:3,text:"Testing Multiple Extensions",id:"testing-multiple-extensions"},{depth:3,text:"Running Tests",id:"running-tests"},{depth:2,text:"Best Practices",id:"best-practices"},{depth:2,text:"Limitations",id:"limitations"},{depth:2,text:"Complete Example",id:"complete-example"}],raw:`
# Testing Extensions
Kit provides a testing package (\`github.com/mark3labs/kit/pkg/extensions/test\`) that enables you to write unit tests for your extensions. Tests run outside the Yaegi interpreter but load your extension code into an isolated interpreter instance, allowing you to verify behavior without running the full Kit TUI.
## Overview
Extension tests allow you to:
- Test event handlers without running the interactive TUI
- Verify tool/command registration
- Assert that context methods (Print, SetWidget, etc.) are called correctly
- Test blocking and non-blocking event handling
- Simulate user input and tool calls
- Verify widget, header, footer, and status bar updates
## Installation
The test package is part of the Kit codebase. Import it in your extension tests:
\`\`\`go
import (
"testing"
"github.com/mark3labs/kit/pkg/extensions/test"
"github.com/mark3labs/kit/internal/extensions"
)
\`\`\`
## Basic Usage
### Testing an Extension File
Create a test file alongside your extension (e.g., \`my-ext_test.go\`):
consts={frontmatter:{title:"Testing with tmux",description:"Test Kit's TUI non-interactively using tmux.",hidden:!1,toc:!0,draft:!1},html:`<h1 id="testing-with-tmux"><a class="heading-anchor" aria-hidden="" tabindex="-1" href="#testing-with-tmux"><span class="icon icon-link"></span></a>Testing with tmux</h1>
<p>Kit's interactive TUI can be tested non-interactively using tmux. This is useful for automated testing, CI pipelines, and extension development.</p>
consts={frontmatter:{title:"Themes",description:"Customize Kit's appearance with built-in themes, custom theme files, and the extension theme API.",hidden:!1,toc:!0,draft:!1},html:`<h1 id="themes"><a class="heading-anchor" aria-hidden="" tabindex="-1" href="#themes"><span class="icon icon-link"></span></a>Themes</h1>
<p>Kit ships with 22 built-in color themes and supports custom themes via YAML/JSON files or the extension API. Themes control all UI colors: input borders, popups, system messages, markdown rendering, syntax highlighting, and diff displays.</p>
<p>Switch themes at runtime with the <code>/theme</code> command:</p>
<pre><code>/theme dracula
/theme catppuccin
/theme kitt
</code></pre>
<p>Run <code>/theme</code> with no arguments to list all available themes.</p>
<p><strong>Theme selections are automatically saved</strong> to <code>~/.config/kit/preferences.yml</code> and restored on next launch. You don't need to add anything to your config file — just <code>/theme <name></code> and it sticks. This persistence also applies to <strong>model</strong> and <strong>thinking level</strong> selections.</p>
<p>A theme file defines adaptive color pairs with <code>light</code> and <code>dark</code> hex values. Any field left empty inherits from the default KITT theme.</p>
<p>Each field is an <code>ext.ThemeColor</code> with <code>Light</code> and <code>Dark</code> hex strings. Empty fields inherit from the default theme.</p>
<p>The preferences file is updated automatically whenever you use <code>/theme</code> or <code>ctx.SetTheme()</code>. It is separate from <code>.kit.yml</code> so it never clobbers your config comments or formatting.</p>
<p>Theme changes via <code>/theme</code> or <code>ctx.SetTheme()</code> take effect immediately on all UI elements, including previously rendered messages.</p>`,headings:[{depth:2,text:"Quick start",id:"quick-start"},{depth:2,text:"Built-in themes",id:"built-in-themes"},{depth:2,text:"Custom theme files",id:"custom-theme-files"},{depth:3,text:"Theme file format",id:"theme-file-format"},{depth:3,text:"Partial themes",id:"partial-themes"},{depth:3,text:"Distributing themes",id:"distributing-themes"},{depth:2,text:"Config file theme",id:"config-file-theme"},{depth:2,text:"Extension theme API",id:"extension-theme-api"},{depth:3,text:"Registering a theme",id:"registering-a-theme"},{depth:3,text:"Switching themes",id:"switching-themes"},{depth:3,text:"Listing available themes",id:"listing-available-themes"},{depth:3,text:"ThemeColorConfig fields",id:"themecolorconfig-fields"},{depth:2,text:"Precedence order",id:"precedence-order"},{depth:3,text:"Startup theme resolution",id:"startup-theme-resolution"}],raw:`
# Themes
Kit ships with 22 built-in color themes and supports custom themes via YAML/JSON files or the extension API. Themes control all UI colors: input borders, popups, system messages, markdown rendering, syntax highlighting, and diff displays.
## Quick start
Switch themes at runtime with the \`/theme\` command:
\`\`\`
/theme dracula
/theme catppuccin
/theme kitt
\`\`\`
Run \`/theme\` with no arguments to list all available themes.
**Theme selections are automatically saved** to \`~/.config/kit/preferences.yml\` and restored on next launch. You don't need to add anything to your config file — just \`/theme <name>\` and it sticks. This persistence also applies to **model** and **thinking level** selections.
## Built-in themes
| Theme | Style |
|-------|-------|
| \`kitt\` | KITT-inspired reds and ambers (default) |
The preferences file is updated automatically whenever you use \`/theme\` or \`ctx.SetTheme()\`. It is separate from \`.kit.yml\` so it never clobbers your config comments or formatting.
Theme changes via \`/theme\` or \`ctx.SetTheme()\` take effect immediately on all UI elements, including previously rendered messages.
<p>You can close this window and return to the terminal.</p>
</body>
</html>`)
})
// Try to start server
listener,err:=net.Listen("tcp","127.0.0.1:1455")
iferr!=nil{
returnnil,fmt.Errorf("port 1455 not available: %w",err)
}
_=listener.Close()
gofunc(){
_=server.ListenAndServe()
}()
return&callbackServer{
Server:server,
CodeChan:codeChan,
State:expectedState,
},nil
}
funclogoutOpenAI()error{
cm,err:=kit.NewCredentialManager()
iferr!=nil{
returnfmt.Errorf("failed to initialize credential manager: %w",err)
}
// Check if authenticated
hasAuth,err:=cm.HasOpenAICredentials()
iferr!=nil{
returnfmt.Errorf("failed to check authentication status: %w",err)
}
if!hasAuth{
fmt.Println("You are not currently authenticated with OpenAI.")
returnnil
}
// Confirm logout
varconfirmbool
err=huh.NewConfirm().
Title("Remove OpenAI credentials").
Description("Are you sure you want to remove your stored credentials?").
Affirmative("Yes").
Negative("No").
Value(&confirm).
Run()
iferr!=nil||!confirm{
fmt.Println("Logout cancelled.")
returnnil
}
// Remove credentials
iferr:=cm.RemoveOpenAICredentials();err!=nil{
returnfmt.Errorf("failed to remove credentials: %w",err)
}
fmt.Println("✓ Successfully logged out from OpenAI!")
fmt.Println("You will need to use environment variables or command-line flags for authentication.")
returnnil
}
funclogoutCopilot()error{
cm,err:=kit.NewCredentialManager()
iferr!=nil{
returnfmt.Errorf("failed to initialize credential manager: %w",err)
}
hasAuth,err:=cm.HasCopilotCredentials()
iferr!=nil{
returnfmt.Errorf("failed to check authentication status: %w",err)
}
if!hasAuth{
fmt.Println("You are not currently authenticated with GitHub Copilot.")
returnnil
}
varconfirmbool
err=huh.NewConfirm().
Title("Remove GitHub Copilot credentials").
Description("Are you sure you want to remove your stored credentials?").
Affirmative("Yes").
Negative("No").
Value(&confirm).
Run()
iferr!=nil||!confirm{
fmt.Println("Logout cancelled.")
returnnil
}
iferr:=cm.RemoveCopilotCredentials();err!=nil{
returnfmt.Errorf("failed to remove credentials: %w",err)
}
fmt.Println("✓ Successfully logged out from GitHub Copilot!")
fmt.Println("You will need to authenticate again with 'kit auth login copilot'.")
fmt.Println("Tip: this removes local credentials only. Revoke the GitHub OAuth grant at https://github.com/settings/applications")
returnnil
}
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.