diff --git a/README.md b/README.md index f0623fff44d..334ab25aa9d 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Ghostty-class terminals with WebGL rendering, infinite splits, and scrollback th - Terminal splits + Terminal splits @@ -96,7 +96,7 @@ Browse PRs, issues, and project boards in-app — open a worktree from any task - GitHub and Linear task workflows in Orca + GitHub and Linear task workflows in Orca @@ -110,7 +110,7 @@ Run agents on a beefy remote box with full file editing, git, and terminals — - Remote worktrees over SSH + Remote worktrees over SSH @@ -152,7 +152,7 @@ Agents drive Orca too — script every workflow with `orca worktree create`, `sn - Script Orca from the CLI + Script Orca from the CLI diff --git a/config/scripts/check-feature-wall-assets.mjs b/config/scripts/check-feature-wall-assets.mjs index c9e3fa02805..d65a228d5d3 100644 --- a/config/scripts/check-feature-wall-assets.mjs +++ b/config/scripts/check-feature-wall-assets.mjs @@ -1,29 +1,16 @@ #!/usr/bin/env node import { readdir, stat } from 'node:fs/promises' import path from 'node:path' +import { FEATURE_WALL_TILES, isFeatureWallMediaTile } from '../../src/shared/feature-wall-tiles.ts' const __dirname = import.meta.dirname const ROOT = path.join(__dirname, '..', '..') const FEATURE_WALL_ASSET_DIR = path.join(ROOT, 'resources', 'onboarding', 'feature-wall') const MAX_BYTES = 11 * 1024 * 1024 -const MEDIA_TILE_IDS = [ - 'tile-01', - 'tile-02', - 'tile-03', - 'tile-04', - 'tile-05', - 'tile-06', - 'tile-07', - 'tile-08', - 'tile-09', - 'tile-10', - 'tile-11', - 'tile-12' -] -const EXPECTED_FILES = MEDIA_TILE_IDS.flatMap((id) => [ - `${id}.gif`, - `${id}.poster.jpg`, - `${id}.recorded-at.json` +const EXPECTED_FILES = FEATURE_WALL_TILES.filter(isFeatureWallMediaTile).flatMap((tile) => [ + tile.gifPath, + tile.posterPath, + tile.recordedAtPath ]) async function collectFiles(dir) { diff --git a/config/scripts/vendor-feature-wall-assets.mjs b/config/scripts/vendor-feature-wall-assets.mjs index b1e9b061f8f..c2c9a7781cd 100644 --- a/config/scripts/vendor-feature-wall-assets.mjs +++ b/config/scripts/vendor-feature-wall-assets.mjs @@ -1,8 +1,11 @@ #!/usr/bin/env node -import { copyFile, mkdir, writeFile } from 'node:fs/promises' +import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { spawnSync } from 'node:child_process' -import { homedir } from 'node:os' +import { homedir, tmpdir } from 'node:os' import path from 'node:path' +import { runProcessSync } from '@orca/process-host' +import { FEATURE_WALL_TILES } from '../../src/shared/feature-wall-tiles.ts' +import { verifyLosslessFeatureWallMedia } from './verify-feature-wall-lossless-media.mjs' const __dirname = import.meta.dirname const ROOT = path.join(__dirname, '..', '..') @@ -101,29 +104,66 @@ function gitRecordedAtSeconds(tile, relativePath) { await mkdir(DEST_ROOT, { recursive: true }) -for (const tile of TILES) { - const sourceRoot = sourceRootForTile(tile) - const sourceGif = path.join(sourceRoot, ...tile.gifRelativePath.split('/')) - const sourcePoster = path.join(sourceRoot, ...tile.posterRelativePath.split('/')) - const destGif = path.join(DEST_ROOT, `${tile.id}.gif`) - const destPoster = path.join(DEST_ROOT, `${tile.id}.poster.jpg`) - const recordedAtSeconds = gitRecordedAtSeconds(tile, tile.gifRelativePath) +const originals = new Map() +const stagingRoot = await mkdtemp(path.join(tmpdir(), 'orca-feature-wall-media-')) +try { + for (const tile of TILES) { + const sourceRoot = sourceRootForTile(tile) + const sourceGif = path.join(sourceRoot, ...tile.gifRelativePath.split('/')) + const sourcePoster = path.join(sourceRoot, ...tile.posterRelativePath.split('/')) + const media = FEATURE_WALL_TILES.find((item) => item.id === tile.id) + if (!media || media.kind !== 'media') { + throw new Error(`Unknown media tile: ${tile.id}`) + } + const destAnimation = path.join(stagingRoot, media.gifPath) + const destPoster = path.join(stagingRoot, `${tile.id}.poster.jpg`) + const recordedAtSeconds = gitRecordedAtSeconds(tile, tile.gifRelativePath) - await copyFile(sourceGif, destGif) - await copyFile(sourcePoster, destPoster) - await writeFile( - path.join(DEST_ROOT, `${tile.id}.recorded-at.json`), - `${JSON.stringify( - { - recordedAtUnixSeconds: recordedAtSeconds, - recordedAtIso: new Date(recordedAtSeconds * 1000).toISOString(), - sourceGif: tile.gifRelativePath, - sourcePoster: tile.posterRelativePath - }, - null, - 2 - )}\n` - ) + originals.set(tile.id, await readFile(sourceGif)) + if (media.gifPath.endsWith('.gif')) { + await copyFile(sourceGif, destAnimation) + } else { + // gif2webp defaults to lossless; the verifier rejects changed pixels or timing. + const result = runProcessSync({ + program: 'gif2webp', + args: ['-m', '6', '-min_size', '-metadata', 'all', sourceGif, '-o', destAnimation], + timeoutMs: 120_000 + }) + if (result.code !== 0) { + throw new Error(`gif2webp failed for ${tile.id}: ${result.stderr}`) + } + } + await copyFile(sourcePoster, destPoster) + await writeFile( + path.join(stagingRoot, `${tile.id}.recorded-at.json`), + `${JSON.stringify( + { + recordedAtUnixSeconds: recordedAtSeconds, + recordedAtIso: new Date(recordedAtSeconds * 1000).toISOString(), + sourceGif: tile.gifRelativePath, + sourcePoster: tile.posterRelativePath + }, + null, + 2 + )}\n` + ) - console.log(`Vendored ${tile.gifRelativePath} -> ${tile.id}`) + console.log(`Vendored ${tile.gifRelativePath} -> ${tile.id}`) + } + + await verifyLosslessFeatureWallMedia(originals, stagingRoot) + for (const tile of TILES) { + const media = FEATURE_WALL_TILES.find((item) => item.id === tile.id) + if (media?.kind !== 'media') { + throw new Error(`Unknown media tile: ${tile.id}`) + } + for (const file of [media.gifPath, media.posterPath, media.recordedAtPath]) { + await copyFile(path.join(stagingRoot, file), path.join(DEST_ROOT, file)) + } + if (media.gifPath.endsWith('.webp')) { + await rm(path.join(DEST_ROOT, `${tile.id}.gif`), { force: true }) + } + } +} finally { + await rm(stagingRoot, { recursive: true, force: true }) } diff --git a/config/scripts/verify-feature-wall-lossless-media.mjs b/config/scripts/verify-feature-wall-lossless-media.mjs new file mode 100644 index 00000000000..76274691721 --- /dev/null +++ b/config/scripts/verify-feature-wall-lossless-media.mjs @@ -0,0 +1,198 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict' +import { readFile } from 'node:fs/promises' +import { createServer } from 'node:http' +import path from 'node:path' +import { chromium } from 'playwright-core' +import { runProcessSync } from '@orca/process-host' +import { FEATURE_WALL_TILES, isFeatureWallMediaTile } from '../../src/shared/feature-wall-tiles.ts' + +const ROOT = path.resolve(import.meta.dirname, '..', '..') +const ASSET_DIR = 'resources/onboarding/feature-wall' + +function skipGifSubBlocks(bytes, offset) { + while (offset < bytes.length && bytes[offset] !== 0) { + offset += 1 + bytes[offset] + } + assert.ok(offset < bytes.length, 'Truncated GIF sub-block') + return offset + 1 +} + +function gifDurations(bytes) { + assert.match(bytes.subarray(0, 6).toString(), /^GIF8[79]a$/) + let offset = 13 + (bytes[10] & 128 ? 3 * 2 ** ((bytes[10] & 7) + 1) : 0) + let duration = 0 + const durations = [] + while (offset < bytes.length) { + const block = bytes[offset++] + if (block === 0x3b) { + return durations + } + if (block === 0x21) { + const label = bytes[offset++] + if (label === 0xf9) { + duration = bytes.readUInt16LE(offset + 2) * 10 + } + offset = skipGifSubBlocks(bytes, offset) + } else { + assert.equal(block, 0x2c, 'Invalid GIF image descriptor') + const packed = bytes[offset + 8] + offset += 9 + (packed & 128 ? 3 * 2 ** ((packed & 7) + 1) : 0) + offset = skipGifSubBlocks(bytes, offset + 1) + durations.push(duration) + duration = 0 + } + } + throw new Error('Missing GIF trailer') +} + +function animationDurations(bytes) { + if (bytes.subarray(0, 3).toString() === 'GIF') { + return gifDurations(bytes) + } + assert.equal(bytes.subarray(0, 4).toString(), 'RIFF') + assert.equal(bytes.subarray(8, 12).toString(), 'WEBP') + const durations = [] + for (let offset = 12; offset < bytes.length;) { + const length = bytes.readUInt32LE(offset + 4) + if (bytes.subarray(offset, offset + 4).toString() === 'ANMF') { + durations.push(bytes.readUIntLE(offset + 20, 3)) + } + offset += 8 + length + (length % 2) + } + return durations +} + +export async function verifyLosslessFeatureWallMedia( + originals, + candidateDirectory = path.join(ROOT, ASSET_DIR) +) { + const candidates = new Map() + for (const tile of FEATURE_WALL_TILES.filter(isFeatureWallMediaTile)) { + const candidate = await readFile(path.join(candidateDirectory, tile.gifPath)) + const original = originals.get(tile.id) + assert.ok(original, `${tile.id}: original GIF missing`) + assert.deepEqual( + animationDurations(candidate), + animationDurations(original), + `${tile.id}: encoded per-frame timing changed` + ) + candidates.set(tile.id, candidate) + } + const server = createServer((request, response) => { + response.setHeader('Cache-Control', 'no-store') + if (request.url === '/') { + return response.end('Animation equality') + } + const [kind, id] = request.url.slice(1).split('/') + const bytes = (kind === 'original' ? originals : candidates).get(id) + response.statusCode = bytes ? 200 : 404 + response.end(bytes) + }) + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) + let browser + try { + browser = await chromium.launch({ + headless: true, + executablePath: process.env.ORCA_ANIMATION_CHROMIUM_EXECUTABLE || undefined + }) + const page = await browser.newPage() + await page.goto(`http://127.0.0.1:${server.address().port}`) + const results = [] + for (const tile of FEATURE_WALL_TILES.filter(isFeatureWallMediaTile)) { + const result = await page.evaluate(async ({ id, gifPath }) => { + const makeDecoder = async (kind, type) => + new ImageDecoder({ + data: await (await fetch(`/${kind}/${id}`)).arrayBuffer(), + type, + preferAnimation: true + }) + const original = await makeDecoder('original', 'image/gif') + const candidate = await makeDecoder( + 'candidate', + gifPath.endsWith('.gif') ? 'image/gif' : 'image/webp' + ) + try { + await Promise.all([original.tracks.ready, candidate.tracks.ready]) + const a = original.tracks.selectedTrack + const b = candidate.tracks.selectedTrack + if (a.frameCount !== b.frameCount || a.repetitionCount !== b.repetitionCount) { + throw new Error(`${id}: frame count or loop changed`) + } + let canvas, context + let durationUs = 0 + for (let frameIndex = 0; frameIndex < a.frameCount; frameIndex++) { + const first = (await original.decode({ frameIndex })).image + const second = (await candidate.decode({ frameIndex })).image + try { + if ( + first.displayWidth !== second.displayWidth || + first.displayHeight !== second.displayHeight || + first.duration !== second.duration || + first.timestamp !== second.timestamp + ) { + throw new Error(`${id}: dimensions or playback timing changed at ${frameIndex}`) + } + if (!canvas) { + canvas = new OffscreenCanvas(first.displayWidth, first.displayHeight) + context = canvas.getContext('2d', { willReadFrequently: true }) + } + context.clearRect(0, 0, canvas.width, canvas.height) + context.drawImage(first, 0, 0) + const left = context.getImageData(0, 0, canvas.width, canvas.height).data + context.clearRect(0, 0, canvas.width, canvas.height) + context.drawImage(second, 0, 0) + const right = context.getImageData(0, 0, canvas.width, canvas.height).data + const leftPixels = new Uint32Array(left.buffer) + const rightPixels = new Uint32Array(right.buffer) + for (let index = 0; index < leftPixels.length; index++) { + if (leftPixels[index] !== rightPixels[index]) { + throw new Error(`${id}: RGBA pixel ${index} changed at frame ${frameIndex}`) + } + } + durationUs += first.duration + } finally { + first.close() + second.close() + } + } + return { id, frames: a.frameCount, durationUs, exactRGBA: true } + } finally { + original.close() + candidate.close() + } + }, tile) + results.push(result) + console.log(JSON.stringify(result)) + } + return results + } finally { + await browser?.close() + await new Promise((resolve) => server.close(resolve)) + } +} + +function readOriginalGif(baseRef, id) { + const result = runProcessSync({ + program: 'git', + args: ['show', `${baseRef}:${ASSET_DIR}/${id}.gif`], + cwd: ROOT, + maxOutputBytes: 16 * 1024 * 1024, + captureStdoutAsBytes: true + }) + assert.equal(result.code, 0, result.stderr) + assert.ok(result.stdoutBytes) + return result.stdoutBytes +} + +if (process.argv[1] && path.resolve(process.argv[1]) === import.meta.filename) { + const baseRef = process.argv[2] + assert.ok(baseRef, 'Usage: node config/scripts/verify-feature-wall-lossless-media.mjs ') + const originals = new Map( + FEATURE_WALL_TILES.filter(isFeatureWallMediaTile).map((tile) => [ + tile.id, + readOriginalGif(baseRef, tile.id) + ]) + ) + await verifyLosslessFeatureWallMedia(originals) +} diff --git a/docs/readme/README.es.md b/docs/readme/README.es.md index 76b997525ab..6d3bc84cdf3 100644 --- a/docs/readme/README.es.md +++ b/docs/readme/README.es.md @@ -68,7 +68,7 @@ Terminales de nivel Ghostty con renderizado WebGL, divisiones infinitas y un scr - Terminales divididas + Terminales divididas @@ -96,7 +96,7 @@ Explora PRs, issues y tableros de proyecto dentro de la app — abre un worktree - Flujos de trabajo de GitHub y Linear en Orca + Flujos de trabajo de GitHub y Linear en Orca @@ -110,7 +110,7 @@ Ejecuta agentes en una máquina remota potente con edición completa de archivos - Worktrees remotos por SSH + Worktrees remotos por SSH @@ -152,7 +152,7 @@ Los agentes también manejan Orca — automatiza cualquier flujo de trabajo con - Automatiza Orca desde la CLI + Automatiza Orca desde la CLI diff --git a/docs/readme/README.fr.md b/docs/readme/README.fr.md index 3d81041f93c..dfab23efe00 100644 --- a/docs/readme/README.fr.md +++ b/docs/readme/README.fr.md @@ -72,7 +72,7 @@ Terminaux de niveau Ghostty avec rendu WebGL, splits infinis et un scrollback qu - Splits de terminal + Splits de terminal @@ -100,7 +100,7 @@ Parcourez PRs, issues et boards de projet dans l'app — ouvrez un worktree depu - Workflows GitHub et Linear dans Orca + Workflows GitHub et Linear dans Orca @@ -114,7 +114,7 @@ Faites tourner des agents sur une machine distante costaude, avec édition de fi - Worktrees distants via SSH + Worktrees distants via SSH @@ -156,7 +156,7 @@ Les agents pilotent aussi Orca — scriptez n'importe quel workflow avec `orca w - Scripter Orca depuis la CLI + Scripter Orca depuis la CLI diff --git a/docs/readme/README.ja.md b/docs/readme/README.ja.md index ff4cde89bc6..47631e0355f 100644 --- a/docs/readme/README.ja.md +++ b/docs/readme/README.ja.md @@ -68,7 +68,7 @@ WebGL レンダリング、無制限の分割、再起動後も残るスクロ - ターミナル分割 + ターミナル分割 @@ -96,7 +96,7 @@ PR、Issue、プロジェクトボードをアプリ内で閲覧 — 任意の - Orca の GitHub と Linear タスクワークフロー + Orca の GitHub と Linear タスクワークフロー @@ -110,7 +110,7 @@ PR、Issue、プロジェクトボードをアプリ内で閲覧 — 任意の - SSH 経由のリモートワークツリー + SSH 経由のリモートワークツリー @@ -152,7 +152,7 @@ PR、Issue、プロジェクトボードをアプリ内で閲覧 — 任意の - CLI から Orca をスクリプト操作 + CLI から Orca をスクリプト操作 diff --git a/docs/readme/README.ko.md b/docs/readme/README.ko.md index 70d11e7196a..9dd3d048334 100644 --- a/docs/readme/README.ko.md +++ b/docs/readme/README.ko.md @@ -68,7 +68,7 @@ WebGL 렌더링, 무한 분할, 재시작 후에도 유지되는 스크롤백을 - 터미널 분할 + 터미널 분할 @@ -96,7 +96,7 @@ PR, issue, 프로젝트 보드를 앱 안에서 탐색하세요 — 어떤 작 - Orca의 GitHub 및 Linear 작업 워크플로 + Orca의 GitHub 및 Linear 작업 워크플로 @@ -110,7 +110,7 @@ PR, issue, 프로젝트 보드를 앱 안에서 탐색하세요 — 어떤 작 - SSH를 통한 원격 worktree + SSH를 통한 원격 worktree @@ -152,7 +152,7 @@ diff의 어느 줄에든 코멘트를 남기고 에이전트에게 바로 보내 - CLI에서 Orca 스크립팅 + CLI에서 Orca 스크립팅 diff --git a/docs/readme/README.pt.md b/docs/readme/README.pt.md index 44b87a271fe..727f683b53f 100644 --- a/docs/readme/README.pt.md +++ b/docs/readme/README.pt.md @@ -68,7 +68,7 @@ Terminais no nível do Ghostty com renderização WebGL, divisões infinitas e s - Terminais divididos + Terminais divididos @@ -96,7 +96,7 @@ Navegue por PRs, issues e quadros de projeto dentro do app — abra um worktree - Fluxos de trabalho de tarefas do GitHub e Linear no Orca + Fluxos de trabalho de tarefas do GitHub e Linear no Orca @@ -110,7 +110,7 @@ Execute agentes em uma máquina remota potente com edição completa de arquivos - Worktrees remotos por SSH + Worktrees remotos por SSH @@ -152,7 +152,7 @@ Agentes também controlam o Orca — automatize qualquer fluxo de trabalho com ` - Automatize o Orca pela CLI + Automatize o Orca pela CLI diff --git a/docs/readme/README.zh-CN.md b/docs/readme/README.zh-CN.md index d76dc8e0228..0cee02206d1 100644 --- a/docs/readme/README.zh-CN.md +++ b/docs/readme/README.zh-CN.md @@ -68,7 +68,7 @@ Ghostty 级终端,支持 WebGL 渲染、无限分屏,以及重启后依然 - 终端分屏 + 终端分屏 @@ -96,7 +96,7 @@ Ghostty 级终端,支持 WebGL 渲染、无限分屏,以及重启后依然 - Orca 中的 GitHub 与 Linear 任务工作流 + Orca 中的 GitHub 与 Linear 任务工作流 @@ -110,7 +110,7 @@ Ghostty 级终端,支持 WebGL 渲染、无限分屏,以及重启后依然 - 通过 SSH 使用远程 worktree + 通过 SSH 使用远程 worktree @@ -152,7 +152,7 @@ VS Code 的编辑器,处处自动保存 — 把文件或图片直接拖入智 - 从 CLI 脚本化 Orca + 从 CLI 脚本化 Orca diff --git a/resources/onboarding/feature-wall/tile-01.gif b/resources/onboarding/feature-wall/tile-01.gif deleted file mode 100644 index 69193d12905..00000000000 Binary files a/resources/onboarding/feature-wall/tile-01.gif and /dev/null differ diff --git a/resources/onboarding/feature-wall/tile-01.webp b/resources/onboarding/feature-wall/tile-01.webp new file mode 100644 index 00000000000..fa29eb6818e Binary files /dev/null and b/resources/onboarding/feature-wall/tile-01.webp differ diff --git a/resources/onboarding/feature-wall/tile-02.gif b/resources/onboarding/feature-wall/tile-02.gif deleted file mode 100644 index 861c8959525..00000000000 Binary files a/resources/onboarding/feature-wall/tile-02.gif and /dev/null differ diff --git a/resources/onboarding/feature-wall/tile-02.webp b/resources/onboarding/feature-wall/tile-02.webp new file mode 100644 index 00000000000..e854eb52031 Binary files /dev/null and b/resources/onboarding/feature-wall/tile-02.webp differ diff --git a/resources/onboarding/feature-wall/tile-03.gif b/resources/onboarding/feature-wall/tile-03.gif deleted file mode 100644 index 8946d4fbfed..00000000000 Binary files a/resources/onboarding/feature-wall/tile-03.gif and /dev/null differ diff --git a/resources/onboarding/feature-wall/tile-03.webp b/resources/onboarding/feature-wall/tile-03.webp new file mode 100644 index 00000000000..f86a222fdcd Binary files /dev/null and b/resources/onboarding/feature-wall/tile-03.webp differ diff --git a/resources/onboarding/feature-wall/tile-04.gif b/resources/onboarding/feature-wall/tile-04.gif deleted file mode 100644 index bbc128af7c8..00000000000 Binary files a/resources/onboarding/feature-wall/tile-04.gif and /dev/null differ diff --git a/resources/onboarding/feature-wall/tile-04.webp b/resources/onboarding/feature-wall/tile-04.webp new file mode 100644 index 00000000000..fddd8132de0 Binary files /dev/null and b/resources/onboarding/feature-wall/tile-04.webp differ diff --git a/resources/onboarding/feature-wall/tile-05.gif b/resources/onboarding/feature-wall/tile-05.gif deleted file mode 100644 index ec59d5c3c1b..00000000000 Binary files a/resources/onboarding/feature-wall/tile-05.gif and /dev/null differ diff --git a/resources/onboarding/feature-wall/tile-05.webp b/resources/onboarding/feature-wall/tile-05.webp new file mode 100644 index 00000000000..f89cfe8bdb2 Binary files /dev/null and b/resources/onboarding/feature-wall/tile-05.webp differ diff --git a/resources/onboarding/feature-wall/tile-06.gif b/resources/onboarding/feature-wall/tile-06.gif deleted file mode 100644 index b6dc393fb1d..00000000000 Binary files a/resources/onboarding/feature-wall/tile-06.gif and /dev/null differ diff --git a/resources/onboarding/feature-wall/tile-06.webp b/resources/onboarding/feature-wall/tile-06.webp new file mode 100644 index 00000000000..9b6d75aa5c4 Binary files /dev/null and b/resources/onboarding/feature-wall/tile-06.webp differ diff --git a/resources/onboarding/feature-wall/tile-08.gif b/resources/onboarding/feature-wall/tile-08.gif deleted file mode 100644 index 015d63c849b..00000000000 Binary files a/resources/onboarding/feature-wall/tile-08.gif and /dev/null differ diff --git a/resources/onboarding/feature-wall/tile-08.webp b/resources/onboarding/feature-wall/tile-08.webp new file mode 100644 index 00000000000..03755f72334 Binary files /dev/null and b/resources/onboarding/feature-wall/tile-08.webp differ diff --git a/resources/onboarding/feature-wall/tile-09.gif b/resources/onboarding/feature-wall/tile-09.gif deleted file mode 100644 index d6b6b640934..00000000000 Binary files a/resources/onboarding/feature-wall/tile-09.gif and /dev/null differ diff --git a/resources/onboarding/feature-wall/tile-09.webp b/resources/onboarding/feature-wall/tile-09.webp new file mode 100644 index 00000000000..cace157fafb Binary files /dev/null and b/resources/onboarding/feature-wall/tile-09.webp differ diff --git a/resources/onboarding/feature-wall/tile-10.gif b/resources/onboarding/feature-wall/tile-10.gif deleted file mode 100644 index a407acbfc31..00000000000 Binary files a/resources/onboarding/feature-wall/tile-10.gif and /dev/null differ diff --git a/resources/onboarding/feature-wall/tile-10.webp b/resources/onboarding/feature-wall/tile-10.webp new file mode 100644 index 00000000000..10f506ee338 Binary files /dev/null and b/resources/onboarding/feature-wall/tile-10.webp differ diff --git a/resources/onboarding/feature-wall/tile-11.gif b/resources/onboarding/feature-wall/tile-11.gif deleted file mode 100644 index ba23ea67a67..00000000000 Binary files a/resources/onboarding/feature-wall/tile-11.gif and /dev/null differ diff --git a/resources/onboarding/feature-wall/tile-11.webp b/resources/onboarding/feature-wall/tile-11.webp new file mode 100644 index 00000000000..ec55f7b1a91 Binary files /dev/null and b/resources/onboarding/feature-wall/tile-11.webp differ diff --git a/resources/onboarding/feature-wall/tile-12.gif b/resources/onboarding/feature-wall/tile-12.gif deleted file mode 100644 index 591cd2269a2..00000000000 Binary files a/resources/onboarding/feature-wall/tile-12.gif and /dev/null differ diff --git a/resources/onboarding/feature-wall/tile-12.webp b/resources/onboarding/feature-wall/tile-12.webp new file mode 100644 index 00000000000..851d657d25b Binary files /dev/null and b/resources/onboarding/feature-wall/tile-12.webp differ diff --git a/src/shared/feature-wall-tiles.ts b/src/shared/feature-wall-tiles.ts index 59c39969b4a..a2a9376e64f 100644 --- a/src/shared/feature-wall-tiles.ts +++ b/src/shared/feature-wall-tiles.ts @@ -61,7 +61,7 @@ export const FEATURE_WALL_TILES: readonly FeatureWallTile[] = [ title: 'Parallel workspace orchestration', caption: 'Give each task its own workspace - no stashing, no branch juggling. Fan work across agents, compare, and continue with the best result.', - gifPath: 'tile-01.gif', + gifPath: 'tile-01.webp', posterPath: 'tile-01.poster.jpg', recordedAtPath: 'tile-01.recorded-at.json', owner: 'worktree-orchestration', @@ -73,7 +73,7 @@ export const FEATURE_WALL_TILES: readonly FeatureWallTile[] = [ title: 'Ghostty-class terminal', caption: 'WebGL rendering, infinite splits, scrollback restored on restart, full scrollback search.', - gifPath: 'tile-02.gif', + gifPath: 'tile-02.webp', posterPath: 'tile-02.poster.jpg', recordedAtPath: 'tile-02.recorded-at.json', owner: 'terminal', @@ -85,7 +85,7 @@ export const FEATURE_WALL_TILES: readonly FeatureWallTile[] = [ title: 'GitHub & Linear, native', caption: 'Find connected GitHub or Linear work in Tasks, open its context, and start workspaces without switching tools.', - gifPath: 'tile-03.gif', + gifPath: 'tile-03.webp', posterPath: 'tile-03.poster.jpg', recordedAtPath: 'tile-03.recorded-at.json', owner: 'task-integrations', @@ -96,7 +96,7 @@ export const FEATURE_WALL_TILES: readonly FeatureWallTile[] = [ kind: 'media', title: 'Supported CLI agents', caption: 'Claude Code, Codex, Cursor CLI, Gemini, Copilot, OpenCode, and Pi are preconfigured.', - gifPath: 'tile-04.gif', + gifPath: 'tile-04.webp', posterPath: 'tile-04.poster.jpg', recordedAtPath: 'tile-04.recorded-at.json', owner: 'agent-integrations', @@ -108,7 +108,7 @@ export const FEATURE_WALL_TILES: readonly FeatureWallTile[] = [ title: 'Embedded browser + Design Mode', caption: 'A real Chromium window per workspace. Click any UI element to send its HTML, CSS, and a cropped screenshot into your agent.', - gifPath: 'tile-05.gif', + gifPath: 'tile-05.webp', posterPath: 'tile-05.poster.jpg', recordedAtPath: 'tile-05.recorded-at.json', owner: 'browser-experience', @@ -120,7 +120,7 @@ export const FEATURE_WALL_TILES: readonly FeatureWallTile[] = [ title: 'Remote workspaces', caption: 'Run agents on a remote machine with the same Orca editing, git, and terminal workflow.', - gifPath: 'tile-06.gif', + gifPath: 'tile-06.webp', posterPath: 'tile-06.poster.jpg', recordedAtPath: 'tile-06.recorded-at.json', owner: 'ssh-workspaces', @@ -132,6 +132,7 @@ export const FEATURE_WALL_TILES: readonly FeatureWallTile[] = [ title: 'Monaco editor, drag-to-agent', caption: "VS Code's editor, autosave everywhere, quick-open with hidden files, drag-drop files or Finder images into an agent prompt.", + // gif2webp changes this recording's 10 ms frame delays. gifPath: 'tile-07.gif', posterPath: 'tile-07.poster.jpg', recordedAtPath: 'tile-07.recorded-at.json', @@ -144,7 +145,7 @@ export const FEATURE_WALL_TILES: readonly FeatureWallTile[] = [ title: 'Inline review, back to the agent', caption: 'Drop markdown comments on any diff line, batch them, ship them back to the agent. Inspect CI, resolve conflicts, open PRs - all in-app.', - gifPath: 'tile-08.gif', + gifPath: 'tile-08.webp', posterPath: 'tile-08.poster.jpg', recordedAtPath: 'tile-08.recorded-at.json', owner: 'diff-review', @@ -155,7 +156,7 @@ export const FEATURE_WALL_TILES: readonly FeatureWallTile[] = [ kind: 'media', title: 'Orca CLI', caption: 'Agents can drive Orca too: create workspaces, snapshot screens, click, and fill.', - gifPath: 'tile-09.gif', + gifPath: 'tile-09.webp', posterPath: 'tile-09.poster.jpg', recordedAtPath: 'tile-09.recorded-at.json', owner: 'orca-cli', @@ -167,7 +168,7 @@ export const FEATURE_WALL_TILES: readonly FeatureWallTile[] = [ title: 'Keyboard-native', caption: 'Jump across workspaces, open files, and remap every shortcut. Move at the speed of your fingers.', - gifPath: 'tile-10.gif', + gifPath: 'tile-10.webp', posterPath: 'tile-10.poster.jpg', recordedAtPath: 'tile-10.recorded-at.json', owner: 'keyboard-ux', @@ -179,7 +180,7 @@ export const FEATURE_WALL_TILES: readonly FeatureWallTile[] = [ title: 'Usage & rate-limit aware', caption: 'See Claude and Codex usage, rate-limit resets, and hot-swap Codex accounts without re-logging in.', - gifPath: 'tile-11.gif', + gifPath: 'tile-11.webp', posterPath: 'tile-11.poster.jpg', recordedAtPath: 'tile-11.recorded-at.json', owner: 'usage-rate-limits', @@ -191,7 +192,7 @@ export const FEATURE_WALL_TILES: readonly FeatureWallTile[] = [ title: 'PDFs, images, CSV, Markdown', caption: 'Preview everything your repo carries: PDFs, image diff modes, CSV tables, wiki-linked Markdown with search.', - gifPath: 'tile-12.gif', + gifPath: 'tile-12.webp', posterPath: 'tile-12.poster.jpg', recordedAtPath: 'tile-12.recorded-at.json', owner: 'file-preview',