A Next.js web UI to browse, view diffs, and recover files from OpenCode sessions. OpenCode captures snapshots before every AI step, storing them as git tree objects. This tool exposes that hidden history.
- Framework: Next.js 16 with App Router (server components by default)
- Styling: Tailwind CSS with custom
oc-*color tokens (OpenCode brand colors) - Language: TypeScript
- Data: Reads from
~/.local/share/opencode/filesystem (no database)
src/
├── app/ # Next.js App Router
│ ├── api/ # API routes (all dynamic)
│ ├── projects/[id]/ # Project detail pages
│ ├── layout.tsx # Root layout with nav
│ └── page.tsx # Home page (project list)
└── lib/
└── opencode.ts # Data layer - ALL filesystem access here
Never scan all projects when you only need one.
This file contains ALL filesystem operations. When modifying:
getProject(id)- Loads a SINGLE project by ID. Does NOT callgetProjects().getProjects()- Only used on the home page to list all projects.projectExists(id)- Lightweight existence check (singlefs.existsSync).getProjectGitDir(id)- Returns git dir path without any I/O validation.
// BAD: Scans every project just to get one
export function getProject(id: string): Project | null {
const projects = getProjects(); // Reads ALL projects!
return projects.find((p) => p.id === id);
}
// GOOD: Direct lookup
export function getProject(id: string): Project | null {
return loadProjectById(id); // Reads only the requested project
}// BAD: Full project load just to check existence
const project = getProject(projectId);
if (!project) return 404;
// GOOD: Lightweight check
if (!projectExists(projectId)) return 404;// BAD: Full project load just to get gitDir
const project = getProject(projectId);
const output = git(project.gitDir, ...);
// GOOD: Direct path construction
const gitDir = getProjectGitDir(projectId);
const output = git(gitDir, ...);OpenCode stores data across multiple directories:
~/.local/share/opencode/snapshot/{projectId}/- Git bare repos~/.local/share/opencode/storage/project/- Project metadata~/.local/share/opencode/storage/session/{projectId}/- Session JSON files~/.local/share/opencode/storage/session_diff/- File diffs~/.local/share/opencode/storage/part/- Message parts with snapshot refs
A full getProjects() call reads ALL of these for EVERY project. With many projects/sessions, this causes noticeable delays.
| Function | I/O Cost | When to Use |
|---|---|---|
projectExists(id) |
1 fs.existsSync | Validation only |
getProjectGitDir(id) |
0 (pure) | Need git dir path |
getProject(id) |
~5-20 file reads | Need full project metadata |
getProjects() |
N × getProject | Home page only |
getSnapshots(projectId) |
Scans ALL 44k+ part files | EXPENSIVE - lazy load only |
getSessionChanges(projectId) |
Reads session + diff files | Changes tab |
The getSnapshots() function scans ~44,000+ part files across all projects. Never call it during initial page load.
Current pattern (see lazy-snapshot-browser.tsx):
- Server renders page WITHOUT snapshots
- Client fetches
/api/projects/{id}/snapshotsonly when "Snapshots" tab is selected - Results cached in memory for back/forward navigation
// BAD: Blocks page render
const snapshots = getSnapshots(id); // Scans 44k files!
// GOOD: Lazy load on client
<LazySnapshotBrowser projectId={id} /> // Fetches via API when neededWhen adding features that need project data:
- Ask: "Do I need the full
Projectobject or just thegitDir?" - If just gitDir: use
getProjectGitDir(id)+fs.existsSync()check - If full project: use
getProject(id)(now optimized for single load) - Never call
getProjects()except on the home page
Uses OpenCode's official color palette from their UI package:
- Background:
#131010(smoke-dark-1) - Text:
#f1ecec/#b7b1b1/#716c6b(strong/base/weak) - Green:
#37db2e(additions, success) - Red:
#ff917b(deletions, errors) - Blue:
#89b5ff(interactive, links)
All colors available as bg-oc-*, text-oc-*, border-oc-* Tailwind classes.
The search feature (/search) lets users find snapshots by searching conversation text.
How it works:
- First search builds an in-memory index by scanning all
storage/part/**/*.jsonfiles (~44k files) - Index is cached for the server's lifetime
- Subsequent searches filter the cached index (sub-second)
Performance:
- First search: ~13 seconds (index building)
- Subsequent searches: ~0.5 seconds
Don't:
- Call
searchPrompts()without caching expectations - Rebuild index on every request
npm run build # Type check + production build
npm run dev # Development serverBuild must pass before committing. No separate test suite currently.