Skip to content

Conversation

@hmans
Copy link
Owner

@hmans hmans commented Dec 20, 2025

Summary

This PR adds a beans serve command that starts a web server providing a full-featured web UI for browsing and managing beans.

  • Web UI - A SvelteKit SPA for viewing beans with hierarchical display, markdown rendering, and syntax-highlighted code blocks
  • GraphQL API - HTTP endpoint at /api/graphql exposing queries, mutations, and subscriptions
  • Live Updates - Real-time sync via GraphQL subscriptions over WebSockets

Key Features

Backend (beans serve)

  • HTTP server using Gin framework
  • GraphQL subscriptions for real-time bean change notifications
  • Static assets embedded into Go binary via //go:embed
  • Configurable port: CLI --port flag, config file server.port, or default (8080)
  • Graceful shutdown with proper signal handling
  • Request logging middleware

Frontend (SvelteKit)

  • Two-pane layout: hierarchical bean list on the left, bean detail on the right
  • Nested display respecting parent/child relationships
  • Markdown rendering for bean bodies with styled headings and task lists
  • Shiki syntax highlighting for code blocks (bundled essential languages only)
  • Copy bean ID to clipboard button
  • Real-time updates via GraphQL subscription
  • Svelte 5 runes-style reactive store

GraphQL Enhancements

  • New Subscription type with beanChanged field
  • includeInitial parameter to receive existing beans on subscription start
  • INITIAL_SYNC_COMPLETE event for explicit sync signaling (no magic timeouts)
  • BeanChangeEvent type with INITIAL, INITIAL_SYNC_COMPLETE, CREATED, UPDATED, DELETED change types

Configuration

  • New server.port option in .beans.yml (default: 8080)
  • beans init now includes server config in generated file
  • CLI --port flag overrides config value

Architecture

beans serve
    ├── /api/graphql  → GraphQL endpoint (queries, mutations, subscriptions)
    ├── /playground   → GraphQL Playground
    └── /*            → SvelteKit SPA (embedded static assets)

Usage

# Start server on default port (8080)
beans serve

# Start on custom port
beans serve --port 3000

# Or configure in .beans.yml:
# server:
#   port: 3000

Development Workflow

# Start both backend and frontend with hot reload
mise dev

# Or manually:
# Terminal 1: Backend
go run . serve

# Terminal 2: Frontend dev server (proxies /api/graphql to backend)
cd frontend && pnpm dev

Test Plan

  • Run beans serve and verify the web UI loads at http://localhost:8080
  • Verify beans are displayed in hierarchical order
  • Click a bean and verify the detail view shows markdown-rendered body
  • Create/update/delete a bean via CLI and verify real-time updates in the UI
  • Verify beans server alias works
  • Verify --port flag overrides default
  • Verify server.port in config is respected
  • All tests pass

hmans and others added 30 commits December 18, 2025 17:57
- Add cmd/serve.go with HTTP server serving GraphQL API
- GraphQL endpoint at POST /graphql
- GraphQL Playground at GET /graphql for interactive queries
- Supports --port/-p flag (default 8080)
- Graceful shutdown on SIGINT/SIGTERM
- Plan web UI feature with subtasks for future implementation
- Add internal/web/ package with go:embed for frontend assets
- Serve embedded SPA from beans serve at /
- Handle SPA routing (unknown paths serve index.html)
- Add mise tasks: build:frontend, build:embed
- Build pipeline now includes frontend compilation
- Use signal.NotifyContext for cleaner signal handling
- Properly deregister signal handlers on exit
- Add gin.Logger() middleware to log HTTP requests
* main:
  perf: update beancore state incrementally instead of full reload
  feat: add channel-based file watcher with typed events
  doc: Add rule about prefixing bean titles w/ IDs when talking to humans
  doc: Add rule about not closing beans that contain open todos
- Add Subscription type with beanChanged field to schema
- Add BeanChangeEvent type and ChangeType enum (CREATED/UPDATED/DELETED)
- Implement subscription resolver using beancore.Subscribe()
- Add WebSocket transport to serve command for subscription support
- Start file watcher automatically when server starts

Usage: Subscribe to bean changes via GraphQL:
```graphql
subscription {
  beanChanged {
    type
    beanId
    bean { id title status }
  }
}
```

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- BeansStore class with $state for reactive beans map
- Load all beans via GraphQL query
- Helper methods: byStatus, byType, children, blockedBy
- Uses SvelteMap for proper reactivity
- Singleton export for app-wide usage

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Display all beans in a card layout
- Show id, type, status, title, tags, and date
- Color-coded status and type badges
- Loading and error states

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add subscribe() method to BeansStore
- Handle CREATED/UPDATED/DELETED events from beanChanged subscription
- Show "Live" indicator when connected
- Clean up subscription on component destroy
- Add wonka dependency for urql streams

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Configure gqlgen with explicit transports (WebSocket first for upgrade handling)
- Add graphql-transport-ws subprotocol for graphql-ws client compatibility
- Frontend connects WebSocket directly to backend in dev (bypasses Vite proxy)
- Force-close connections if graceful shutdown times out
- Add CORS middleware for development
Add includeInitial argument to beanChanged subscription that emits all
current beans on connect as INITIAL events, followed by real-time
CREATED/UPDATED/DELETED changes. This eliminates race conditions between
loading and subscribing.

- Add includeInitial argument (default false) to beanChanged subscription
- Add INITIAL to ChangeType enum for initial state events
- Update resolver to emit all beans with INITIAL type when includeInitial=true
- Simplify frontend to use single subscribe() call instead of load()+subscribe()
Display beans in a tree structure matching CLI output:
- Top-level beans shown at root
- Children indented with visual tree lines
- Color-coded left border by type (milestone, epic, feature, bug, task)
- Shows child count on parent beans
- Recursive BeanItem component for unlimited nesting depth
- Two-pane layout: bean list on left, detail view on right
- Draggable separator with localStorage persistence (200-600px range)
- Compact bean list items with short ID, title, status badge
- Detail view shows: metadata, tags, relationships, body, timestamps
- Click bean to select and view details
- Selected bean stays in sync with real-time updates
- Add shiki package for syntax highlighting
- Create markdown.ts utility with lazy highlighter initialization
- Use github-dark theme for code blocks
- Add styling for code blocks and inline code
- Preload highlighter in layout for faster first render
- Remove header for cleaner layout
- Make separator more subtle with gray tones
- Fix HTML comment syntax
hmans added 15 commits December 20, 2025 14:11
- Use shiki/core with explicit language imports instead of full bundle
- Bundle 9 languages: JS, TS, Go, Bash, JSON, YAML, Markdown, GraphQL, Diff
- Other languages fall back to plain styled code blocks
- Skip shiki during SSR (browser-only)

Frontend build: 9.7 MB → 1.8 MB (81% reduction)
Go binary: 60 MB → 53 MB
- Add INITIAL_SYNC_COMPLETE event type for explicit sync signaling
- Replace fragile 500ms timeout in frontend with server-sent signal
- Add type guard to filter in BeanDetail.svelte for type safety
- Remove redundant {#if b} check since filter already excludes undefined
- Add tests for subscription resolver (INITIAL_SYNC_COMPLETE)
- Add tests for web embed package (Handler, DistFS)
- Change default port from 22880 to 8080
- Add `server.port` config option in `.beans.yml`
- CLI `--port` flag overrides config value
- Priority: CLI flag > config file > default (8080)
The `beans init` command now writes `server.port: 8080` to the
generated `.beans.yml` file.
beans-4zsu: Add comments to generated .beans.yml config file
* main:
  chore: beans
  feat: add bean for agent-specific beans prime output
  chore: cleanup OpenCode plugin
  docs: add OpenCode plugin and integration instructions
  fix: Tweak `beans prime` prompt
  chore: Claude
* main:
  fix(cli): allow `beans version` to run without .beans directory (#28)
  fix: In the prompt primer, request the agent to check for existing beans before creating a new one
  feat(tui): add 'y' shortcut to copy bean-id (#26)
  docs: clarify completion rules for beans with unchecked items (#27)
  feat: support short IDs (without prefix) in GraphQL queries
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants