Files
yellowjacket/AGENTS.md
T

9.3 KiB

AGENTS.md - YellowJacket

Guidelines for AI coding agents working in this repository.

Project Overview

YellowJacket is a cross-platform desktop music player built with:

  • Backend: Go 1.25 with Wails v2 framework
  • Frontend: TypeScript with Lit Web Components
  • Database: SQLite (pure-Go driver via modernc.org/sqlite)
  • Build Tools: Make, Wails CLI, Vite, pnpm

Build Commands

make dev          # Development with hot-reload
make build-dev    # Debug build
make build-prod   # Production build (obfuscated + UPX compressed)
make generate     # Run all code generators (sqlc, templ)
make clean        # Clean frontend build artifacts
make lint         # Run golangci-lint
make test         # Run all Go tests (race detector, no cache, 2min timeout)

Frontend Only

cd frontend && pnpm install   # Install dependencies
cd frontend && pnpm dev       # Vite dev server
cd frontend && pnpm build     # Production build

Testing

Important: Tests require the -tags webkit2_41 build tag.

make test                                                              # All tests (preferred)
go test -tags webkit2_41 ./...                                         # All tests manually
go test -tags webkit2_41 ./backend/player/                             # Single package
go test -tags webkit2_41 -run TestFunctionName ./backend/player/       # Single test
go test -tags webkit2_41 -v -run TestFunctionName ./backend/player/    # Verbose single test

Test files are colocated with source as *_test.go. Test fixtures live in test_data/. Some tests skip in CI when they require hardware (audio device, Wails runtime).

Linting

golangci-lint v2 config (.golangci.yml) with strict rules. Key linters:

  • gocritic, errorlint, err113, godot, revive, sloglint, nlreturn, wsl
  • Formatters: gci, gofmt, gofumpt, goimports, golines
make lint                                                # Lint all Go code
golangci-lint run --build-tags webkit2_41 ./...           # With build tags explicitly

Frontend type checking: cd frontend && pnpm exec tsc --noEmit

Avoiding Common Linting Errors

Always run make lint before considering a task complete. Below are the most common linting violations and how to avoid them.

Line length (golines): Keep lines under 100 characters. Break long function calls, especially slog calls, across multiple lines:

// Bad — over 100 characters:
q.logger.Warn("Current index out of range", "index", q.currentIndex, "trackCount", len(q.tracks))

// Good — broken across lines:
q.logger.Warn(
    "Current index out of range",
    "index", q.currentIndex, "trackCount", len(q.tracks),
)

Stuttering type names (revive): Exported types must not repeat the package name. Consumers would write queue.Track, not queue.QueueTrack:

// Bad — stutters as queue.QueueTrack:
type QueueTrack struct { ... }

// Good:
type Track struct { ... }

Cuddled declarations (wsl): var and const declarations must be separated from the preceding statement by a blank line:

// Bad:
wasEmpty := len(q.tracks) == 0
var newTracks []Track

// Good:
wasEmpty := len(q.tracks) == 0

var newTracks []Track

Blank line after early returns (nlreturn): An if block that ends with return, continue, or break must be followed by a blank line:

if err != nil {
    return err
}

doNextThing()

Error sentinels (err113): Never use errors.New(...) or fmt.Errorf("...") inline in return statements. Define package-level sentinel errors instead:

var errNotFound = errors.New("not found")

Doc comments (godot): All doc comments on exported types and functions must end with a period:

// Track represents a track in the queue with its metadata.
type Track struct { ... }

Import order (gci): Three groups separated by blank lines — stdlib, third-party, internal (yellowjacket/...). Let the formatter handle this, but be aware of the expected grouping.

Code Generation

go:generate directives live in backend/app.go (templ) and backend/database/database.go (sqlc). After modifying .templ files or SQL in backend/database/sql/, run make generate. Never edit files in backend/database/sql/sqlcgen/ or *_templ.go — they are generated.

Go Code Style

Package Documentation

Every package must have a doc comment ending with a period:

// Package player provides audio playback functionality.
package player

Import Organization

Three groups separated by blank lines (enforced by gci): stdlib, third-party, internal.

import (
    "context"
    "fmt"

    "github.com/wailsapp/wails/v2/pkg/runtime"

    "yellowjacket/backend/events"
)

Error Handling

  • Wrap errors with context: fmt.Errorf("failed to open file: %w", err)
  • Sentinel errors as package-level vars (enforced by err113):
    var ErrUnsupportedFileType = errors.New("unsupported file type")
    
  • Unexported sentinels for internal use: var errNotDirectory = errors.New("not a directory")
  • Use errors.Join() for accumulating multiple errors
  • Return early on errors; blank line required after early returns (nlreturn)

Naming Conventions

  • Structs/exported: PascalCase — Unexported: camelCase
  • Constants: PascalCase for exported, grouped with const (...)
  • Custom domain types: type PlayerState string, type UserVolume int, type AudioFileExtension string

Logging

log/slog with structured key-value pairs. Logger injected via constructors, scoped with logger.WithGroup("player"):

p.logger.Info("File loaded", "file", filePath)
p.logger.Error("Failed to decode", "path", filePath, "err", err)

Comments & Formatting

  • Doc comments on all exported functions/types, ending with periods (enforced by godot)
  • Blank line after early returns (enforced by nlreturn)

Constructor Pattern

func NewPlayer(ctx context.Context, logger *slog.Logger, db *database.DB) (*Player, error) {
    player := &Player{ctx: ctx, logger: logger.WithGroup("player"), state: Stopped}
    return player, nil
}

SetContext Pattern (Two-Phase Initialization)

Components needing Wails runtime use two phases (runtime unavailable until OnStartup):

  1. New*() constructor — created before Wails runtime is available
  2. SetContext(ctx context.Context) — called after runtime starts; registers event handlers, restores state

Build Tags

Dev/prod detection via internal/dev/: //go:build devIsDev = true, //go:build !devIsDev = false.

TypeScript/Lit Code Style

Import Organization

Use path aliases from tsconfig.json. Use import type for type-only imports (verbatimModuleSyntax).

import { EventsOn, EventsEmit } from '@runtime/runtime';
import type { TrackInfo } from '@store/player-store';

Aliases: @go/*, @components/*, @store/*, @runtime/*, @utils/*, @assets/*, @pages/*

Lit Component Pattern

@customElement('component-name')
export class ComponentName extends LitElement {
  @state() private someState: Type = initialValue;
  static override styles = css`...`;
  override connectedCallback() { super.connectedCallback(); }
  override disconnectedCallback() { super.disconnectedCallback(); }
  override render() { return html`...`; }
}
  • override keyword required (noImplicitOverride: true)
  • Private event handlers as arrow functions: private handleClick = () => { ... }
  • strict: true, noUncheckedIndexedAccess: true, verbatimModuleSyntax: true, experimentalDecorators: true, noUnusedLocals: true, noUnusedParameters: true
  • Singleton stores in frontend/src/store/ (backend is source of truth). ReactiveController pattern connects Lit components to stores — subscribe in hostConnected(), unsubscribe in hostDisconnected().

Frontend-Backend Communication

Event System

Events are the primary communication mechanism. Event names must match exactly in both files:

  • Go: backend/events/events.go — TypeScript: frontend/src/events.ts
runtime.EventsEmit(p.ctx, events.TrackChanged, trackInfo)
runtime.EventsOn(p.ctx, events.RequestPlay, func(_ ...any) { p.Play() })
EventsEmit(Events.RequestPlay);
EventsOn(Events.TrackChanged, (trackInfo: TrackInfo) => { ... });

HTMX

The config page uses HTMX for HTML fragment loading. Backend serves fragments via templ templates (backend/config/config-form.templ, backend/library/config.templ). Config has a separate entry point (src/pages/config/).

Database

SQLite with sqlc for type-safe queries. Schemas in backend/database/sql/schemas/, queries in backend/database/sql/queries/, generated code in backend/database/sql/sqlcgen/. SQLite opened with WAL mode and SetMaxOpenConns(1) (single-writer). After modifying SQL files, run make generate.

Directory Structure

  • backend/ — Go: config/, database/, events/, library/, metadata/, models/, player/, queue/, system/, logging/, frontendutil/, assets/
  • frontend/src/ — TypeScript/Lit: components/, pages/, store/, utils/
  • frontend/wailsjs/ — Auto-generated Wails bindings (do not edit)
  • internal/dev/ — Build-tag-based dev/prod detection
  • pkg/templcomp/ — Shared templ component utilities
  • test_data/ — Audio test fixtures