We've all done it. You hit a tricky bug, open a chat with your AI of choice, and start pasting. First the component. Then the hook it uses. Then the API route. Then the types. Then the utility function that the API route calls. Fifteen minutes later you have a 4000-token mess and the AI is confidently hallucinating a solution that doesn't apply to your actual code. The context window isn't magic — it's real estate, and you need to manage it like one.
After building peal.dev and shipping a bunch of side projects with heavy AI assistance, we've developed some hard opinions about context management. Most devs treat the context window like a junk drawer. We're going to show you how to treat it like a curated brief.
Why the AI Starts Lying When Context Gets Messy
Here's what's actually happening under the hood: transformer models process all tokens in the context window simultaneously, but their attention degrades with distance and noise. When you dump 6000 tokens of loosely related code, the model spends its 'attention budget' on irrelevant patterns instead of your actual problem. It's not that the AI got dumber — you gave it bad signal-to-noise.
There's also the 'lost in the middle' problem, which is well-documented in research. Information placed in the middle of a long context gets retrieved less reliably than information at the start or end. So if you paste your relevant code in the middle of a wall of boilerplate, the AI is literally less likely to use it correctly.
The context window is not a bucket you fill — it's a conversation you curate. Every token you add either sharpens or dilutes the AI's understanding.
The Minimum Viable Context Principle
Start with less than you think you need. This sounds counterintuitive but it forces you to identify what's actually relevant. Ask yourself: if I had to explain this problem to a colleague in a Slack message, what code would I paste? That's your starting context.
A practical approach: start with only the broken thing plus its immediate types. If the AI can't help with that, add one layer of dependencies. Keep expanding until you get something useful. You'll be surprised how often the minimal context works.
// Instead of pasting this entire auth flow (300+ lines)...
// ...paste only the specific function that's broken:
// BAD: dumping everything
// [AuthProvider component]
// [useAuth hook - full implementation]
// [authUtils.ts - all 15 functions]
// [middleware.ts]
// [types/auth.ts]
// GOOD: minimal viable context
// The problem: refreshToken is being called in a loop
async function refreshToken(token: string): Promise<AuthToken> {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
})
if (!response.ok) {
// This throws, which triggers the error boundary,
// which re-renders, which calls refreshToken again
throw new AuthError('Refresh failed')
}
return response.json()
}
// Types needed for context:
interface AuthToken {
accessToken: string
refreshToken: string
expiresAt: number
}
class AuthError extends Error {
constructor(message: string) {
super(message)
this.name = 'AuthError'
}
}Notice what we didn't paste: the entire auth system, the middleware, the provider, the 15 utility functions. Just the broken function, the error it produces, and the types it touches. That's a 200-token context instead of a 2000-token one, and the AI's answer will be dramatically better.
Structuring Context for Maximum Signal
When you do need to provide more context, order matters. Put the most important information first. Here's a template we actually use:
## What I'm trying to do
[One sentence. Be specific.]
## What's actually happening
[Error message, unexpected behavior, etc.]
## Relevant code
[The specific function/component that's broken]
## Types and interfaces it depends on
[Only the types directly used in the above code]
## What I've already tried
[This prevents the AI from suggesting things you've ruled out]
## Constraints
["I can't change the API shape", "Must work with React 18", etc.]This structure isn't just for aesthetics. The AI sees 'what I'm trying to do' first, which primes every subsequent token interpretation toward your actual goal. It's the difference between asking for directions while holding a map and asking while describing every street you've ever driven.
When You Actually Need Big Context: Do It Right
Sometimes you genuinely need the AI to understand a large codebase. Maybe you're doing a refactor, or you want it to generate code consistent with your existing patterns. Here's how to handle this without descending into noise.
Create a project context file. We keep a CLAUDE.md (or CURSOR_RULES.md, or whatever your tool supports) in the root of every project. It's a curated, dense summary of the codebase that you maintain manually.
# Project Context — peal.dev
## Stack
- Next.js 15 App Router, TypeScript strict mode
- Prisma + PostgreSQL (Supabase)
- Stripe for payments (subscriptions + one-time)
- Resend for email, React Email for templates
- Tailwind + shadcn/ui
- Deployed on Vercel
## Key Conventions
- Server components by default, 'use client' only when needed
- All DB queries go through /lib/db/ — never import Prisma directly in components
- API routes in /app/api/ follow REST conventions
- Errors are handled with a Result type, not try/catch everywhere
- Auth via NextAuth v5 — session accessed with auth() in server, useSession() in client
## Result Type Pattern (used everywhere)
// type Result<T> = { success: true; data: T } | { success: false; error: string }
## File Structure
/app - routes and pages
/components - shared UI components
/lib - business logic, db queries, external services
/lib/db - all Prisma queries, organized by domain
/lib/stripe - Stripe helpers and webhook handlers
/types - shared TypeScript types
## Do NOT
- Use any (use unknown and narrow)
- Fetch in useEffect (use Server Components or SWR)
- Handle Stripe logic in API routes directly (use /lib/stripe/)
- Import Prisma client outside /lib/db/This file is 400 tokens. It tells the AI everything it needs to write consistent code without you pasting 20 files. When you start a new conversation, paste this first, then ask your question. You'll notice the AI immediately writes code that matches your conventions instead of doing its own thing.
The Art of Selective Pasting
Not all code is equal in a context window. Here's our ranking of what to include vs. skip:
- ALWAYS include: TypeScript types and interfaces — they're dense information at low token cost
- ALWAYS include: The exact error message (stack trace if relevant, trimmed to the relevant frames)
- USUALLY include: The function signature + first 10 lines of any function the broken code calls
- SOMETIMES include: Test cases that demonstrate the expected vs. actual behavior
- RARELY include: Full component files — extract only the relevant logic
- NEVER include: package.json, lockfiles, config files unless they're the actual problem
- NEVER include: Commented-out code, console.log debugging statements, or TODO comments
One trick that works really well: use your TypeScript types as a proxy for the full implementation. A 50-line interface tells the AI almost as much as the 200-line class it describes, at one quarter the token cost. We learned this when working on multi-tenant features — instead of pasting the entire tenant resolution middleware, pasting the TenantContext type and the middleware signature was enough for the AI to write correct code.
Managing Long Conversations Without Losing Your Mind
Long conversation threads are a trap. After 10-15 back-and-forth messages, the early context starts to decay in the model's effective attention. The AI starts to forget constraints you mentioned, inconsistencies creep in, and you end up with code that contradicts decisions made three exchanges ago.
Our rule: start a fresh conversation every time you switch to a different problem, even if it's the same codebase. Copy the relevant conclusions from the old thread (not the whole thing — just the decisions made) and paste them as context at the top of the new one.
## Context from previous session
We decided to:
- Use optimistic updates for the like button (no server round-trip for UI)
- Store user preferences in a cookie, not DB (no auth required to work)
- The LikeButton component is in /components/ui/LikeButton.tsx
- API endpoint is POST /api/posts/[id]/like — returns { liked: boolean, count: number }
## New question
Now I need to implement the unlike functionality. The current like implementation is:
[paste only the current like handler]This sounds like extra work but it's actually faster. A focused 5-message conversation beats a rambling 25-message one every time. You also end up with a useful decision log as a side effect.
Tools That Help (and the Limits of Each)
Cursor's @codebase feature, Copilot's workspace mode, Claude's Projects — all of these are trying to solve the context management problem automatically. They work, with caveats. Automatic context retrieval is good at finding obviously related files, but it often misses implicit dependencies and architectural constraints that you'd know to include manually.
We use Cursor heavily, and the @file and @symbol references are genuinely good. But we still write our own CLAUDE.md because the automatic indexing doesn't know our conventions — it knows our code exists, not why it's structured the way it is. The context file bridges that gap.
One workflow we've settled on for new features in our peal.dev templates: write the types first, get AI review on the types alone, then use the approved types as context for generating the implementation. Type-driven development pairs beautifully with AI assistance because types are both low-token and high-information.
// Step 1: Define and validate types with AI first (cheap, ~100 tokens)
interface SubscriptionPlan {
id: string
name: string
priceId: string // Stripe price ID
interval: 'month' | 'year'
features: string[]
limits: {
projects: number
teamMembers: number
storageGb: number
}
}
interface UserSubscription {
planId: string
status: 'active' | 'canceled' | 'past_due' | 'trialing'
currentPeriodEnd: Date
cancelAtPeriodEnd: boolean
}
// Step 2: Use these types as context when asking for implementation
// "Given these types, write the Stripe webhook handler for subscription updates"
// The AI now has all the shape information it needs without the full Stripe library contextThe whole approach maps onto how good engineers think anyway. You define your interfaces before your implementations. You understand the shape of data before you write the code that transforms it. AI just rewards this discipline more explicitly — garbage types in, garbage code out.
If you're using a template from peal.dev, this stuff gets easier because we ship with a CLAUDE.md convention and typed everything aggressively. You're not starting from a blank slate trying to explain your project structure — the context file is part of the template.
The goal isn't to give the AI more context. It's to give it the right context. Precision beats volume every time.
Here's the practical takeaway: next time you're about to paste a file, stop and ask yourself what specific information in that file is necessary for the AI to answer your question. Then paste only that. Write a project context file today — it'll take 30 minutes and pay off in every future AI conversation. And when a conversation goes past 15 messages, start a new one with a crisp summary of decisions made. These three habits alone will make your AI interactions dramatically more productive than any prompt engineering trick you'll find in a YouTube tutorial.
