- 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.
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.
- 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
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
- 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
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
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.
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.
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
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
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.
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
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.
Relocate SDK usage examples to the top-level examples directory alongside
extension examples for better discoverability. Add README for the SDK
examples directory.
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.
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
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
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.
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.
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.
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.
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.
- 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
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)
- 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