* 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.
- 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
- 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)
- 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
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.
- 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
- 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.
- 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
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.
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.
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 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)
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.
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.
- 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).
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
- 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
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.
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.