Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions .github/scripts/create-tapes.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,37 @@ Sleep ${wait}s`;
? 'Enter\nSleep 1s\nEnter'
: 'Enter';

// VHS Type command must be single-line; split multi-line prompts
const lines = text.split('\n');
let typeBlock;
if (lines.length > 1) {
typeBlock = lines
.map((line, i) => i < lines.length - 1 ? `Type "${line}"\nEnter` : `Type "${line}"`)
.join('\n');
} else {
typeBlock = `Type "${text}"`;
}

// Break response wait into chunks with periodic hidden nudges.
// A hidden space+backspace forces copilot's TUI to scroll to the input area.
const nudgeInterval = 3;
let waitBlock = '';
let remaining = wait;
while (remaining > nudgeInterval) {
waitBlock += `Sleep ${nudgeInterval}s\nHide\nType " "\nBackspace\nShow\n`;
remaining -= nudgeInterval;
}
if (remaining > 0) {
waitBlock += `Sleep ${remaining}s`;
}

return `# ${label}
Type "${text}"
${typeBlock}
Sleep 2s
${enterBlock}

# Wait for response
Sleep ${wait}s`;
# Wait for response (with periodic nudges to keep input visible)
${waitBlock}`;
}

function generateTapeContent(demo, settings) {
Expand Down Expand Up @@ -95,9 +119,14 @@ Sleep ${s.startupWait}s

${promptBlocks}

# Nudge TUI to scroll to input area
Type " "
Backspace
Sleep ${s.exitWait}s

# Exit cleanly
Ctrl+C
Sleep ${s.exitWait}s
Sleep 1s
`;
}

Expand Down
28 changes: 6 additions & 22 deletions .github/scripts/demos.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"typingSpeed": "60ms",
"framerate": 10,
"startupWait": 5,
"responseWait": 25,
"exitWait": 2
"responseWait": 40,
"exitWait": 3
},
"demos": [
{
Expand Down Expand Up @@ -69,14 +69,14 @@
"chapter": "03-development-workflows",
"name": "refactor-demo",
"description": "Workflow 2: Refactoring - refactor command handling to dictionary dispatch",
"responseWait": 30,
"responseWait": 60,
"prompt": "@samples/book-app-project/book_app.py Refactor the command handling to use a dictionary dispatch pattern instead of if/elif chains"
},
{
"chapter": "03-development-workflows",
"name": "fix-bug-demo",
"description": "Workflow 3: Debugging - debug a search issue",
"responseWait": 30,
"responseWait": 60,
"prompt": "@samples/book-app-buggy/books_buggy.py Users report that searching for 'The Hobbit' returns no results even though it's in the data. Debug why."
},
{
Expand Down Expand Up @@ -110,8 +110,8 @@
"chapter": "05-skills",
"name": "skill-trigger-demo",
"description": "Copilot detects and triggers a matching skill",
"responseWait": 45,
"prompt": "Review the book collection code for issues"
"responseWait": 120,
"prompt": "Check the book collection code for quality issues"
},
{
"chapter": "06-mcp-servers",
Expand All @@ -129,22 +129,6 @@
{ "text": "@samples/book-app-project/books.py What code handles the search functionality?", "responseWait": 20 },
{ "text": "Based on the code, suggest improvements for the search feature", "responseWait": 30 }
]
},
{
"chapter": "07-putting-it-together",
"name": "full-review-demo",
"description": "Idea to merged PR: plan, implement, test a new feature",
"prompts": [
{ "text": "I need to add a 'list unread' command to the book app that shows only books where read is False. What files need to change?", "responseWait": 25 },
{ "text": "/agent", "agentSelect": "python-reviewer", "arrowDown": 3, "responseWait": 3 },
{ "text": "@samples/book-app-project/books.py Design a get_unread_books method. What is the best approach?", "responseWait": 30 },
{ "text": "/agent", "agentSelect": "pytest-helper", "arrowDown": 2, "responseWait": 3 },
{ "text": "@samples/book-app-project/tests/test_books.py Design test cases for filtering unread books.", "responseWait": 30 },
{ "text": "Add a get_unread_books method to BookCollection in books.py. Add a 'list unread' command option in book_app.py. Update the help text in the show_help function.", "responseWait": 30 },
{ "text": "Generate comprehensive tests for the new feature", "responseWait": 35 },
{ "text": "/review", "responseWait": 25 },
{ "text": "Create a pull request titled 'Feature: Add list unread books command'", "responseWait": 25 }
]
}
]
}
108 changes: 108 additions & 0 deletions .github/scripts/preview-gifs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env node
/**
* Extract a preview frame from each demo GIF for quick visual inspection.
* Saves individual PNG frames to demo-previews/ directory.
*
* Requires: ffmpeg, gifsicle (for frame delay info)
*
* Usage:
* node preview-gifs.js # default: 3s before end
* node preview-gifs.js --before 5 # 5s before end
*/

const { execSync } = require('child_process');
const { readdirSync, existsSync, mkdirSync, rmSync } = require('fs');
const { join, basename } = require('path');

const rootDir = join(__dirname, '..', '..');
const previewDir = join(rootDir, 'demo-previews');

// Parse CLI args
const args = process.argv.slice(2);
let beforeSeconds = 3;

for (let i = 0; i < args.length; i++) {
if (args[i] === '--before' && args[i + 1]) {
beforeSeconds = parseFloat(args[++i]);
}
}

// Find all demo GIFs
function findGifs() {
const gifs = [];
for (const entry of readdirSync(rootDir)) {
if (!/^\d{2}-/.test(entry)) continue;
const imagesDir = join(rootDir, entry, 'images');
if (!existsSync(imagesDir)) continue;
for (const file of readdirSync(imagesDir)) {
if (file.endsWith('-demo.gif')) {
gifs.push({ path: join(imagesDir, file), chapter: entry });
}
}
}
return gifs.sort((a, b) => a.path.localeCompare(b.path));
}

// Get frame delays from a GIF
function getFrameDelays(gifPath) {
const output = execSync(`gifsicle --info "${gifPath}"`, { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
const delays = [];
const delayRegex = /delay (\d+(?:\.\d+)?)s/g;
let match;
while ((match = delayRegex.exec(output)) !== null) {
delays.push(parseFloat(match[1]));
}
return delays;
}

// Find frame index at N seconds before the end
function frameAtSecondsBeforeEnd(delays, seconds) {
const totalTime = delays.reduce((a, b) => a + b, 0);
const targetTime = totalTime - seconds;
if (targetTime <= 0) return 0;

let cumulative = 0;
for (let i = 0; i < delays.length; i++) {
cumulative += delays[i];
if (cumulative >= targetTime) return i;
}
return delays.length - 1;
}

// Main
if (existsSync(previewDir)) rmSync(previewDir, { recursive: true });
mkdirSync(previewDir, { recursive: true });

const gifs = findGifs();
if (gifs.length === 0) {
console.log('No demo GIFs found');
process.exit(0);
}

console.log(`\nExtracting frames (${beforeSeconds}s before end) from ${gifs.length} GIFs...\n`);

let count = 0;
for (const { path: gif, chapter } of gifs) {
const name = basename(gif, '.gif');
const delays = getFrameDelays(gif);
const frameIndex = frameAtSecondsBeforeEnd(delays, beforeSeconds);
const prefix = chapter.replace(/^(\d+)-.+/, '$1');
const outName = `${prefix}-${name}.png`;
const outPath = join(previewDir, outName);

try {
execSync(
`ffmpeg -y -i "${gif}" -vf "select=eq(n\\,${frameIndex})" -vframes 1 -update 1 "${outPath}" 2>/dev/null`,
{ stdio: 'pipe' }
);
console.log(` ✓ ${outName} (frame #${frameIndex}/${delays.length})`);
count++;
} catch (e) {
console.log(` ✗ ${name}: extraction failed`);
}
}

console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
console.log(`✓ ${count} preview frames saved to demo-previews/`);
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
console.log(`\nOpen in Finder: open demo-previews/`);
1 change: 1 addition & 0 deletions .github/scripts/scan-demos.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const defaultSettings = {
height: 600,
theme: "Dracula",
typingSpeed: "60ms",
framerate: 15,
startupWait: 5,
responseWait: 25,
exitWait: 2
Comment on lines +30 to 33
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The framerate: 15 default added here diverges from the framerate: 10 used in demos.json (line 8). Similarly, responseWait: 25 and exitWait: 2 here don't match the updated demos.json values of responseWait: 40 and exitWait: 3. While scan:demos was removed from the generate:demos pipeline, it's still available as a standalone script (npm run scan:demos). Running it would overwrite demos.json with these stale defaults, undoing the curated settings. Consider updating the defaults here to match demos.json, or adding a comment noting these are intentionally different.

Suggested change
framerate: 15,
startupWait: 5,
responseWait: 25,
exitWait: 2
framerate: 10,
startupWait: 5,
responseWait: 40,
exitWait: 3

Copilot uses AI. Check for mistakes.
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,4 @@ Desktop.ini
.claude
.plans
.plans.vhs-wrapper
demo-previews
Binary file modified 00-quick-start/images/hello-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
76 changes: 73 additions & 3 deletions 00-quick-start/images/hello-demo.tape
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,79 @@ Type "Say hello and tell me what you can help with"
Sleep 2s
Enter

# Wait for response
Sleep 25s
# Wait for response (with periodic nudges to keep input visible)
Sleep 3s
Hide
Type " "
Backspace
Show
Sleep 3s
Hide
Type " "
Backspace
Show
Sleep 3s
Hide
Type " "
Backspace
Show
Sleep 3s
Hide
Type " "
Backspace
Show
Sleep 3s
Hide
Type " "
Backspace
Show
Sleep 3s
Hide
Type " "
Backspace
Show
Sleep 3s
Hide
Type " "
Backspace
Show
Sleep 3s
Hide
Type " "
Backspace
Show
Sleep 3s
Hide
Type " "
Backspace
Show
Sleep 3s
Hide
Type " "
Backspace
Show
Sleep 3s
Hide
Type " "
Backspace
Show
Sleep 3s
Hide
Type " "
Backspace
Show
Sleep 3s
Hide
Type " "
Backspace
Show
Sleep 1s

# Nudge TUI to scroll to input area
Type " "
Backspace
Sleep 3s

# Exit cleanly
Ctrl+C
Sleep 2s
Sleep 1s
Binary file modified 01-setup-and-first-steps/images/code-review-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading