Development Guide
Learn how to contribute to Code, set up your development environment, and understand the codebase.
Getting Started
Prerequisites
- Bun >= 1.3.1
- Node.js >= 18
- Git
- Modern code editor (VSCode recommended)
Setup Development Environment
- Fork and Clone:
git clone https://github.com/YOUR_USERNAME/code.git
cd code- Install Dependencies:
bun install- Build Packages:
bun run build- Run Tests:
bun test- Start Development:
# Terminal UI (hot reload)
bun dev:code
# Web UI (hot reload)
bun dev:web
# Server (daemon mode)
PORT=3000 bun --cwd packages/code-server startProject Structure
code/
├── packages/
│ ├── code-core/ # Headless SDK (350+ files)
│ │ ├── ai/ # AI providers, streaming, agents
│ │ ├── database/ # libSQL + Drizzle ORM
│ │ ├── tools/ # Built-in tools
│ │ └── config/ # Configuration system
│ ├── code-server/ # tRPC v11 server
│ │ ├── trpc/ # Routers, procedures
│ │ ├── services/ # Business logic
│ │ └── context.ts # AppContext
│ ├── code-client/ # Pure UI client
│ │ ├── stores/ # Zustand stores
│ │ ├── lib/ # Event bus
│ │ └── hooks/ # React hooks
│ ├── code/ # Terminal UI (Ink)
│ │ ├── screens/ # Chat, Settings, Dashboard
│ │ ├── commands/ # Slash commands
│ │ └── components/ # UI components
│ └── code-web/ # Web UI (Next.js)
│ ├── app/ # Next.js app router
│ └── components/ # React components
├── docs/ # VitePress documentation
├── .changeset/ # Changesets for versioning
└── turbo.json # Turborepo configurationDevelopment Workflow
Making Changes
- Create a feature branch:
git checkout -b feature/your-feature-name- Make your changes:
- Follow existing code style
- Add tests for new features
- Update documentation
- Run tests:
bun test- Type check:
bun type-check- Format code:
bun format- Create changeset:
bunx changesetFollow prompts to describe your changes.
- Commit changes:
git add .
git commit -m "feat: add your feature"Commit Convention:
feat:New featurefix:Bug fixdocs:Documentationstyle:Code style (formatting)refactor:Code refactoringtest:Testschore:Build, dependencies
- Push and create PR:
git push origin feature/your-feature-nameThen create a Pull Request on GitHub.
Testing
Running Tests
# All tests
bun test
# Specific package
bun test --filter "@sylphx/code-client"
# Watch mode
bun test:watch
# Coverage
bun test:coverageWriting Tests
Unit Test Example:
// packages/code-client/src/lib/event-bus.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { EventBus } from './event-bus'
describe('EventBus', () => {
let eventBus: EventBus
beforeEach(() => {
eventBus = new EventBus()
})
it('should emit and receive events', () => {
const handler = vi.fn()
eventBus.on('test', handler)
eventBus.emit('test', { data: 'value' })
expect(handler).toHaveBeenCalledWith({ data: 'value' })
})
it('should unsubscribe from events', () => {
const handler = vi.fn()
const unsubscribe = eventBus.on('test', handler)
unsubscribe()
eventBus.emit('test', { data: 'value' })
expect(handler).not.toHaveBeenCalled()
})
})Integration Test Example:
// packages/code-server/src/trpc/routers/session.test.ts
import { describe, it, expect } from 'vitest'
import { createTestServer } from '../testing'
describe('Session Router', () => {
it('creates and retrieves session', async () => {
const { client } = createTestServer()
const created = await client.session.create.mutate({
provider: 'openrouter',
model: 'claude-3.5-sonnet'
})
expect(created.id).toBeDefined()
const retrieved = await client.session.get.query({
sessionId: created.id
})
expect(retrieved?.id).toBe(created.id)
})
})Test Coverage Goals
- Core packages: > 80% coverage
- Critical paths: 100% coverage
- Event bus: 100% coverage (currently: 33 tests)
Debugging
Debug Logging
Use the debug package for logging:
import createDebug from 'debug'
const debug = createDebug('sylphx:feature:operation')
debug('Starting operation with params:', params)Enable logs:
# All logs
DEBUG=sylphx:* bun dev:code
# Specific namespace
DEBUG=sylphx:stream:* bun dev:code
# Multiple namespaces
DEBUG=sylphx:stream:*,sylphx:tool:* bun dev:codeVSCode Debugging
Create .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug TUI",
"type": "node",
"request": "launch",
"runtimeExecutable": "bun",
"runtimeArgs": ["--cwd", "packages/code", "start"],
"env": {
"DEBUG": "sylphx:*"
},
"sourceMaps": true
}
]
}Code Style
TypeScript
- Use TypeScript strict mode
- Avoid
any, useunknowninstead - Prefer interfaces for objects
- Use discriminated unions for variants
Good:
interface User {
id: string
name: string
}
type Result<T> =
| { success: true; data: T }
| { success: false; error: string }Bad:
type User = { // Prefer interface
id: any // Avoid any
name: string
}Formatting
Code uses Biome for formatting:
# Format all files
bun format
# Check formatting
bun format:checkRules:
- 2 spaces indentation
- Single quotes
- Semicolons
- Trailing commas
Naming Conventions
- Files: kebab-case (
event-bus.ts,session-router.ts) - Components: PascalCase (
ChatScreen.tsx,MessageList.tsx) - Functions: camelCase (
createSession,handleEvent) - Constants: UPPER_SNAKE_CASE (
MAX_RETRIES,DEFAULT_TIMEOUT) - Types/Interfaces: PascalCase (
Session,StreamEvent)
Architecture Guidelines
Pure UI Client Principle
Client should:
- ✅ Handle UI state only
- ✅ Use optimistic updates
- ✅ Listen to events
- ✅ Render UI
Client should NOT:
- ❌ Contain business logic
- ❌ Make decisions
- ❌ Persist data
- ❌ Directly call AI APIs
Event-Driven Communication
Use event bus for store coordination:
// ✅ Good - Event-driven
sessionStore.createSession(...)
eventBus.emit('session:created', { sessionId })
settingsStore.on('session:created', ({ sessionId }) => {
updateCurrentSession(sessionId)
})
// ❌ Bad - Direct coupling
sessionStore.createSession(...)
settingsStore.setCurrentSession(sessionId) // Circular dependencyServer-Side Business Logic
Keep all decisions on server:
// ✅ Good - Server decides
compact: procedure.mutation(async ({ ctx }) => {
const summary = await generateSummary(...)
const newSession = await createSession(...)
streamAIResponse({ sessionId: newSession.id }) // Auto-trigger
return newSession
})
// ❌ Bad - Client decides
const result = await client.session.compact.mutate(...)
client.message.streamResponse.subscribe(...) // Client triggersAdding Features
Adding a New AI Tool
- Create tool definition:
// packages/code-core/src/tools/my-tool.ts
import type { Tool } from '../types'
export const myTool: Tool = {
name: 'my_tool',
description: 'Does something useful',
parameters: {
type: 'object',
properties: {
param: { type: 'string', description: 'A parameter' }
},
required: ['param']
},
execute: async ({ param }) => {
// Implementation
return { result: 'success' }
}
}- Register tool:
// packages/code-core/src/tools/index.ts
export * from './my-tool'- Add tests:
// packages/code-core/src/tools/my-tool.test.ts
import { describe, it, expect } from 'vitest'
import { myTool } from './my-tool'
describe('myTool', () => {
it('executes successfully', async () => {
const result = await myTool.execute({ param: 'value' })
expect(result.result).toBe('success')
})
})Adding a New tRPC Procedure
- Define procedure:
// packages/code-server/src/trpc/routers/my-router.ts
import { router, publicProcedure } from '../trpc'
import { z } from 'zod'
export const myRouter = router({
myProcedure: publicProcedure
.input(z.object({ param: z.string() }))
.query(async ({ ctx, input }) => {
return { result: 'success' }
})
})- Add to root router:
// packages/code-server/src/trpc/router.ts
import { myRouter } from './routers/my-router'
export const appRouter = router({
// ... existing routers
my: myRouter
})- Use in client:
const result = await client.my.myProcedure.query({ param: 'value' })Adding a New UI Component
- Create component:
// packages/code/src/components/MyComponent.tsx
import React from 'react'
import { Box, Text } from 'ink'
interface MyComponentProps {
message: string
}
export const MyComponent: React.FC<MyComponentProps> = ({ message }) => {
return (
<Box>
<Text>{message}</Text>
</Box>
)
}- Add tests:
// packages/code/src/components/MyComponent.test.tsx
import { render } from 'ink-testing-library'
import { MyComponent } from './MyComponent'
it('renders message', () => {
const { lastFrame } = render(<MyComponent message="Hello" />)
expect(lastFrame()).toContain('Hello')
})Performance Optimization
Build Performance
Code uses Bun for fast builds:
# Clean build
bun clean
bun run build
# Development builds (watch mode)
bun --cwd packages/code-core devBuild times:
- code-core: ~75ms
- code-server: ~23ms
- code: ~39ms
Runtime Performance
Measure performance:
const start = performance.now()
// ... operation
const duration = performance.now() - start
debug('Operation took %dms', duration)Profile with Node:
node --prof --cwd packages/code start
node --prof-process isolate-*.log > profile.txtDocumentation
Updating Docs
- Edit VitePress docs:
cd docs
# Edit .md files- Preview locally:
bun --cwd docs dev- Build docs:
bun --cwd docs buildWriting Good Documentation
- Use clear, concise language
- Include code examples
- Add diagrams for complex concepts
- Link to related documentation
- Keep it up-to-date
Release Process
Creating a Release
- Create changesets for all changes:
bunx changeset- Version packages:
bunx changeset versionUpdate CHANGELOG.md (automatic)
Build all packages:
bun run build- Run all tests:
bun test- Publish:
bunx changeset publish- Push tags:
git push --follow-tagsGetting Help
Resources
Ask Questions
- Check existing issues and discussions
- Search documentation
- Ask in GitHub Discussions
- Contact maintainers
Code of Conduct
We are committed to providing a welcoming and inspiring community for all.
Expected Behavior:
- Be respectful and inclusive
- Welcome newcomers
- Accept constructive criticism
- Focus on what's best for the community
Unacceptable Behavior:
- Harassment or discrimination
- Trolling or insulting comments
- Personal or political attacks
- Any conduct inappropriate in a professional setting
License
Code is MIT licensed. See LICENSE.
By contributing, you agree that your contributions will be licensed under the MIT License.
Thank you for contributing to Code! 🎉