- name
- session-replay
- description
- Create a codevibing session replay — an animated canvas-based video that replays a Claude Code building session with real prompts, AI responses, and project images. Use when asked to create a session, session replay, or codevibing video for a project.
# Session Replay Creation
Create cinematic canvas-animated replays of Claude Code sessions for codevibing.com. Each session is a standalone HTML file that autoplays a timeline of typed prompts, AI responses, and image reveals.
## Overview
1. Find the project and extract real prompts from Claude history
2. Copy 6-8 representative images to `public/<slug>/`
3. Write the session HTML to `public/sessions/<slug>.html`
4. Add the session to the gallery in `src/app/sessions/page.tsx`
## Step 1: Find Prompts
Search Claude history JSONL files for the project:
```bash
ls -lhS ~/.claude/projects/-Users-dereklomas-<project-path>/
```
Extract human messages from the largest JSONL file(s):
```python
import json
fname = '<session-id>.jsonl'
with open(fname) as f:
for line in f:
obj = json.loads(line)
if obj.get('type') == 'user':
msg = obj.get('message', {})
content = msg.get('content', '')
if isinstance(content, list):
text = ' '.join(p.get('text','') for p in content if isinstance(p, dict) and p.get('type') == 'text')
else:
text = str(content)
text = text.strip()
if len(text) > 10 and not text.startswith(('<task-', '<local-command', '<command-', '[Request interrupted')):
ts = obj.get('timestamp', '')
print(f'[{ts}] {text[:120]}')
```
Curate to ~15-22 prompts that tell the narrative arc. Look for:
- The genesis prompt (what kicked it off)
- Key creative decisions and pivots
- Design critiques and iterations
- Review interfaces (thumbs up/down, card review UIs, approval workflows)
- Deployment moments (github, vercel, domain setup)
- New websites going live (show as `ai` events: "Deployed to project.vercel.app")
- The "aha" moments
**Important**: Don't skip process moments like review UIs, feedback loops, and deployments — these show the human-AI collaboration workflow that makes sessions interesting.
Also check git log for context: `git -C ~/path/to/project log --oneline`
## Step 2: Copy Images
Pick 6-8 representative images from the project that show the visual output. Copy them to the codevibing2 public directory:
```bash
mkdir -p ~/codevibing2/public/<slug>/
cp ~/project/path/to/images/*.png ~/codevibing2/public/<slug>/
```
Choose images that:
- Show different stages or categories of the project
- Work well as backgrounds (title cards) and in 2x2 grids
- Are visually distinct from each other
## Step 3: Write the Session HTML
Create `public/sessions/<slug>.html` using this template. Customize the marked sections.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- CUSTOMIZE: title and meta tags -->
<title>PROJECT_TITLE — codevibing session</title>
<meta property="og:title" content="PROJECT_TITLE — SUBTITLE">
<meta property="og:description" content="Watch DESCRIPTION get built with AI">
<meta name="twitter:card" content="summary_large_image">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #08080c; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, sans-serif; color: white; }
canvas { border-radius: 12px; max-width: 100%; height: auto; cursor: pointer; }
.back { position: fixed; top: 20px; left: 20px; color: #6a6a80; text-decoration: none; font-size: 14px; z-index: 10; }
.back:hover { color: #f0ece4; }
</style>
</head>
<body>
<a href="/sessions" class="back">← all sessions</a>
<canvas id="canvas" width="1280" height="720"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = 1280, H = 720;
// CUSTOMIZE: accent and gold colors to match project aesthetic
const C = {
bg: '#08080c',
surface: '#101018',
border: '#1c1c2c',
text: '#f0ece4',
dim: '#7a7068',
prompt: '#6ee7b7',
ai: '#93c5fd',
code: '#fbbf24',
accent: '#c4956a', // CUSTOMIZE: project accent color
violet: '#a78bfa',
rust: '#c67b5c',
lapis: '#4a7bbd',
gold: '#C4A35A', // CUSTOMIZE: project gold/secondary color
};
// CUSTOMIZE: image sources from public/<slug>/
const images = {};
const imageSources = {
img1: '/<slug>/image1.png',
img2: '/<slug>/image2.png',
// ... 6-8 images
};
let imagesLoaded = 0;
Object.entries(imageSources).forEach(([key, src]) => {
const img = new Image();
img.onload = () => { imagesLoaded++; };
img.onerror = () => { imagesLoaded++; };
img.src = src;
images[key] = img;
});
// CUSTOMIZE: the entire events timeline
const events = [
// --- TITLE CARD (0-5s) ---
{ t: 0, type: 'title-card', bgImage: 'img1', title: 'PROJECT_TITLE', subtitle: 'SUBTITLE', meta: 'built with Claude Code · DATE' },
// --- TERMINAL SESSION ---
{ t: 5000, type: 'terminal-start' },
{ t: 5500, type: 'prompt', text: "the first real prompt from the user" },
{ t: 8500, type: 'prompt', text: "second prompt" },
{ t: 11000, type: 'ai', text: "AI response summary" },
// --- IMAGE REVEAL (full-screen single image) ---
{ t: 14000, type: 'image-reveal', key: 'img2', label: 'Image Title · Detail' },
// --- RETURN TO TERMINAL ---
{ t: 20000, type: 'terminal-resume' },
{ t: 20500, type: 'prompt', text: "next prompt continues the story" },
// --- IMAGE GRID (2x2 grid of 4 images) ---
{ t: 28000, type: 'image-grid', keys: ['img1', 'img2', 'img3', 'img4'] },
// --- END TITLE CARD ---
{ t: 36000, type: 'title-card', bgImage: 'img3', title: 'PROJECT_TITLE', subtitle: 'project-url.vercel.app', meta: 'built with Claude Code' },
{ t: 42000, type: 'end' },
];
let startTime = null;
let animFrame = null;
let currentElapsed = 0;
let animRunning = false;
let zoom = 1, zoomTarget = 1, panY = 0, panYTarget = 0;
function lerp(a, b, t) { return a + (b - a) * Math.min(Math.max(t, 0), 1); }
function easeOut(t) { return 1 - Math.pow(1 - Math.min(t, 1), 3); }
function drawRoundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath();
}
function drawImageCover(img, x, y, w, h) {
if (!img || !img.complete || !img.naturalWidth) return;
const imgR = img.naturalWidth / img.naturalHeight, boxR = w / h;
let sx = 0, sy = 0, sw = img.naturalWidth, sh = img.naturalHeight;
if (imgR > boxR) { sw = sh * boxR; sx = (img.naturalWidth - sw) / 2; }
else { sh = sw / boxR; sy = (img.naturalHeight - sh) / 2; }
ctx.drawImage(img, sx, sy, sw, sh, x, y, w, h);
}
function getActiveMode(elapsed) {
const visible = events.filter(e => e.t <= elapsed);
for (let i = visible.length - 1; i >= 0; i--) {
const t = visible[i].type;
if (t === 'title-card') return 'title-card';
if (t === 'image-reveal') return 'image-reveal';
if (t === 'image-grid') return 'image-grid';
if (t === 'terminal-resume' || t === 'terminal-start') return 'terminal';
if (['prompt','ai','code'].includes(t)) return 'terminal';
}
return 'title-card';
}
function drawTitleCard(elapsed) {
const cards = events.filter(e => e.t <= elapsed && e.type === 'title-card');
const card = cards[cards.length - 1];
if (!card) return;
const age = elapsed - card.t;
const img = images[card.bgImage];
if (img && img.complete && img.naturalWidth) {
ctx.globalAlpha = easeOut(Math.min(age / 1200, 1));
drawImageCover(img, 0, 0, W, H);
const ov = ctx.createLinearGradient(0, 0, 0, H);
ov.addColorStop(0, 'rgba(8,8,12,0.4)');
ov.addColorStop(0.4, 'rgba(8,8,12,0.3)');
ov.addColorStop(0.7, 'rgba(8,8,12,0.6)');
ov.addColorStop(1, 'rgba(8,8,12,0.85)');
ctx.fillStyle = ov; ctx.fillRect(0, 0, W, H);
ctx.globalAlpha = 1;
}
ctx.globalAlpha = Math.max(easeOut(Math.min((age - 500) / 800, 1)), 0);
const tx = 80, ty = H - 120;
ctx.shadowColor = 'rgba(0,0,0,0.7)'; ctx.shadowBlur = 24;
ctx.font = '700 72px -apple-system, sans-serif';
ctx.fillStyle = '#fff'; ctx.textAlign = 'left';
ctx.fillText(card.title, tx, ty);
ctx.font = '300 28px -apple-system, sans-serif';
ctx.fillStyle = C.gold; ctx.shadowBlur = 12;
ctx.fillText(card.subtitle, tx, ty + 42);
if (card.meta) {
ctx.font = '400 15px -apple-system, sans-serif';
ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.shadowBlur = 6;
ctx.fillText(card.meta, tx, ty + 72);
}
ctx.shadowBlur = 0; ctx.globalAlpha = 1;
}
function drawTerminal(elapsed) {
zoom = lerp(zoom, zoomTarget, 0.06);
panY = lerp(panY, panYTarget, 0.06);
ctx.save();
ctx.translate(W/2, H/2); ctx.scale(zoom, zoom); ctx.translate(-W/2, -H/2 + panY);
const tX = 60, tY = 30, tW = W - 120, tH = H - 60;
drawRoundRect(tX, tY, tW, tH, 14); ctx.fillStyle = C.surface; ctx.fill();
ctx.strokeStyle = C.border; ctx.lineWidth = 1; ctx.stroke();
drawRoundRect(tX, tY, tW, 44, 14); ctx.fillStyle = '#141420'; ctx.fill();
ctx.fillStyle = C.surface; ctx.fillRect(tX + 1, tY + 30, tW - 2, 14);
const dY = tY + 22;
[[tX+22,'#ff5f57'],[tX+42,'#ffbd2e'],[tX+62,'#28c840']].forEach(([dx,c]) => {
ctx.beginPath(); ctx.arc(dx, dY, 6, 0, Math.PI*2);
ctx.fillStyle = c; ctx.globalAlpha = 0.85; ctx.fill(); ctx.globalAlpha = 1;
});
// CUSTOMIZE: terminal title bar path
ctx.font = '500 13px "SF Mono", monospace'; ctx.fillStyle = C.dim;
ctx.fillText('~/project-path · claude', tX + 86, dY + 4);
const secs = Math.floor(elapsed / 1000);
View on GitHub