Skip to main content
session-replay 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.
跳到安装 Skills Marketplace 发现并探索由社区构建的 Agent Skills
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/JDerekLomas/claude-code-skills --skill session-replay命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
下载 Zip 下载中... 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
Find the project and extract real prompts from Claude history
Copy 6-8 representative images to public/<slug>/
Write the session HTML to public/sessions/<slug>.html
Add the session to the gallery in src/app/sessions/page.tsx
Step 1: Find Prompts
Search Claude history JSONL files for the project:
ls -lhS ~/.claude/projects/-Users-dereklomas-<project-path>/
Extract human messages from the largest JSONL file(s):
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:
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.
<!DOCTYPE html >
<html lang ="en" >
<head >
<meta charset ="UTF-8" >
<meta name ="viewport" content ="width=device-width, initial-scale=1.0" >
<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 ; : -apple-system, sans-serif; : white; }
{ : ; : ; : auto; : pointer; }
{ : fixed; : ; : ; : ; : none; : ; : ; }
{ : ; }
all sessions
Step 4: Update the Gallery
Add a new entry to the sessions array in src/app/sessions/page.tsx:
{
slug : "<slug>" ,
title : "Project Title" ,
subtitle : "Short description" ,
thumbnail : "/<slug>/thumbnail-image.png" ,
date : "Mon YYYY" ,
duration : "1:XX" ,
tool : "Claude Code" ,
prompts : 20 ,
},
Event Types Reference
Type Fields Description title-cardbgImage, title, subtitle, metaFull-screen image with text overlay. Use for opening and closing. terminal-start— Opens the terminal window. Use once at the beginning. terminal-resume— Returns to terminal after an image scene. prompttextUser prompt with typewriter animation and green > prefix. aitextAI response in blue. codetextCode output in yellow. image-revealkey, labelFull-screen single image reveal. Label format: "Title · Detail" image-gridkeys2x2 grid of 4 images with staggered fade-in. end— Marks the end of the animation.
Timeline Pacing Guidelines
Title card : 5 seconds before first terminal event
Between prompts : 2.5-3.5 seconds apart
Before image-reveal : leave ~2.5s gap after last prompt
Image-reveal duration : ~6 seconds visible before terminal-resume
Image-grid duration : ~8 seconds visible before terminal-resume
End title card : ~5.5 seconds before end event
Total runtime : 80-110 seconds (aim for ~1:30)
Customization Checklist
When creating a new session, customize these items:
Color Palette Examples
Project accent gold Vibe Garden of Eden #c4956a#C4A35AWarm desert Design Therapy #7c9885#c4956aSage green Alchemy Deck #C4A35A#C4A35AAlchemical gold Futures Deck #3b82f6#C4A35AElectric blue Therapy Cards #c67b5c#C4A35AWarm terracotta dereklomas.me #8B7355#D4A76AScholarly brown
Deployment
All session files go in /Users/dereklomas/codevibing2/:
HTML files: public/sessions/<slug>.html
Images: public/<slug>/
Gallery: src/app/sessions/page.tsx
Deploy: vercel --prod from the codevibing2 directory.
font-family
color
canvas
border-radius
12px
max-width
100%
height
cursor
.back
position
top
20px
left
20px
color
#6a6a80
text-decoration
font-size
14px
z-index
10
.back
:hover
color
#f0ece4
</style >
</head >
<body >
<a href ="/sessions" class ="back" >
←
</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 ;
const C = {
bg : '#08080c' ,
surface : '#101018' ,
border : '#1c1c2c' ,
text : '#f0ece4' ,
dim : '#7a7068' ,
prompt : '#6ee7b7' ,
ai : '#93c5fd' ,
code : '#fbbf24' ,
accent : '#c4956a' ,
violet : '#a78bfa' ,
rust : '#c67b5c' ,
lapis : '#4a7bbd' ,
gold : '#C4A35A' ,
};
const images = {};
const imageSources = {
img1 : '/<slug>/image1.png' ,
img2 : '/<slug>/image2.png' ,
};
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;
});
const events = [
{ t : 0 , type : 'title-card' , bgImage : 'img1' , title : 'PROJECT_TITLE' , subtitle : 'SUBTITLE' , meta : 'built with Claude Code · DATE' },
{ 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" },
{ t : 14000 , type : 'image-reveal' , key : 'img2' , label : 'Image Title · Detail' },
{ t : 20000 , type : 'terminal-resume' },
{ t : 20500 , type : 'prompt' , text : "next prompt continues the story" },
{ t : 28000 , type : 'image-grid' , keys : ['img1' , 'img2' , 'img3' , 'img4' ] },
{ 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 ;
});
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 );
ctx.textAlign = 'right' ; ctx.font = '12px "SF Mono", monospace' ;
ctx.fillText (`${Math .floor(secs/60 )} :${String (secs%60 ).padStart(2 ,'0' )} ` , tX + tW - 18 , dY + 4 );
ctx.textAlign = 'left' ;
const content = events.filter (e => e.t <= elapsed && ['prompt' ,'ai' ,'code' ].includes (e.type ));
const lH = 48 , sY = tY + 78 , sX = tX + 32 ;
const maxL = Math .floor ((tH - 90 ) / lH);
content.slice (-maxL).forEach ((ev, i ) => {
const y = sY + i * lH, age = elapsed - ev.t ;
ctx.globalAlpha = easeOut (Math .min (age / 350 , 1 ));
const fs = 20 ;
if (ev.type === 'prompt' ) {
ctx.font = `bold ${fs} px "SF Mono", monospace` ; ctx.fillStyle = C.prompt ;
ctx.fillText ('> ' , sX, y);
const pw = ctx.measureText ('> ' ).width ;
ctx.font = `${fs} px "SF Mono", monospace` ; ctx.fillStyle = C.text ;
const ch = Math .min (Math .floor (age / 28 ), ev.text .length );
ctx.fillText (ev.text .substring (0 , ch), sX + pw, y);
if (ch < ev.text .length && Math .floor (elapsed/530 )%2 ===0 ) {
ctx.fillStyle = C.violet ;
ctx.fillRect (sX + pw + ctx.measureText (ev.text .substring (0 ,ch)).width + 2 , y-15 , 11 , 22 );
}
} else if (ev.type === 'ai' ) {
ctx.font = `${fs} px "SF Mono", monospace` ; ctx.fillStyle = C.ai ;
ctx.fillText (ev.text .substring (0 , Math .min (Math .floor (age/22 ), ev.text .length )), sX+12 , y);
} else if (ev.type === 'code' ) {
ctx.font = `${fs-1 } px "SF Mono", monospace` ; ctx.fillStyle = C.code ;
ctx.fillText (ev.text .substring (0 , Math .min (Math .floor (age/18 ), ev.text .length )), sX+12 , y);
}
ctx.globalAlpha = 1 ;
});
const pY = tY+tH-14 , pX = tX+20 , pW = tW-40 ;
ctx.fillStyle = C.border ; ctx.fillRect (pX, pY, pW, 2 );
const maxT = events[events.length -1 ].t ;
const g = ctx.createLinearGradient (pX,0 ,pX+pW,0 );
g.addColorStop (0 ,C.rust ); g.addColorStop (1 ,C.gold );
ctx.fillStyle = g; ctx.fillRect (pX, pY, Math .max ((elapsed/maxT)*pW,2 ), 2 );
ctx.restore ();
}
function drawImageReveal (elapsed ) {
const reveals = events.filter (e => e.type ==='image-reveal' && e.t <=elapsed);
const cur = reveals[reveals.length -1 ]; if (!cur) return ;
const age = elapsed - cur.t , img = images[cur.key ];
ctx.fillStyle = C.bg ; ctx.fillRect (0 ,0 ,W,H);
const iW=520 , iH=620 , iX=W-iW-60 , iY=(H-iH)/2 ;
ctx.globalAlpha = easeOut (Math .min (age/800 ,1 ));
ctx.save (); drawRoundRect (iX,iY,iW,iH,8 ); ctx.clip ();
drawImageCover (img, iX, iY, iW, iH); ctx.restore ();
ctx.globalAlpha = Math .max (easeOut (Math .min ((age-400 )/600 ,1 )),0 );
const lX=80 , lY=H/2 ;
ctx.font ='500 12px -apple-system, sans-serif' ; ctx.fillStyle =C.dim ;
ctx.fillText ('PROJECT LABEL' , lX, lY-50 );
const parts = cur.label .split (' · ' );
ctx.font ='600 36px -apple-system, sans-serif' ; ctx.fillStyle =C.text ;
ctx.fillText (parts[0 ], lX, lY-10 );
if (parts[1 ]) { ctx.font ='300 22px -apple-system, sans-serif' ; ctx.fillStyle =C.gold ; ctx.fillText (parts[1 ], lX, lY+25 ); }
ctx.globalAlpha = 1 ;
}
function drawImageGrid (elapsed ) {
const grids = events.filter (e => e.type ==='image-grid' && e.t <=elapsed);
const cur = grids[grids.length -1 ]; if (!cur) return ;
const age = elapsed - cur.t ;
ctx.fillStyle = C.bg ; ctx.fillRect (0 ,0 ,W,H);
const gap=16 , tW=W-160 , tH=H-120 , cW=(tW-gap)/2 , cH=(tH-gap)/2 ;
cur.keys .forEach ((key, i ) => {
const col=i%2 , row=Math .floor (i/2 );
const x=80 +col*(cW+gap), y=60 +row*(cH+gap), delay=i*400 , ca=age-delay;
if (ca<0 ) return ;
const fade=easeOut (Math .min (ca/600 ,1 )), scale=lerp (0.92 ,1 ,easeOut (Math .min (ca/800 ,1 )));
ctx.globalAlpha =fade;
ctx.save ();
const cx=x+cW/2 , cy=y+cH/2 ;
ctx.translate (cx,cy); ctx.scale (scale,scale); ctx.translate (-cx,-cy);
drawRoundRect (x,y,cW,cH,10 ); ctx.clip ();
drawImageCover (images[key], x, y, cW, cH);
ctx.restore (); ctx.globalAlpha =1 ;
});
}
function drawFrame (elapsed ) {
ctx.fillStyle = C.bg ; ctx.fillRect (0 ,0 ,W,H);
const mode = getActiveMode (elapsed);
if (mode==='title-card' ) drawTitleCard (elapsed);
else if (mode==='image-reveal' ) drawImageReveal (elapsed);
else if (mode==='image-grid' ) drawImageGrid (elapsed);
else drawTerminal (elapsed);
ctx.globalAlpha =0.2 ; ctx.font ='bold 13px -apple-system, sans-serif' ;
ctx.fillStyle =C.dim ; ctx.textAlign ='right' ;
ctx.fillText ('codevibing' , W-24 , H-12 );
ctx.textAlign ='left' ; ctx.globalAlpha =1 ;
const maxT=events[events.length -1 ].t ;
ctx.fillStyle ='rgba(255,255,255,0.04)' ; ctx.fillRect (0 ,H-3 ,W,3 );
const g=ctx.createLinearGradient (0 ,0 ,W,0 );
g.addColorStop (0 ,C.rust ); g.addColorStop (1 ,C.violet );
ctx.fillStyle =g; ctx.fillRect (0 ,H-3 ,Math .max ((elapsed/maxT)*W,2 ),3 );
}
function animate (ts ) {
if (!startTime) startTime = ts;
currentElapsed = ts - startTime;
drawFrame (currentElapsed);
const maxT = events[events.length -1 ].t ;
if (currentElapsed < maxT + 2000 ) {
animFrame = requestAnimationFrame (animate);
} else {
animRunning = false ;
}
}
function play ( ) {
startTime = null ; zoom=1 ; zoomTarget=1 ; panY=0 ; panYTarget=0 ;
currentElapsed = 0 ; animRunning = true ;
if (animFrame) cancelAnimationFrame (animFrame);
animFrame = requestAnimationFrame (animate);
}
function jumpTo (t ) {
startTime = performance.now () - t;
currentElapsed = t;
if (!animRunning) {
animRunning = true ;
animFrame = requestAnimationFrame (animate);
}
}
canvas.addEventListener ('click' , () => {
const maxT = events[events.length - 1 ].t ;
if (!animRunning || currentElapsed >= maxT) {
play (); return ;
}
const next = events.find (e => e.t > currentElapsed + 200 );
if (next) {
jumpTo (next.t );
} else {
jumpTo (maxT);
}
});
function autoplay ( ) {
if (imagesLoaded >= Object .keys (imageSources).length ) { play (); return ; }
if (imagesLoaded > 0 ) { play (); return ; }
setTimeout (play, 2000 );
}
setTimeout (autoplay, 500 );
</script >
</body >
</html >