Crush — Patterns#
Concurrency patterns#
Anonymous goroutines (go func)#
- Usage: 32 occurrences throughout the codebase
- Example:
internal/app/app.go— background goroutines for MCP initialization, update check, and event fan-out;internal/cmd/root.go— event bridge goroutine callingws.Subscribe(program) - Assessment: Idiomatic and controlled. Each goroutine is supervised by either context cancellation or a WaitGroup; naked fire-and-forget goroutines are rare.
errgroup for parallel async initialization#
- Usage:
internal/agent/coordinator.go:99—readyWg errgroup.Groupused to parallelizebuildSystemPrompt()andbuildTools()during coordinator setup - Example:
coordinator.go:422—c.readyWg.Go(func() error { ... })x2; first agent call blocks onc.readyWg.Wait()at line 154 - Assessment: Excellent pattern for deferred mandatory initialization — the agent appears ready to the caller (coordinator is returned immediately) but blocks exactly at the first meaningful use. Two concurrent init tasks reduces startup latency.
Channel-per-request (request-reply over pubsub)#
- Usage:
internal/permission/permission.go:234— permission requests use a dedicated buffered channel for each pending request - Example: A permission request publishes via
s.Publish(pubsub.CreatedEvent, permission), then waits onrespCh := make(chan bool, 1)stored ins.pendingRequests. The TUI reads the event and callspermission.Respond(toolCallID, granted)which sends to the channel. - Assessment: Clean pattern for turning an async event (user decides) into a synchronous call from the agent loop. The
select { case <-ctx.Done(): ... case granted := <-respCh: }ensures the agent can be cancelled while waiting for permission.
Non-blocking fan-out pub-sub (drop-on-full)#
- Usage:
internal/pubsub/broker.go—Broker[T]publishes to all subscribers with a non-blockingselect - Example:
for sub := range b.subs { select { case sub <- event: default: // Channel is full, subscriber is slow - skip this event } } - Assessment: A deliberate trade-off of correctness for liveness. Explicitly documented in the architecture: the TUI is a “slow consumer” in real-time scenarios. The buffer size of 64 per subscriber makes drops rare in practice. This is the right call for a real-time UI.
sync.WaitGroup for parallel LSP operations#
- Usage:
internal/lsp/manager.go— 4 distinct WaitGroup patterns for parallel LSP client start, stop, document open, and document update - Example:
lsp/manager.go:87— iterates over all active LSP clients, launches each in a goroutine, then callswg.Wait()for collective completion - Assessment: Textbook usage. LSP clients for different languages can be initialized concurrently; the WaitGroup gives a clean synchronization point before returning to the caller.
Context cancellation propagation#
- Usage: 584 occurrences of
context.Contextacross the codebase — pervasive - Example:
internal/lsp/client.go:143—closeCtx, cancel := context.WithTimeout(ctx, closeTimeout)ensures LSP shutdown doesn’t hang;internal/client/client.go:131— 30-second timeout on workspace creation - Assessment: Exemplary context discipline. Every blocking operation (network, subprocess, LSP init, OAuth polling) uses either a deadline or inherits the parent cancellation. The root context from
main.goflows through the entire application, enabling clean shutdown viafang.Execute.
Graceful shutdown via injected ShutdownFunc#
- Usage:
internal/backend/backend.go—ShutdownFunccallback type passed toBackend.New(), called when the server needs to stop - Example:
backend.go:35—type ShutdownFunc func()is injected at construction; the HTTP server calls it when a/shutdownrequest arrives, which closes the listener in the server loop - Assessment: Clean inversion of control: the
Backenddoesn’t import theserverpackage; it receives a callback. This keeps the dependency graph acyclic.
Polling with time.Ticker (not time.Sleep)#
- Usage: OAuth device flow (
internal/oauth/hyper/device.go:92,internal/oauth/copilot/oauth.go:72), LSP progress polling (internal/lsp/client.go:306), bash tool output streaming (internal/agent/tools/bash.go:306) - Example:
bash.go— a ticker fires every 100ms to stream partial output from a running shell command back to the UI - Assessment: Idiomatic Go. Using a ticker inside a
selectwithctx.Done()is the correct pattern for periodic operations that must be cancellable.
Error handling#
- Style: Mixed —
fmt.Errorfwith%wfor wrapping (dominant), sentinelerrors.Newvariables for caller-checkable conditions, one structured error type in the HTTP layer - Error types defined:
internal/proto/proto.go—Error structwithCode intandMessage stringfor JSON-over-HTTP error responsesinternal/backend/backend.go— 6 exported sentinel errors (ErrWorkspaceNotFound,ErrLSPClientNotFound,ErrAgentNotInitialized,ErrPathRequired,ErrInvalidPermissionAction,ErrUnknownCommand)internal/oauth/copilot/oauth.go:25—ErrNotAvailablesentinel for “copilot not configured”internal/config/scope.go:29—ErrNoWorkspaceConfigfor missing workspace config
- Wrapping approach:
fmt.Errorf("context message: %w", err)throughout. Nopkg/errors; no custom.Wrap().errors.Is()used for sentinel checks (os.ErrNotExist,os.ErrPermission);errors.As()not seen prominently. - Examples:
internal/fsext/lookup.go:38—fmt.Errorf("error probing file %s: %w", fpath, err)adds file path contextinternal/skills/skills.go:136—fmt.Errorf("parsing frontmatter: %w", err)adds operation contextinternal/fsext/lookup_test.go:400—errors.Is(err, os.ErrNotExist)for stdlib sentinel checking
Configuration pattern#
- Approach: Config struct (no functional options for the config object itself); functional options used selectively for sub-components like
Promptand dialog widgets - Config struct:
internal/config/store.go—ConfigStoreholds a*Config(pure data) plus runtimeOverrides. Multi-source loading: built-in defaults → global JSON → project JSON → env var template substitution → flag overrides - Functional options (limited scope):
internal/agent/prompt/prompt.go:48—type Option func(*Prompt)withWithTimeFunc,WithPlatform,WithWorkingDirinternal/ui/dialog/permissions.go:169—type PermissionsOption func(*Permissions)withWithDiffMode
- Example of config struct construction:
// config loaded once at startup: store, err := config.Init(cwd, dataDir, debug) // components receive *config.ConfigStore and call store.Config() - Assessment: The functional options pattern is used where it’s appropriate (components with optional parameters) and absent where it adds no value (the global config, which must be loaded from files anyway). Pragmatic, not dogmatic.
Dependency injection#
- Approach: Manual constructor wiring — no DI framework (no
wire,dig, orfx) - Evidence:
internal/app/app.go:79—func New(ctx context.Context, conn *sql.DB, store *config.ConfigStore) (*App, error)creates all domain services internallyinternal/agent/coordinator.go—NewCoordinator(ctx, sessions, messages, history, permissions, filetracker, lsp, hooks, skills, config)— all service interfaces injected via constructorinternal/ui/model/ui.go:276—func New(com *common.Common, ...) *UI— the TUI receives a*common.Commonwhich embeds theWorkspaceinterface
- Assessment: Manual wiring is feasible because the dependency graph is shallow and acyclic. The
Workspaceinterface acts as the single injection point for the TUI, keeping it decoupled from all backend services. The coordinator receives service interfaces rather than concrete types, enabling test substitution.
Other notable patterns#
Generic concurrent data structures (internal/csync)#
A dedicated package of generic, thread-safe collections:
Map[K comparable, V any]— RWMutex-protected map withGet,Set,Del,Copy,Seq2VersionedMap[K, V]— wrapsMapwith anatomic.Uint64version counter for cache invalidationSlice[T]— RWMutex-protected slice withAppend,Get,Copy,Seq2,SetSliceLazySlice[T]— wrapsWaitGroup.Goto populate a slice in the background; firstSeq()call blocks until readyValue[T]— atomic-style wrapper for any value (likelysync/atomic.Valueor mutex-protected)
This package is a library-quality set of generic primitives that the rest of the codebase uses freely without repeating the mutex-boilerplate. The LazySlice pattern is particularly interesting: it’s the “async init, sync first access” pattern applied generically.
Generic setupSubscriber[T] fan-out helper#
- Location:
internal/app/app.go:492 - Pattern: A generic function that takes any domain service subscriber and a
pubsub.Broker[tea.Msg], then bridges them in a goroutine. Used to wire all 6 domain services into the single TUI event bus. - Assessment: Eliminates 6 copies of nearly identical goroutine fan-out boilerplate. A textbook use of generics for DRY concurrency scaffolding.
Table-driven tests#
- Prevalence: Heavy — 338
t.Run/testCases/ table-struct patterns in_test.gofiles - Style: Named struct slice (
[]struct{ name, input, expected }), iterated witht.Run(tc.name, ...). Subtests run in parallel where appropriate. - Example:
internal/fsext/lookup_test.go— comprehensive table of path lookup scenarios
Type switches over message/event types#
- Usage: Throughout the TUI and client layers, type switches over
tea.Msgand generic event payloads - Example:
internal/workspace/client_workspace.go:569—switch e := ev.(type)maps SSE events totea.Msgconcrete types;internal/ui/model/ui.go— theUpdate(msg tea.Msg)method dispatches via type switch over the full event vocabulary - Assessment: Idiomatic BubbleTea pattern. The broad type switch in
Updateis unavoidable in the BubbleTea architecture; the team has organized it well.
Builder-style common.Common injection#
- Location:
internal/ui/common/common.go - Pattern: A
Commonstruct accumulates all shared state (Workspace, config, styles, glamour renderer, keymap) and is passed to every UI sub-model constructor. Sub-models receive what they need via field access, not explicit parameters. - Assessment: A pragmatic alternative to passing 8 parameters to every constructor. Works well at this scale (single TUI); would require discipline to keep from becoming a “God struct” in a larger codebase.
Versioned map for LSP/tool cache invalidation#
- Location:
internal/csync/versionedmap.go—VersionedMap[K, V]withatomic.Uint64version counter - Pattern: Write operations (
Set,Del) atomically increment the version. Readers can cache a version number and checkMap.Version() != cachedVersionto know when to invalidate. - Assessment: A lightweight optimistic cache invalidation pattern without channels or callbacks. Original and well-suited to the LSP diagnostic / tool list refresh use cases.
Generics used purposefully#
Key generic functions beyond the csync package:
pubsub.Broker[T]andpubsub.Subscriber[T]— type-safe event channels withoutanycaststruncate[T any](input []T, limit int)infsext/fileutil.go— generic slice truncationassignIfNil[T any](ptr **T, val T)inconfig/load.go— helper for zero-safe config mergingptrValOr[T any](t *T, el T) Tinconfig/config.go— nil-safe pointer dereferencecache[T any]inconfig/provider.go— generic file-backed cache
Generics are used where they genuinely eliminate duplication or where type safety across event types matters (pubsub). They are not used speculatively.
Go 1.23 range iterators (iter.Seq, iter.Seq2)#
- Usage:
csync.Slice,csync.Map,csync.VersionedMap, andcsync.LazySliceall exposeSeq()and/orSeq2()returningiter.Seq[T]/iter.Seq2[K, V] - Assessment: This project is ahead of most Go codebases in adopting the Go 1.23 iterator protocol. It allows callers to
for v := range collection.Seq()cleanly and composes well withslices.Collect. A forward-looking choice.