From 9433de75b8c5479994c70e1d72f705e8e806053e Mon Sep 17 00:00:00 2001 From: arkon Date: Wed, 4 Mar 2026 12:17:38 +0100 Subject: [PATCH] chore: version packages Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 6 + CLAUDE.md | 2 +- package-lock.json | 318 +++++++++++++- package.json | 4 +- public | 1 + src/web/public/app.js | 61 +++ src/web/public/styles.css | 2 + tools/remotion/Root.tsx | 27 +- tools/remotion/components/IOSKeyboard.tsx | 203 +++++++++ .../remotion/components/IPhone17ProFrame.tsx | 171 ++++++++ tools/remotion/components/TerminalScreen.tsx | 117 ++++++ tools/remotion/compositions/ZerolagDemo.tsx | 391 ++++++++++++++++++ 12 files changed, 1291 insertions(+), 12 deletions(-) create mode 120000 public create mode 100644 tools/remotion/components/IOSKeyboard.tsx create mode 100644 tools/remotion/components/IPhone17ProFrame.tsx create mode 100644 tools/remotion/components/TerminalScreen.tsx create mode 100644 tools/remotion/compositions/ZerolagDemo.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index e5875524..53b16898 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # aicodeman +## 0.3.2 + +### Patch Changes + +- Make file browser panel draggable by its header + ## 0.3.1 ### Patch Changes diff --git a/CLAUDE.md b/CLAUDE.md index 9f49bfbb..04384e39 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,7 +52,7 @@ When user says "COM": 4. **Sync CLAUDE.md version**: Update the `**Version**` line below to match the new version from `package.json` 5. **Commit and deploy**: `git add -A && git commit -m "chore: version packages" && git push && npm run build && systemctl --user restart codeman-web` -**Version**: 0.3.1 (must match `package.json`) +**Version**: 0.3.2 (must match `package.json`) ## Project Overview diff --git a/package-lock.json b/package-lock.json index afd7d7ff..3221ef39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aicodeman", - "version": "0.3.0", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aicodeman", - "version": "0.3.0", + "version": "0.3.1", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -17,6 +17,8 @@ "@fastify/compress": "^8.3.1", "@fastify/cookie": "^11.0.2", "@fastify/static": "^8.0.0", + "@remotion/compositor-linux-x64-gnu": "^4.0.432", + "@rspack/binding-linux-x64-gnu": "^1.7.7", "chalk": "^5.3.0", "chokidar": "^3.6.0", "commander": "^12.1.0", @@ -515,6 +517,40 @@ "node": ">=18" } }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "dev": true, @@ -1163,6 +1199,19 @@ "@module-federation/sdk": "0.22.0" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", + "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -1324,6 +1373,95 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@remotion/compositor-darwin-arm64": { + "version": "4.0.429", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.429.tgz", + "integrity": "sha512-V3Cv7TZyKulBPOBQJu2GONiKA+DDjGT6rWAc3WX4M9J9EDUwKiFFTDuTEpC7s4mcUDoAUXbnYE8ZlnWGaAuJhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-darwin-x64": { + "version": "4.0.429", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.429.tgz", + "integrity": "sha512-vB8NM0j7gvCniu1dZ0p4MmTDN44Dvgd2iq6vStigzEQ1P2vlcx7qHEdVfaZBHsjdjMXD/UdwNmb34+lw3v6KEw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-gnu": { + "version": "4.0.429", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.429.tgz", + "integrity": "sha512-F8N1HsRiCDw1EWWWI9hLNFTQy1mY7QLsWh0ywnrNCx62aF+8NpdYdW4pt/e7s4SQmkLh7enrkuqI0a6aGw4rpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-musl": { + "version": "4.0.429", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.429.tgz", + "integrity": "sha512-rvDjjascY9HAQ1nfbrdlVxyQ/xYj84jf0LylLmWJgr0kZVw5l8D/QXH4HTQeoPKhM3cBsCMKpW7oseHMoDyM5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-gnu": { + "version": "4.0.432", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.432.tgz", + "integrity": "sha512-KR/oSdCX6PBjO6K2KhYpC+QBbVMo8aXwQoTHaVwNl94+t7tyXDj7Vi8b3EJaIDSeXAc7lNjs54Tzx/WQ615Avg==", + "cpu": [ + "x64" + ], + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-musl": { + "version": "4.0.429", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.429.tgz", + "integrity": "sha512-tlgWyrOWFDw1Z8IyPGDrQhd2B3saZ/sGRC7nN0XhcbMnaq3wxWe+cUSCJPT9kFNR9tKNCZ0y/jG/B8h6Z0GiHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-win32-x64-msvc": { + "version": "4.0.429", + "resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.429.tgz", + "integrity": "sha512-YnpRTVwrOnDA8nYCZEP6Tdx+Ea+LQO4kXm9sjg2Rxe3s55lt+dUURl1yvOSKTW8nKZlfDAqldI1YDVvfD8NnCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@remotion/licensing": { "version": "4.0.429", "dev": true, @@ -1393,6 +1531,19 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@remotion/renderer/node_modules/@remotion/compositor-linux-x64-gnu": { + "version": "4.0.429", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.429.tgz", + "integrity": "sha512-YRe6+oSnNFpc2jgkrmAISAO2xUA0LqzE49ykqe0zCH3MXKtof76lMwDPoFJT1C4vDKErjSedltQNnnG7bPQGlw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@remotion/renderer/node_modules/source-map": { "version": "0.8.0-beta.0", "dev": true, @@ -1646,6 +1797,158 @@ "@rspack/binding-win32-x64-msvc": "1.7.6" } }, + "node_modules/@rspack/binding-darwin-arm64": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.6.tgz", + "integrity": "sha512-NZ9AWtB1COLUX1tA9HQQvWpTy07NSFfKBU8A6ylWd5KH8AePZztpNgLLAVPTuNO4CZXYpwcoclf8jG/luJcQdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rspack/binding-darwin-x64": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.6.tgz", + "integrity": "sha512-J2g6xk8ZS7uc024dNTGTHxoFzFovAZIRixUG7PiciLKTMP78svbSSWrmW6N8oAsAkzYfJWwQpVgWfFNRHvYxSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rspack/binding-linux-arm64-gnu": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.6.tgz", + "integrity": "sha512-eQfcsaxhFrv5FmtaA7+O1F9/2yFDNIoPZzV/ZvqvFz5bBXVc4FAm/1fVpBg8Po/kX1h0chBc7Xkpry3cabFW8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-arm64-musl": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.6.tgz", + "integrity": "sha512-DfQXKiyPIl7i1yECHy4eAkSmlUzzsSAbOjgMuKn7pudsWf483jg0UUYutNgXSlBjc/QSUp7906Cg8oty9OfwPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-x64-gnu": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.7.tgz", + "integrity": "sha512-aRvf8gCI7jDeEN9i4u9fY5coa3ZAyHzGVA4ZhTJCgZ5wWA5A9SQewMSq7khS1WAAFE1USlk1tUuPujnrGoYrGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-x64-musl": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.6.tgz", + "integrity": "sha512-rEy6MHKob02t/77YNgr6dREyJ0e0tv1X6Xsg8Z5E7rPXead06zefUbfazj4RELYySWnM38ovZyJAkPx/gOn3VA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-wasm32-wasi": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.6.tgz", + "integrity": "sha512-YupOrz0daSG+YBbCIgpDgzfMM38YpChv+afZpaxx5Ml7xPeAZIIdgWmLHnQ2rts73N2M1NspAiBwV00Xx0N4Vg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "1.0.7" + } + }, + "node_modules/@rspack/binding-win32-arm64-msvc": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.6.tgz", + "integrity": "sha512-INj7aVXjBvlZ84kEhSK4kJ484ub0i+BzgnjDWOWM1K+eFYDZjLdAsQSS3fGGXwVc3qKbPIssFfnftATDMTEJHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding-win32-ia32-msvc": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.6.tgz", + "integrity": "sha512-lXGvC+z67UMcw58In12h8zCa9IyYRmuptUBMItQJzu+M278aMuD1nETyGLL7e4+OZ2lvrnnBIcjXN1hfw2yRzw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding-win32-x64-msvc": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.6.tgz", + "integrity": "sha512-zeUxEc0ZaPpmaYlCeWcjSJUPuRRySiSHN23oJ2Xyw0jsQ01Qm4OScPdr0RhEOFuK/UE+ANyRtDo4zJsY52Hadw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding/node_modules/@rspack/binding-linux-x64-gnu": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.6.tgz", + "integrity": "sha512-NdA+2X3lk2GGrMMnTGyYTzM3pn+zNjaqXqlgKmFBXvjfZqzSsKq3pdD1KHZCd5QHN+Fwvoszj0JFsquEVhE1og==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rspack/core": { "version": "1.7.6", "dev": true, @@ -1700,6 +2003,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "dev": true, diff --git a/package.json b/package.json index d87deaf5..3244784d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aicodeman", - "version": "0.3.1", + "version": "0.3.2", "description": "The missing control plane for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence", "type": "module", "main": "dist/index.js", @@ -51,6 +51,8 @@ "@fastify/compress": "^8.3.1", "@fastify/cookie": "^11.0.2", "@fastify/static": "^8.0.0", + "@remotion/compositor-linux-x64-gnu": "^4.0.432", + "@rspack/binding-linux-x64-gnu": "^1.7.7", "chalk": "^5.3.0", "chokidar": "^3.6.0", "commander": "^12.1.0", diff --git a/public b/public new file mode 120000 index 00000000..b9e407f6 --- /dev/null +++ b/public @@ -0,0 +1 @@ +tools/remotion/public \ No newline at end of file diff --git a/src/web/public/app.js b/src/web/public/app.js index 5f226a6d..3817b42e 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -3560,6 +3560,24 @@ class CodemanApp { if (fileBrowserPanel) { fileBrowserPanel.classList.add('visible'); this.loadFileBrowser(sessionId); + // Attach drag listeners if not already attached + if (!this.fileBrowserDragListeners) { + const header = fileBrowserPanel.querySelector('.file-browser-header'); + if (header) { + const onFirstDrag = () => { + if (!fileBrowserPanel.style.left) { + const rect = fileBrowserPanel.getBoundingClientRect(); + fileBrowserPanel.style.left = `${rect.left}px`; + fileBrowserPanel.style.top = `${rect.top}px`; + fileBrowserPanel.style.right = 'auto'; + } + }; + header.addEventListener('mousedown', onFirstDrag); + header.addEventListener('touchstart', onFirstDrag, { passive: true }); + this.fileBrowserDragListeners = this.makeWindowDraggable(fileBrowserPanel, header); + this.fileBrowserDragListeners._onFirstDrag = onFirstDrag; + } + } } } }); @@ -7050,6 +7068,25 @@ class CodemanApp { if (showFileBrowser && this.activeSessionId) { fileBrowserPanel.classList.add('visible'); this.loadFileBrowser(this.activeSessionId); + // Attach drag listeners if not already attached + if (!this.fileBrowserDragListeners) { + const header = fileBrowserPanel.querySelector('.file-browser-header'); + if (header) { + // Convert right-positioned to left/top before drag so makeWindowDraggable works + const onFirstDrag = () => { + if (!fileBrowserPanel.style.left) { + const rect = fileBrowserPanel.getBoundingClientRect(); + fileBrowserPanel.style.left = `${rect.left}px`; + fileBrowserPanel.style.top = `${rect.top}px`; + fileBrowserPanel.style.right = 'auto'; + } + }; + header.addEventListener('mousedown', onFirstDrag); + header.addEventListener('touchstart', onFirstDrag, { passive: true }); + this.fileBrowserDragListeners = this.makeWindowDraggable(fileBrowserPanel, header); + this.fileBrowserDragListeners._onFirstDrag = onFirstDrag; + } + } } else { fileBrowserPanel.classList.remove('visible'); } @@ -10349,6 +10386,7 @@ class CodemanApp { fileBrowserExpandedDirs = new Set(); fileBrowserFilter = ''; fileBrowserAllExpanded = false; + fileBrowserDragListeners = null; filePreviewContent = ''; async loadFileBrowser(sessionId) { @@ -10532,6 +10570,29 @@ class CodemanApp { const panel = this.$('fileBrowserPanel'); if (panel) { panel.classList.remove('visible'); + // Reset position so it reopens at default location + panel.style.left = ''; + panel.style.top = ''; + panel.style.bottom = ''; + panel.style.right = ''; + } + // Clean up drag listeners + if (this.fileBrowserDragListeners) { + const dl = this.fileBrowserDragListeners; + document.removeEventListener('mousemove', dl.move); + document.removeEventListener('mouseup', dl.up); + document.removeEventListener('touchmove', dl.touchMove); + document.removeEventListener('touchend', dl.up); + document.removeEventListener('touchcancel', dl.up); + if (dl.handle) { + dl.handle.removeEventListener('mousedown', dl.handleMouseDown); + dl.handle.removeEventListener('touchstart', dl.handleTouchStart); + if (dl._onFirstDrag) { + dl.handle.removeEventListener('mousedown', dl._onFirstDrag); + dl.handle.removeEventListener('touchstart', dl._onFirstDrag); + } + } + this.fileBrowserDragListeners = null; } // Save setting const settings = this.loadAppSettingsFromStorage(); diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 6fba197d..4418d1a5 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -5825,6 +5825,8 @@ kbd { background: var(--bg-input); border-bottom: 1px solid var(--border); flex-shrink: 0; + cursor: move; + user-select: none; } .file-browser-title { diff --git a/tools/remotion/Root.tsx b/tools/remotion/Root.tsx index b4592351..9fda9557 100644 --- a/tools/remotion/Root.tsx +++ b/tools/remotion/Root.tsx @@ -1,16 +1,27 @@ import React from 'react'; import { Composition } from 'remotion'; import { CodemanDemo, TOTAL_FRAMES } from './compositions/CodemanDemo'; +import { ZerolagDemo, ZEROLAG_TOTAL_FRAMES } from './compositions/ZerolagDemo'; export const RemotionRoot: React.FC = () => { return ( - + <> + + + ); }; diff --git a/tools/remotion/components/IOSKeyboard.tsx b/tools/remotion/components/IOSKeyboard.tsx new file mode 100644 index 00000000..80b8faae --- /dev/null +++ b/tools/remotion/components/IOSKeyboard.tsx @@ -0,0 +1,203 @@ +import React from 'react'; +import { fonts } from '../lib/theme'; + +type IOSKeyboardProps = { + activeKey?: string; + /** How many frames since the key was pressed (for highlight decay) */ + pressAge?: number; +}; + +const ROW_1 = ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p']; +const ROW_2 = ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l']; +const ROW_3 = ['z', 'x', 'c', 'v', 'b', 'n', 'm']; + +const KEY_H = 42; +const KEY_GAP = 6; +const ROW_GAP = 11; +const SIDE_PAD = 3; + +const KEYBOARD_BG = '#1c1c1e'; +const KEY_BG = '#3a3a3c'; +const KEY_BG_ACTIVE = '#636366'; +const KEY_TEXT = '#fff'; +const SPECIAL_BG = '#2c2c2e'; + +const Key: React.FC<{ + label: string; + width: number; + isActive: boolean; + pressAge: number; + fontSize?: number; +}> = ({ label, width, isActive, pressAge, fontSize = 22 }) => { + // Highlight decays over 4 frames + const highlightOpacity = isActive && pressAge < 4 ? 1 - pressAge / 4 : 0; + const bg = highlightOpacity > 0 + ? lerpColor(KEY_BG, KEY_BG_ACTIVE, highlightOpacity) + : KEY_BG; + + return ( +
+ {label} +
+ ); +}; + +function lerpColor(a: string, b: string, t: number): string { + const pa = parseInt(a.slice(1), 16); + const pb = parseInt(b.slice(1), 16); + const r = Math.round(((pa >> 16) & 0xff) * (1 - t) + ((pb >> 16) & 0xff) * t); + const g = Math.round(((pa >> 8) & 0xff) * (1 - t) + ((pb >> 8) & 0xff) * t); + const bl = Math.round((pa & 0xff) * (1 - t) + (pb & 0xff) * t); + return `#${((r << 16) | (g << 8) | bl).toString(16).padStart(6, '0')}`; +} + +export const IOSKeyboard: React.FC = ({ activeKey, pressAge = 99 }) => { + const isActive = (key: string) => + activeKey !== undefined && key.toLowerCase() === activeKey.toLowerCase(); + const age = (key: string) => (isActive(key) ? pressAge : 99); + + // Key widths: 10 keys + 9 gaps in ~375px row → each key ~33px + const letterKeyW = 33; + // Row 2 has 9 keys → same key width but centered with side padding + // Row 3 has shift + 7 keys + delete + + return ( +
+ {/* Row 1: q-p */} +
+ {ROW_1.map((k) => ( + + ))} +
+ + {/* Row 2: a-l */} +
+ {ROW_2.map((k) => ( + + ))} +
+ + {/* Row 3: shift + z-m + delete */} +
+
+ + + +
+ {ROW_3.map((k) => ( + + ))} +
+ + + + +
+
+ + {/* Row 4: 123 / globe / space / return */} +
+
+ 123 +
+
+ + + + +
+ {/* Space bar */} + +
+ return +
+
+
+ ); +}; diff --git a/tools/remotion/components/IPhone17ProFrame.tsx b/tools/remotion/components/IPhone17ProFrame.tsx new file mode 100644 index 00000000..19f94b3e --- /dev/null +++ b/tools/remotion/components/IPhone17ProFrame.tsx @@ -0,0 +1,171 @@ +import React from 'react'; +import { spring, useCurrentFrame, useVideoConfig } from 'remotion'; + +/** + * Pixel-accurate iPhone 17 Pro frame. + * + * Dimensions based on iPhone 16 Pro (same form factor): + * - Screen: 393×852 CSS points (2622×1206 @3x) + * - Corner radius: 55px (device), 50px (screen inner) + * - Bezel: ~3.5px (thinnest in any iPhone) + * - Dynamic Island: 126×37 pill, centered 13px from top + * - Safe area: top 59px, bottom 34px (home indicator) + * - Frame: natural titanium (#8a8a8e border) + */ + +type IPhone17ProFrameProps = { + children: React.ReactNode; + /** Disable the spring entrance animation */ + noAnimation?: boolean; +}; + +// Device dimensions (CSS points) +const SCREEN_W = 393; +const SCREEN_H = 852; +const BEZEL = 4; +const DEVICE_W = SCREEN_W + BEZEL * 2; // 401 +const DEVICE_H = SCREEN_H + BEZEL * 2; // 860 +const DEVICE_RADIUS = 55; +const SCREEN_RADIUS = 50; + +// Dynamic Island +const DI_W = 126; +const DI_H = 37; +const DI_TOP = 13; // from top of screen +const DI_RADIUS = DI_H / 2; // pill shape + +export { SCREEN_W, SCREEN_H }; + +export const IPhone17ProFrame: React.FC = ({ children, noAnimation }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const scale = noAnimation + ? 1 + : spring({ frame, fps, config: { damping: 15, stiffness: 80 } }); + + return ( +
+ {/* Titanium frame (outer body) */} +
+ {/* Side button (right) — power */} +
+ + {/* Side buttons (left) — volume up, down, action */} +
+
+
+ + {/* Screen */} +
+ {/* App content */} + {children} + + {/* Dynamic Island (on top of everything) */} +
+
+
+ + {/* Home indicator */} +
+
+ ); +}; diff --git a/tools/remotion/components/TerminalScreen.tsx b/tools/remotion/components/TerminalScreen.tsx new file mode 100644 index 00000000..1ae79cf5 --- /dev/null +++ b/tools/remotion/components/TerminalScreen.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { useCurrentFrame } from 'remotion'; +import { colors, fonts } from '../lib/theme'; + +type OverlayChar = { + char: string; + confirmed: boolean; +}; + +type TerminalScreenProps = { + typed: string; + cursorVisible: boolean; + overlayChars?: OverlayChar[]; + fontSize?: number; +}; + +/** + * Renders a terminal area styled exactly like the real Codeman xterm.js terminal. + * Background #0d0d0d, Fira Code font, block cursor, Claude Code prompt. + */ +export const TerminalScreen: React.FC = ({ + typed, + cursorVisible, + overlayChars, + fontSize = 28, +}) => { + const frame = useCurrentFrame(); + + // Block cursor — solid, no blink (matches Codeman's cursorBlink: false) + const cursorOn = cursorVisible; + // But add a subtle blink for video clarity so viewers notice it + const cursorOpacity = cursorOn ? (Math.floor(frame / 20) % 2 === 0 ? 0.9 : 0.6) : 0; + + const lineHeight = Math.round(fontSize * 1.35); + const charWidth = fontSize * 0.6; + + return ( +
+ {/* Previous terminal output lines (fake history for realism) */} +
+
+ ❯ + claude --dangerously-skip-permissions +
+
+ ╭────────────────────────────────────╮ +
+
+ │ Claude Code session active │ +
+
+ ╰────────────────────────────────────╯ +
+
+ + {/* Active prompt line — this is where the typing happens */} +
+ {/* Prompt character */} + ❯ + + {/* Typed text */} + {overlayChars ? ( + // Zerolag mode: show overlay chars with confirmed/unconfirmed color + overlayChars.map((oc, i) => ( + + {oc.char} + + )) + ) : ( + {typed} + )} + + {/* Block cursor */} + +
+
+ ); +}; diff --git a/tools/remotion/compositions/ZerolagDemo.tsx b/tools/remotion/compositions/ZerolagDemo.tsx new file mode 100644 index 00000000..5a7c4c2c --- /dev/null +++ b/tools/remotion/compositions/ZerolagDemo.tsx @@ -0,0 +1,391 @@ +import React from 'react'; +import { + AbsoluteFill, + Img, + interpolate, + Sequence, + spring, + staticFile, + useCurrentFrame, + useVideoConfig, +} from 'remotion'; +import { colors, fonts } from '../lib/theme'; +import { IPhone17ProFrame, SCREEN_W, SCREEN_H } from '../components/IPhone17ProFrame'; +import { IOSKeyboard } from '../components/IOSKeyboard'; + +// ─── Scene timing (frames @ 30fps) ─── +const TITLE_DUR = 60; +const PHONES_DUR = 30; +const TYPING_DUR = 420; +const HOLD_DUR = 60; +const OUTRO_DUR = 45; + +const TITLE_START = 0; +const PHONES_START = TITLE_DUR; // 60 +const TYPING_START = PHONES_START + PHONES_DUR; // 90 +const HOLD_START = TYPING_START + TYPING_DUR; // 510 +const OUTRO_START = HOLD_START + HOLD_DUR; // 570 + +export const ZEROLAG_TOTAL_FRAMES = OUTRO_START + OUTRO_DUR; // 615 + +// ─── iPhone 17 Pro safe area ─── +const SAFE_AREA_TOP = 59; // Below Dynamic Island +const PHONE_SCALE = 1.12; // Scale up to fill more of the frame + +// The Codeman screenshot starts content at y=0 (the session tab). +// On a real device it would sit below the safe area, so we offset it. +const SCREENSHOT_Y_OFFSET = SAFE_AREA_TOP; + +// Terminal typing overlay position (relative to screenshot top) +// Session tab is ~44px, then terminal starts. Adding safe area offset: +const TERMINAL_TOP = SAFE_AREA_TOP + 52; +const TERMINAL_LEFT = 14; +const TERMINAL_FONT = 22; // Large for video readability + +// ─── Typing schedule ─── +const TYPED_TEXT = 'fix the auth bug in the login flow'; +const FRAME_GAP = 12; // ~400ms between keystrokes + +// Remote connection lag: 600ms-1.2s+ per char (18-36+ frames) +const LAGGY_DELAYS = [ + 24, 30, 36, 32, 26, 22, 34, 28, 38, 20, 30, 24, 32, 26, 36, 22, + 30, 24, 32, 28, 34, 26, 30, 22, 28, 36, 24, 30, 32, 26, 34, 28, 24, 30, +]; + +type KeyEvent = { frame: number; char: string; lagDelay: number }; + +const TYPING_SCHEDULE: KeyEvent[] = TYPED_TEXT.split('').map((char, i) => ({ + frame: i * FRAME_GAP, + char, + lagDelay: LAGGY_DELAYS[i % LAGGY_DELAYS.length], +})); + +const CONFIRM_DELAY = 14; + +// ─── iOS Status Bar (sits in the safe area, flanking Dynamic Island) ─── +const IOSStatusBar: React.FC = () => ( +
+ 9:41 +
+ {/* Signal */} + + + + + + + {/* WiFi */} + + + + + + {/* Battery */} + + + + + +
+
+); + +// ─── Typing overlay ─── +const TypingOverlay: React.FC<{ + typed: string; + overlayChars?: { char: string; confirmed: boolean }[]; + cursorVisible: boolean; +}> = ({ typed, overlayChars, cursorVisible }) => { + const frame = useCurrentFrame(); + const cursorOpacity = cursorVisible ? (Math.floor(frame / 18) % 2 === 0 ? 0.85 : 0.5) : 0; + const lineH = Math.round(TERMINAL_FONT * 1.4); + const charW = TERMINAL_FONT * 0.62; + + return ( +
+ {'❯ '} + {overlayChars + ? overlayChars.map((oc, i) => ( + + {oc.char} + + )) + : {typed}} + +
+ ); +}; + +// ─── Single phone: iPhone 17 Pro + real screenshot + typing + keyboard ─── +const MobileCodeman: React.FC<{ + typed: string; + overlayChars?: { char: string; confirmed: boolean }[]; + cursorVisible: boolean; + activeKey?: string; + pressAge?: number; + showKeyboard?: boolean; + noAnimation?: boolean; +}> = ({ typed, overlayChars, cursorVisible, activeKey, pressAge, showKeyboard = true, noAnimation }) => ( + +
+ {/* Real Codeman mobile screenshot, pushed down by safe area */} + + + {/* iOS status bar in the safe area */} + + + {/* Typing animation */} + + + {/* iOS keyboard at bottom */} + {showKeyboard && ( +
+ +
+ )} +
+
+); + +// ─── Label beneath phone ─── +const PhoneLabel: React.FC<{ + title: string; + detail: string; + dotColor: string; + detailColor: string; +}> = ({ title, detail, dotColor, detailColor }) => ( +
+
+
+ {title} +
+
+ {detail} +
+
+); + +// ─── Title scene ─── +const TitleScene: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const titleScale = spring({ frame, fps, config: { damping: 15, stiffness: 80 } }); + const titleOpacity = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: 'clamp' }); + const subtitleOpacity = interpolate(frame, [15, 35], [0, 1], { extrapolateRight: 'clamp' }); + + return ( + +
+
+ Zerolag Input +
+
+ Local echo for remote terminal sessions +
+
+
+ ); +}; + +// ─── Outro ─── +const OutroScene: React.FC = () => { + const frame = useCurrentFrame(); + const opacity = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: 'clamp' }); + + return ( + +
+
Codeman
+
+ Zero-latency mobile input +
+
+ npm i xterm-zerolag-input +
+
+
+ ); +}; + +// ─── Typing demo scene ─── +const TypingDemo: React.FC = () => { + const frame = useCurrentFrame(); + + const laggyChars = TYPING_SCHEDULE.filter((ev) => frame >= ev.frame + ev.lagDelay).map((ev) => ev.char); + const laggyTyped = laggyChars.join(''); + + const zerolagOverlay = TYPING_SCHEDULE.filter((ev) => frame >= ev.frame).map((ev) => ({ + char: ev.char, + confirmed: frame >= ev.frame + CONFIRM_DELAY, + })); + + let activeKey: string | undefined; + let pressAge = 99; + for (let i = TYPING_SCHEDULE.length - 1; i >= 0; i--) { + const ev = TYPING_SCHEDULE[i]; + if (frame >= ev.frame && frame < ev.frame + 5) { + activeKey = ev.char; + pressAge = frame - ev.frame; + break; + } + } + + return ( + +
+
+ + +
+
+ + +
+
+
+ ); +}; + +// ─── Phones entrance ─── +const PanelsEntrance: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const scale = spring({ frame, fps, config: { damping: 15, stiffness: 80 } }); + const opacity = interpolate(frame, [0, 10], [0, 1], { extrapolateRight: 'clamp' }); + + return ( + +
+
+ + +
+
+ + +
+
+
+ ); +}; + +// ─── Main composition ─── +export const ZerolagDemo: React.FC = () => { + return ( + + + + + + + + + + + + + + + ); +};