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