Crush — Interfaces#
Interface catalog#
Workspace#
- Package:
github.com/charmbracelet/crush/internal/workspace - File:
internal/workspace/workspace.go - Methods (grouped):
- Sessions (7):
CreateSession,GetSession,ListSessions,SaveSession,DeleteSession,CreateAgentToolSessionID,ParseAgentToolSessionID - Messages (3):
ListMessages,ListUserMessages,ListAllUserMessages - Agent (14):
AgentRun,AgentCancel,AgentIsBusy,AgentIsSessionBusy,AgentModel,AgentIsReady,AgentQueuedPrompts,AgentQueuedPromptsList,AgentClearQueue,AgentSummarize,UpdateAgentModel,InitCoderAgent,GetDefaultSmallModel - Permissions (5):
PermissionGrant,PermissionGrantPersistent,PermissionDeny,PermissionSkipRequests,PermissionSetSkipRequests - FileTracker (3):
FileTrackerRecordRead,FileTrackerLastReadTime,FileTrackerListReadFiles - History (1):
ListSessionHistory - LSP (4):
LSPStart,LSPStopAll,LSPGetStates,LSPGetDiagnosticCounts - Config (8):
Config,WorkingDir,Resolver,UpdatePreferredModel,SetCompactMode,SetProviderAPIKey,SetConfigField,RemoveConfigField,ImportCopilot,RefreshOAuthToken - Project lifecycle (3):
ProjectNeedsInitialization,MarkProjectInitialized,InitializePrompt - MCP (7):
MCPGetStates,MCPRefreshPrompts,MCPRefreshResources,RefreshMCPTools,ReadMCPResource,GetMCPPrompt,EnableDockerMCP,DisableDockerMCP - Events (2):
Subscribe,Shutdown
- Sessions (7):
- Purpose: The singular seam between any frontend (TUI,
crush run, IDE) and the backend. Abstracts whether the implementation is in-process or remote over a Unix socket. This is the most important interface in the project. - Implementations:
AppWorkspace(internal/app) — in-process;ClientWorkspace(internal/client) — HTTP client over Unix socket - Design quality: Intentionally broad — it is a façade, not a focused role interface. With ~41 methods it violates ISP in strict terms, but that breadth is the point: a single injection point lets the TUI and CLI call anything without knowing whether they are local or remote. The grouping in the source by comment sections (
// Sessions,// Agent, etc.) partially mitigates readability concerns.
Coordinator#
- Package:
github.com/charmbracelet/crush/internal/agent - File:
internal/agent/coordinator.go - Methods:
Run(ctx, sessionID, prompt, attachments...),Cancel(sessionID),CancelAll(),IsSessionBusy(sessionID),IsBusy(),QueuedPrompts(sessionID),QueuedPromptsList(sessionID),ClearQueue(sessionID),Summarize(ctx, sessionID),Model(),UpdateModels(ctx) - Purpose: Manages the agent lifecycle: running LLM turns, cancellation, queue management, model refresh, and summarization. The
Appowns oneCoordinatorand delegates all agent operations to it. - Implementations:
coordinator(private struct,internal/agent) - Design quality: Well-scoped. 11 methods covering exactly what the
AppandWorkspaceneed to drive the agent. The commented-outSetMainAgenthints at future multi-agent plans without polluting the current surface.
SessionAgent#
- Package:
github.com/charmbracelet/crush/internal/agent - File:
internal/agent/agent.go - Methods:
Run(ctx, SessionAgentCall),SetModels(large, small),SetTools(tools),SetSystemPrompt(systemPrompt),Cancel(sessionID),CancelAll(),IsSessionBusy(sessionID),IsBusy(),QueuedPrompts(sessionID),QueuedPromptsList(sessionID),ClearQueue(sessionID),Summarize(ctx, sessionID, opts),Model() - Purpose: The per-session agent runner. Drives the fantasy LLM loop: loading history, calling the provider, executing tools, and persisting results.
Coordinatorholds oneSessionAgent(with a map for multi-agent expansion) and delegatesRun()calls to it. - Implementations:
sessionAgent(private struct); sub-agents created viacoordinator.runSubAgent()also satisfy this interface - Design quality: Reasonable scope. The overlap with
Coordinator(cancel, busy, queue methods) exists becauseCoordinatorwrapsSessionAgentand proxies these calls upward. Slightly redundant but avoids leaking theSessionAgentout of theagentpackage.
Publisher / Subscriber (generic)#
- Package:
github.com/charmbracelet/crush/internal/pubsub - File:
internal/pubsub/events.go - Methods:
Publisher[T]:Publish(EventType, T)Subscriber[T]:Subscribe(context.Context) <-chan Event[T]
- Purpose: Decoupled event bus.
Publisher[T]is satisfied by*Broker[T]and used by domain services to emit events.Subscriber[T]is embedded into every domainServiceinterface so consumers can subscribe to their data changes. - Implementations:
*pubsub.Broker[T]implements both. All domain services embed*Broker[T]to satisfySubscriber[T]for free. - Design quality: Exemplary ISP. Two single-method interfaces, generic over the payload type. The embedding pattern (domain
Serviceembedspubsub.Subscriber[DomainType]) is clean composition. The non-blocking publish (drop slow consumers) is an explicit liveness-over-correctness trade-off appropriate for a real-time UI.
session.Service#
- Package:
github.com/charmbracelet/crush/internal/session - File:
internal/session/session.go - Methods:
pubsub.Subscriber[Session](embedded);Create,CreateTitleSession,CreateTaskSession,Get,GetLast,List,Save,UpdateTitleAndUsage,Rename,Delete; agent tool session helpers:CreateAgentToolSessionID,ParseAgentToolSessionID,IsAgentToolSession - Purpose: CRUD plus event subscription for
Sessionrecords backed by SQLite. The agent tool session helpers encode/decode a structured session ID that encodes the parent message and tool call, enabling sub-agent session trees. - Implementations:
service(private struct in the same package) - Design quality: Slightly mixed responsibilities (CRUD + agent ID encoding), but the encoding logic is tightly coupled to session identity so colocation is defensible.
message.Service#
- Package:
github.com/charmbracelet/crush/internal/message - File:
internal/message/message.go - Methods:
pubsub.Subscriber[Message](embedded);Create,Update,Get,List,ListUserMessages,ListAllUserMessages,Delete,DeleteSessionMessages - Purpose: CRUD plus event subscription for
Messagerecords. The TUI receivesMessageupdate events via this subscriber to stream partial LLM output in real time. - Implementations:
service(private struct); embeds*pubsub.Broker[Message]directly so the struct satisfiesSubscriber[Message]via embedding - Design quality: Clean, focused. 8 methods, no scope creep. The
Subscriberembedding is idiomatic.
permission.Service#
- Package:
github.com/charmbracelet/crush/internal/permission - File:
internal/permission/permission.go - Methods:
pubsub.Subscriber[PermissionRequest](embedded);GrantPersistent,Grant,Deny,Request(ctx, opts),AutoApproveSession,SetSkipRequests,SkipRequests,SubscribeNotifications - Purpose: Intercepts tool calls that require user approval, manages grant/deny state (session-scoped and persistent), and broadcasts
PermissionRequestevents for the TUI to display. The hook context key (WithHookApproval) allows pre-approved tool calls from hooks to bypass the interactive prompt. - Implementations:
permissionService(private struct) - Design quality: Good scope. The dual-subscriber pattern (one for requests, one for notifications via
SubscribeNotifications) is unusual but motivated by the bidirectional nature of permissions (request arrives from agent, answer must return to agent).
history.Service#
- Package:
github.com/charmbracelet/crush/internal/history - File:
internal/history/file.go - Methods:
pubsub.Subscriber[File](embedded);Create,CreateVersion,Get,GetByPathAndSession,ListBySession,ListLatestSessionFiles,Delete,DeleteSessionFiles - Purpose: Versioned file snapshot store. When the agent writes a file,
history.Servicecaptures the before-state so the TUI can show diffs. - Implementations:
service(private struct) - Design quality: Clean. The
CreateVersionvsCreatedistinction cleanly separates initial snapshots from subsequent versions.
db.Querier#
- Package:
github.com/charmbracelet/crush/internal/db - File:
internal/db/querier.go - Methods: 38 CRUD/analytics methods (sqlc-generated): file, message, session operations plus analytics (
GetHourDayHeatmap,GetUsageByModel,GetToolUsage, etc.) - Purpose: The database access boundary. Generated by sqlc from SQL queries; domain services receive a
Querierand never import a concrete*sql.DBor a specific driver. - Implementations:
*Queries(sqlc-generated struct wrappingDBTX); also satisfiesDBTXsub-interface used in transactions - Design quality: Machine-generated, so mechanically correct. The
Querierinterface enables test doubles (mock queriers) without a real SQLite. The analytics methods mixed in with CRUD are a code-generation artefact — they don’t affect runtime design.
MessageItem (UI hierarchy)#
- Package:
github.com/charmbracelet/crush/internal/ui/chat - File:
internal/ui/chat/messages.go - Methods (full hierarchy):
Identifiable:ID() stringAnimatable:StartAnimation() tea.Cmd,Animate(anim.StepMsg) tea.CmdExpandable:ToggleExpanded() boolKeyEventHandler:HandleKeyEvent(tea.KeyMsg) (bool, tea.Cmd)MessageItem: embedslist.Item,list.RawRenderable,IdentifiableHighlightableMessageItem: embedsMessageItem,list.HighlightableFocusableMessageItem: embedsMessageItem,list.Focusable
- Purpose: Compose the set of capabilities a chat list item can have. The
chat.Modelstores a heterogeneous[]MessageItemslice and type-asserts to the optional interfaces (Animatable,Expandable, etc.) at render/key-handling time. - Implementations: Multiple concrete message item structs (
assistantMessage,userMessage,toolMessage, etc.) - Design quality: Well-designed capability decomposition. Each optional interface is a single responsibility; composition via embedding keeps concrete types clean. The type-assertion dispatch is idiomatic Go for optional behavior without reflection.
Interface patterns#
Size distribution: Bimodal. The infrastructure interfaces (
Publisher,Subscriber,Identifiable) have 1–2 methods each — strictly ISP-compliant. The façade interfaces (Workspace,db.Querier,SessionAgent) are intentionally large (11–41 methods) to serve as complete seams. Domain services are mid-range (8–13 methods). Average across all non-trivial interfaces is roughly 8–10 methods.Embedding: Heavy and idiomatic. Every domain
Serviceinterface embedspubsub.Subscriber[DomainType], giving all services a uniform subscription API. The UIMessageItemhierarchy composes 3 base interfaces.HighlightableMessageItemandFocusableMessageItemadd one more each.DBTXis embedded inQuerier. This is the project’s primary reuse mechanism.Implicit satisfaction: Mixed, appropriate to layer. Interfaces in
workspace,agent, andpubsubare defined in separate packages from their implementations — classic consumer-side interface. DomainServiceinterfaces are defined alongside their implementations in the same package — provider-side, which is fine since there is only ever one implementation and the interface exists mainly for testability and injection.Stdlib interfaces used: Not prominently.
context.Contextis ubiquitous as a parameter but noio.Reader/io.Writerinterfaces are embedded in the core abstractions.database/sql’sDBTXis wrapped bydb.Querier. The BubbleTeatea.Modelinterface (Init,Update,View) is satisfied by all UI components throughui/common.Model[T], but this is from an external charm library rather than stdlib.
Key abstractions#
Workspace— The most architecturally significant interface. It is the boundary between all frontends and all backends. With ~41 methods it is a façade rather than a role interface, but the trade-off is intentional: it makes the optional client/server split invisible to TUI and CLI code. Two implementations cover both modes without any branching in the callers.pubsub.Publisher[T]/pubsub.Subscriber[T]— The generic event bus primitives. Their 1-method design, combined with structural embedding in every domainService, creates a uniform reactive layer across sessions, messages, permissions, history, and files. The non-blocking publish semantics are encoded here and propagate to all domain services automatically.agent.Coordinator— The boundary between the application container (App) and the LLM engine. It hides all session-queue management, model selection, and tool registry details behind an 11-method interface. TheAppneeds onlyRun(),Cancel(), andUpdateModels()for normal operation; the rest support the TUI status displays.agent.SessionAgent— The inner abstraction that theCoordinatoruses for the actual LLM loop. Its separation fromCoordinatormakes sub-agent spawning first-class:runSubAgent()creates a childSessionAgentthat satisfies the same interface as the parent, enabling recursive delegation with no special code paths.db.Querier— The persistence boundary. By acceptingQuerierinstead of*sql.DB, all domain services are decoupled from the SQLite driver and fully testable with a mock. The sqlc-generated implementation is the only production implementation, but the interface unlocks test doubles without ORM complexity.
Interface-driven extensibility#
LLM provider swapping: Done at the
fantasy.Provider/fantasy.LanguageModellevel (external library). Crush’s ownagent.Coordinatorbuilds a provider from config and stores it as afantasy.LanguageModel— adding a new provider is acaseinbuildProvider()with no interface changes.Client/server split: Entirely powered by the
Workspaceinterface.AppWorkspace(in-process) andClientWorkspace(HTTP) both satisfyWorkspace. The CLI selects one at startup; all downstream code is unaware of the choice.Tool extensibility: The agent’s tool registry accepts
[]fantasy.AgentTool(external interface). MCP tools and LSP tools are dynamically registered at startup and converted tofantasy.AgentToolvalues. Adding a new tool type requires no changes toSessionAgentorCoordinator.UI component extensibility: The
MessageItemhierarchy allows new message display types to be added by implementing the base interface and optionally satisfyingAnimatable,Expandable,HighlightableMessageItem, orFocusableMessageItem. The chat list handles any mix via type assertions.Notification backends:
internal/ui/notificationdefines aBackendinterface for OS-level notification dispatch, allowing platform-specific implementations to be swapped without touching the notification logic.