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
  • 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 App owns one Coordinator and delegates all agent operations to it.
  • Implementations: coordinator (private struct, internal/agent)
  • Design quality: Well-scoped. 11 methods covering exactly what the App and Workspace need to drive the agent. The commented-out SetMainAgent hints 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. Coordinator holds one SessionAgent (with a map for multi-agent expansion) and delegates Run() calls to it.
  • Implementations: sessionAgent (private struct); sub-agents created via coordinator.runSubAgent() also satisfy this interface
  • Design quality: Reasonable scope. The overlap with Coordinator (cancel, busy, queue methods) exists because Coordinator wraps SessionAgent and proxies these calls upward. Slightly redundant but avoids leaking the SessionAgent out of the agent package.

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 domain Service interface so consumers can subscribe to their data changes.
  • Implementations: *pubsub.Broker[T] implements both. All domain services embed *Broker[T] to satisfy Subscriber[T] for free.
  • Design quality: Exemplary ISP. Two single-method interfaces, generic over the payload type. The embedding pattern (domain Service embeds pubsub.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 Session records 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 Message records. The TUI receives Message update events via this subscriber to stream partial LLM output in real time.
  • Implementations: service (private struct); embeds *pubsub.Broker[Message] directly so the struct satisfies Subscriber[Message] via embedding
  • Design quality: Clean, focused. 8 methods, no scope creep. The Subscriber embedding 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 PermissionRequest events 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.Service captures the before-state so the TUI can show diffs.
  • Implementations: service (private struct)
  • Design quality: Clean. The CreateVersion vs Create distinction 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 Querier and never import a concrete *sql.DB or a specific driver.
  • Implementations: *Queries (sqlc-generated struct wrapping DBTX); also satisfies DBTX sub-interface used in transactions
  • Design quality: Machine-generated, so mechanically correct. The Querier interface 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() string
    • Animatable: StartAnimation() tea.Cmd, Animate(anim.StepMsg) tea.Cmd
    • Expandable: ToggleExpanded() bool
    • KeyEventHandler: HandleKeyEvent(tea.KeyMsg) (bool, tea.Cmd)
    • MessageItem: embeds list.Item, list.RawRenderable, Identifiable
    • HighlightableMessageItem: embeds MessageItem, list.Highlightable
    • FocusableMessageItem: embeds MessageItem, list.Focusable
  • Purpose: Compose the set of capabilities a chat list item can have. The chat.Model stores a heterogeneous []MessageItem slice 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 Service interface embeds pubsub.Subscriber[DomainType], giving all services a uniform subscription API. The UI MessageItem hierarchy composes 3 base interfaces. HighlightableMessageItem and FocusableMessageItem add one more each. DBTX is embedded in Querier. This is the project’s primary reuse mechanism.

  • Implicit satisfaction: Mixed, appropriate to layer. Interfaces in workspace, agent, and pubsub are defined in separate packages from their implementations — classic consumer-side interface. Domain Service interfaces 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.Context is ubiquitous as a parameter but no io.Reader/io.Writer interfaces are embedded in the core abstractions. database/sql’s DBTX is wrapped by db.Querier. The BubbleTea tea.Model interface (Init, Update, View) is satisfied by all UI components through ui/common.Model[T], but this is from an external charm library rather than stdlib.


Key abstractions#

  1. 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.

  2. pubsub.Publisher[T] / pubsub.Subscriber[T] — The generic event bus primitives. Their 1-method design, combined with structural embedding in every domain Service, 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.

  3. 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. The App needs only Run(), Cancel(), and UpdateModels() for normal operation; the rest support the TUI status displays.

  4. agent.SessionAgent — The inner abstraction that the Coordinator uses for the actual LLM loop. Its separation from Coordinator makes sub-agent spawning first-class: runSubAgent() creates a child SessionAgent that satisfies the same interface as the parent, enabling recursive delegation with no special code paths.

  5. db.Querier — The persistence boundary. By accepting Querier instead 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.LanguageModel level (external library). Crush’s own agent.Coordinator builds a provider from config and stores it as a fantasy.LanguageModel — adding a new provider is a case in buildProvider() with no interface changes.

  • Client/server split: Entirely powered by the Workspace interface. AppWorkspace (in-process) and ClientWorkspace (HTTP) both satisfy Workspace. 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 to fantasy.AgentTool values. Adding a new tool type requires no changes to SessionAgent or Coordinator.

  • UI component extensibility: The MessageItem hierarchy allows new message display types to be added by implementing the base interface and optionally satisfying Animatable, Expandable, HighlightableMessageItem, or FocusableMessageItem. The chat list handles any mix via type assertions.

  • Notification backends: internal/ui/notification defines a Backend interface for OS-level notification dispatch, allowing platform-specific implementations to be swapped without touching the notification logic.