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 calling ws.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:99readyWg errgroup.Group used to parallelize buildSystemPrompt() and buildTools() during coordinator setup
  • Example: coordinator.go:422c.readyWg.Go(func() error { ... }) x2; first agent call blocks on c.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 on respCh := make(chan bool, 1) stored in s.pendingRequests. The TUI reads the event and calls permission.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.goBroker[T] publishes to all subscribers with a non-blocking select
  • 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 calls wg.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.Context across the codebase — pervasive
  • Example: internal/lsp/client.go:143closeCtx, 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.go flows through the entire application, enabling clean shutdown via fang.Execute.

Graceful shutdown via injected ShutdownFunc#

  • Usage: internal/backend/backend.goShutdownFunc callback type passed to Backend.New(), called when the server needs to stop
  • Example: backend.go:35type ShutdownFunc func() is injected at construction; the HTTP server calls it when a /shutdown request arrives, which closes the listener in the server loop
  • Assessment: Clean inversion of control: the Backend doesn’t import the server package; 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 select with ctx.Done() is the correct pattern for periodic operations that must be cancellable.

Error handling#

  • Style: Mixed — fmt.Errorf with %w for wrapping (dominant), sentinel errors.New variables for caller-checkable conditions, one structured error type in the HTTP layer
  • Error types defined:
    • internal/proto/proto.goError struct with Code int and Message string for JSON-over-HTTP error responses
    • internal/backend/backend.go — 6 exported sentinel errors (ErrWorkspaceNotFound, ErrLSPClientNotFound, ErrAgentNotInitialized, ErrPathRequired, ErrInvalidPermissionAction, ErrUnknownCommand)
    • internal/oauth/copilot/oauth.go:25ErrNotAvailable sentinel for “copilot not configured”
    • internal/config/scope.go:29ErrNoWorkspaceConfig for missing workspace config
  • Wrapping approach: fmt.Errorf("context message: %w", err) throughout. No pkg/errors; no custom .Wrap(). errors.Is() used for sentinel checks (os.ErrNotExist, os.ErrPermission); errors.As() not seen prominently.
  • Examples:
    • internal/fsext/lookup.go:38fmt.Errorf("error probing file %s: %w", fpath, err) adds file path context
    • internal/skills/skills.go:136fmt.Errorf("parsing frontmatter: %w", err) adds operation context
    • internal/fsext/lookup_test.go:400errors.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 Prompt and dialog widgets
  • Config struct: internal/config/store.goConfigStore holds a *Config (pure data) plus runtime Overrides. 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:48type Option func(*Prompt) with WithTimeFunc, WithPlatform, WithWorkingDir
    • internal/ui/dialog/permissions.go:169type PermissionsOption func(*Permissions) with WithDiffMode
  • 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, or fx)
  • Evidence:
    • internal/app/app.go:79func New(ctx context.Context, conn *sql.DB, store *config.ConfigStore) (*App, error) creates all domain services internally
    • internal/agent/coordinator.goNewCoordinator(ctx, sessions, messages, history, permissions, filetracker, lsp, hooks, skills, config) — all service interfaces injected via constructor
    • internal/ui/model/ui.go:276func New(com *common.Common, ...) *UI — the TUI receives a *common.Common which embeds the Workspace interface
  • Assessment: Manual wiring is feasible because the dependency graph is shallow and acyclic. The Workspace interface 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 with Get, Set, Del, Copy, Seq2
  • VersionedMap[K, V] — wraps Map with an atomic.Uint64 version counter for cache invalidation
  • Slice[T] — RWMutex-protected slice with Append, Get, Copy, Seq2, SetSlice
  • LazySlice[T] — wraps WaitGroup.Go to populate a slice in the background; first Seq() call blocks until ready
  • Value[T] — atomic-style wrapper for any value (likely sync/atomic.Value or 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.go files
  • Style: Named struct slice ([]struct{ name, input, expected }), iterated with t.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.Msg and generic event payloads
  • Example: internal/workspace/client_workspace.go:569switch e := ev.(type) maps SSE events to tea.Msg concrete types; internal/ui/model/ui.go — the Update(msg tea.Msg) method dispatches via type switch over the full event vocabulary
  • Assessment: Idiomatic BubbleTea pattern. The broad type switch in Update is unavoidable in the BubbleTea architecture; the team has organized it well.

Builder-style common.Common injection#

  • Location: internal/ui/common/common.go
  • Pattern: A Common struct 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.goVersionedMap[K, V] with atomic.Uint64 version counter
  • Pattern: Write operations (Set, Del) atomically increment the version. Readers can cache a version number and check Map.Version() != cachedVersion to 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] and pubsub.Subscriber[T] — type-safe event channels without any casts
  • truncate[T any](input []T, limit int) in fsext/fileutil.go — generic slice truncation
  • assignIfNil[T any](ptr **T, val T) in config/load.go — helper for zero-safe config merging
  • ptrValOr[T any](t *T, el T) T in config/config.go — nil-safe pointer dereference
  • cache[T any] in config/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, and csync.LazySlice all expose Seq() and/or Seq2() returning iter.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 with slices.Collect. A forward-looking choice.