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
-
+
@@ -96,7 +96,7 @@ Browse PRs, issues, and project boards in-app — open a worktree from any task
-
+
@@ -110,7 +110,7 @@ Run agents on a beefy remote box with full file editing, git, and terminals —
-
+
@@ -152,7 +152,7 @@ Agents drive Orca too — script every workflow with `orca worktree create`, `sn
-
+
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
-
+
@@ -96,7 +96,7 @@ Explora PRs, issues y tableros de proyecto dentro de la app — abre un worktree
-
+
@@ -110,7 +110,7 @@ Ejecuta agentes en una máquina remota potente con edición completa de archivos
-
+
@@ -152,7 +152,7 @@ Los agentes también manejan Orca — automatiza cualquier flujo de trabajo con
-
+
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
-
+
@@ -100,7 +100,7 @@ Parcourez PRs, issues et boards de projet dans l'app — ouvrez un worktree depu
-
+
@@ -114,7 +114,7 @@ Faites tourner des agents sur une machine distante costaude, avec édition de fi
-
+
@@ -156,7 +156,7 @@ Les agents pilotent aussi Orca — scriptez n'importe quel workflow avec `orca w
-
+
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、プロジェクトボードをアプリ内で閲覧 — 任意の
-
+
@@ -110,7 +110,7 @@ PR、Issue、プロジェクトボードをアプリ内で閲覧 — 任意の
-
+
@@ -152,7 +152,7 @@ PR、Issue、プロジェクトボードをアプリ内で閲覧 — 任意の
-
+
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, 프로젝트 보드를 앱 안에서 탐색하세요 — 어떤 작
-
+
@@ -110,7 +110,7 @@ PR, issue, 프로젝트 보드를 앱 안에서 탐색하세요 — 어떤 작
-
+
@@ -152,7 +152,7 @@ diff의 어느 줄에든 코멘트를 남기고 에이전트에게 바로 보내
-
+
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
-
+
@@ -96,7 +96,7 @@ Navegue por PRs, issues e quadros de projeto dentro do app — abra um worktree
-
+
@@ -110,7 +110,7 @@ Execute agentes em uma máquina remota potente com edição completa de arquivos
-
+
@@ -152,7 +152,7 @@ Agentes também controlam o Orca — automatize qualquer fluxo de trabalho com `
-
+
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 渲染、无限分屏,以及重启后依然
-
+
@@ -110,7 +110,7 @@ Ghostty 级终端,支持 WebGL 渲染、无限分屏,以及重启后依然
-
+
@@ -152,7 +152,7 @@ VS Code 的编辑器,处处自动保存 — 把文件或图片直接拖入智
-
+
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',