Imported from skkut/SilkStack-Image-Browser (
AGENTS.md). Install upstream withnpx skills add skkut/SilkStack-Image-Browser. Copyright stays with the author.
Basic Instructions
SilkStack Image Browser is a desktop application (Electron + React + TypeScript) for browsing, searching, and organizing AI-generated images locally.
The development is done in Windows, so use Windows-specific tools and commands when invoking commands for development. Always ensure that the code is compatible with Windows, MacOS and Linux. Always check if code builds without errors, even for small changes. Use only tmp folder for temporary files.
When working on this codebase:
- Always make sure the build works (npm run build)
- Always read existing code before modifying
- Always ensure MPL compliance, warn for MPL violations
- Maintain backward compatibility with cached data
- Consider performance impact on large collections
- Keep privacy-first approach (all processing stays local; the only outbound calls allowed are the documented ones - anonymous usage ping, license activation, subscription re-validation, model downloads, checkpoint base-model lookups on civitai.com / huggingface.co, update checks - and any new one must be documented in README and here)
- Follow TypeScript best practices
- Write tests for new functionality
- Always keep
docs/CHANGELOG.md's## [Unreleased]section current — add a short, value-first entry as each user-facing feature or fix is completed (create the section above the newest release if absent).docs/CHANGELOG.mdis the only copy; the app does not bundle a changelog. - Do not auto commit to git
- Do not run deploy scripts automatically
- NEVER add the project folder (
C:\Projects\AI-Images-Browser) as a library folder in the app — including in dev mode and including "just to try something". Use onlyC:\Projects\TestImagesfor test data (see Test Data Folder Rule)
Code Style
- Use TypeScript strict mode
- Follow existing naming conventions
- Use functional React components with hooks
- Prefer explicit typing over
any - Keep components focused and single-responsibility
Key Dependencies & Technologies:
- Runtime & Build:
electron(^38.1.2),vite(^8.0.9),typescript(^5.2.2),eslint(^9.37.0) (Note: package scope is"type": "module") - Frontend Framework:
react&react-dom(^18.2.0) - State Management:
zustand(^5.0.8) - Styling & UI:
tailwindcss(^3.4.18),postcss(^8.5.6),autoprefixer(^10.4.21) - Icons & Animations:
lucide-react(^0.544.0),framer-motion(^12.23.24) - UI Components & Virtualization:
react-window(^1.8.10),react-virtualized-auto-sizer(^1.0.26),recharts(^3.4.1),react-zoom-pan-pinch(^3.6.1) - Testing:
vitest(^4.1.4),@testing-library/react(^16.3.0),playwright(^1.59.1) - Metadata & Files:
exifr(^7.1.3),pngjs(^7.0.0),cbor-js(^0.1.0),chokidar(^5.0.0) - Storage: IndexedDB for caching
Key NPM Scripts (from package.json):
npm run dev:app- Run the full Electron + React dev environmentnpm run dev- Run the browser-only dev servernpm run build- Compile TypeScript and build the React outputnpm test- Run tests via vitestnpm run lint- Run ESLintnpm run electron-dist- Build distributable without publishingnpm run auto-release <version>- Execute the complete automated CI/CD pipelinenpm run cli:parse/npm run cli:index- Execute CLI interface commandsnpm run prompt <file>- Extract a plain prompt from a workflow JSON or a media filenpm run comfy:parse/npm run comfy:batch- Parse ComfyUI workflows (one file / a directory)
See docs/CLI-TOOLS.md for every script, flag, and known gotcha.
Project Structure
/
├── electron/ # Electron application code
│ ├── main.mjs # Electron main process
│ ├── preload.js # Electron preload script
│ └── fileWatcher.mjs # File system watcher
├── scripts/ # Maintenance, release, and CLI scripts (cli.ts)
├── src/ # Main React application source
│ ├── components/ # React components
│ ├── services/ # Business logic services
│ ├── store/ # Zustand state management
│ ├── utils/ # Utility functions
│ ├── hooks/ # Custom React hooks
│ ├── App.tsx # Main React application component
│ ├── types.ts # TypeScript type definitions
│ └── __tests__/ # Test files
├── docs/ # Documentation and guides
└── public/ # Static assets
- README.md: README.md (User-facing documentation and features)
- ARCHITECTURE.md: docs/ARCHITECTURE.md (Technical architecture and design decisions)
- CHANGELOG.md: docs/CHANGELOG.md (Version history and changes)
- RELEASE-GUIDE.md: docs/RELEASE-GUIDE.md (Release workflow for maintainers)
- CLI-TOOLS.md: docs/CLI-TOOLS.md (Every command-line script — the single CLI reference)
Key Features to Maintain
- Privacy: All processing is local; the only outbound calls are the documented ones (anonymous usage ping, license activation, subscription re-validation, model downloads, checkpoint base-model lookups on civitai.com / huggingface.co, update checks)
- Performance: Optimized for 18,000+ images with smart caching
- Metadata Search: Full-text search across all metadata fields
- Multi-Format Support: Handle various AI generator formats
- File Operations: Rename, move, delete (desktop only)
- Video & GIF Support: Indexing, playback, and thumbnail support for MP4, WEBM, and GIF files
- Shadow Metadata: View original metadata and revert changes (non-destructive editing)
- Subfolder Management: Ability to exclude specific subfolders from indexing
- Auto-Watch: Real-time monitoring of output folders during generation
- Base Model Lookup: Each image's checkpoint is resolved to the architecture it was built on (SD 1.5, SDXL 1.0, Pony, Flux.1 D), cached in an offline table keyed by checkpoint
Smart Library & Auto-Tags
- Clustering & Stacks:
services/clusteringEngine.ts,services/workers/clusteringWorker.ts,components/SmartLibrary.tsx,components/StackCard.tsx,components/StackExpandedView.tsx - Auto-Tags (TF-IDF):
services/autoTaggingEngine.ts,services/workers/aiWorker.ts,components/TagsAndFavorites.tsx,components/ImageModal.tsx,components/ImagePreviewSidebar.tsx - Cluster Cache:
services/clusterCacheManager.ts(atomic writes, userData path resolution)
Base Model Lookup
Keyed by checkpoint, not image — a library of tens of thousands of images has only a few dozen distinct checkpoints, so the whole feature costs a handful of network requests once and then nothing. That is also why there is no IndexedDB store or per-image cache field for it.
- Identity:
electron/checkpointKeys.mjs— pure, dependency-free, imported by both the main process and the renderer. The two sides must derive the same key or every lookup misses forever. - Resolution engine:
electron/checkpointLookup.mjs— cascade of offline table → Civitai by hash → Civitai by name (exact match only) → HuggingFace. Rate limited, atomic table writes. - IPC:
get-base-model-table/resolve-base-models/clear-base-model-table/set-base-model-mappingsinelectron/main.mjs, mirrored inelectron/preload.jsandElectronAPIinsrc/types.ts— three places that must agree, with nothing verifying it. Every write handler then pushes the payload-freebase-model-table-changedto every window but the caller (whose own invoke already returned the new table). - Renderer:
src/services/baseModelTable.ts— holds the table in memory and notifies subscribers.baseModelForImageruns the resolver's three steps in the resolver's order — a manual mapping filed by name, then the image's own key, then a name the table has learned — through the shared helpers incheckpointKeys.mjs, so the column cannot disagree with whatresolveCheckpointsdecided. Those steps are what let a window that never runs a resolve (the popped-out viewer, the stack window) answer a hash-less image at all.useImageStoresubscribes to bumpbaseModelTableVersionso derived UI re-filters when an async resolution lands (a value that lives outside the image cannot be seen by a memo comparator — it must be passed as a prop, seeImageTableRow, or listed as a memo dependency, seeModelView's entry memo, which is what makes a checkpoint answered while the Models tab is open grow its card without a reload). - Per-window state: that table is module state in a renderer, so every window holds its own
copy. Any window that shows a base model has to adopt it —
hooks/useBaseModelResolution.ts→useBaseModelTable(load on mount + re-read onbase-model-table-changed). The main window gets it viauseBaseModelResolution; the popped-out image viewer, which is where the image modal actually lives in Electron (App.tsxmounts the in-app modal only withoutelectronAPI), calls the hook itself inImageModalWindow. Add a new window that renders generation details and it needs the hook too — skipping it shows no Base Model row and no error, becauseMetaRowrenders nothing for an empty value. - Switch: the on/off toggle, Resolve now and Clear cached table all live in Settings →
Base Models (
components/CheckpointMappingSection.tsx), not in the top bar — it is a network preference, and that screen is the one that lists the checkpoints it is answering for. The card is rendered only whenisBaseModelLookupAvailable(); the tab itself is gated the same way inSettingsModal.tsx, so a browser build has no dead switch. Resolution otherwise runs on its own fromhooks/useBaseModelResolution.tsin the main window once the library settles (delayed, and skipped mid-index); the toggle only decides whether it may touch the network. - Manual mappings: Settings → Base Models (
components/CheckpointMappingSection.tsx) lets the user assign a base model to any checkpoint the lookup cannot answer. They are stored as ordinary rows withsource: 'manual'— not in a side file — which is what makes them authoritative for free (isEntryFreshnever expires a truthybaseModel), surviveclearTable, and answer the images of the checkpoint they were filed against. Four invariants to preserve when touching this area:resolveCheckpointsmust never overwrite a manual row, and must re-read the table insidewithTableLockbefore writing — a mapping saved while a lookup is in flight exists only on disk, so writing the pre-fetch snapshot would erase it.- The scope of a mapping is the row the user clicked, and that is what the name/manual split in
buildNameIndexencodes. Filed by name, it answers every ref carrying that name — hash-bearing ones included — and is checked before the ref's own key, so a mapping can correct a checkpoint the lookup already answered. Filed under a hash it stays scoped to that file plus its hash-less images: two files sharing a name are not the same checkpoint. Looked-up entries keep the strict rule (learnedNameForis for hash-less refs only) — a name guess must never stand in for a hash. A mapped checkpoint therefore costs zero requests, which is the promise the screen makes.manualMappingFor/learnedNameFor/buildNameIndexlive incheckpointKeys.mjsand are called by both the engine and the renderer — never re-implement the rule on one side. clearTablekeeps manual rows and drops everything looked up. The renderer'sclearBaseModelTablemirrors that split in memory; letting the two disagree blanks the column.- The picker's values are a hand-copied snapshot of Civitai's own "Base model" filter
dropdown, trimmed by hand — grouped and ordered as Civitai presents it (by publisher), which
makes a refresh a mechanical copy-paste. This is deliberately not a live fetch: Civitai
publishes no endpoint that lists base models, and enumeration-by-sampling would keep only what
is currently popular (a 1,600-version sweep found 33 values and missed 14 that Civitai filters
on fine). Do not add one. To refresh: copy the dropdown, verify values with
GET /api/v1/models?baseModels=<value>(any items at all means it is recognised;Otheris the sole exception — real, but the filter rejects it), then delete everything named inEXCLUDED_BASE_MODELSincomponents/CheckpointMappingSection.tsx, which records what the user removed by hand on 2026-10-16 and why. That step is not optional bookkeeping: every excluded value is still a real, filterable Civitai value, so a plain paste restores all of them silently. A guard test incheckpointMappingSection.test.tsxfails if one reappears. Exactness is the whole point: a guessed or stale string splits the sidebar facet into two entries for one architecture, with half the images behind each.
Common Tasks
Adding Smart Library Features
- Update clustering or auto-tagging logic in
src/services/orsrc/utils/ - Update UI in
src/components/SmartLibrary.tsxor stack components - Extend store state in
src/store/useImageStore.tsif needed - Add/adjust worker logic in
src/services/workers/for background tasks - Add tests in
src/__tests__/when parser or tokenizer changes
Adding New Metadata Format Support
- Add parser in
src/services/parsers/ - Update type definitions in
src/types.ts - Add tests in
src/__tests__/ - Update CLI parser if applicable
- Document in
docs/CHANGELOG.md
Adding New UI Features
- Create component in
src/components/ - Add state management in
src/store/if needed - Update
src/App.tsxto integrate - Consider Electron/browser compatibility
- Test with large collections
Fixing Performance Issues
- Check IndexedDB caching logic
- Review virtual scrolling implementation
- Profile with large image collections
- Consider lazy loading and background processing
Testing Strategy
- Unit tests for parsers and utilities
- Component tests for React components
- Integration tests for metadata extraction
- Manual testing with various AI generator outputs
- Performance testing with 10,000+ images
Test Data Folder Rule (strict)
Never, ever add C:\Projects\AI-Images-Browser itself as a library folder in the running app — not in dev mode, not in a smoke test, not "just to check one thing". The only folder to add when testing is C:\Projects\TestImages.
Why this matters:
- The app persists the last-used library folder in its profile, so a folder added once during a test sticks around and is re-indexed and file-watched on every later dev launch.
- Indexing the project root pulls in
node_modules/,dist/,.git/,tmp/and the full source tree — tens of thousands of non-image files. That destroys dev-app performance (indexing, watcher, metadata cache) and buries real test results in noise. tmp/being inside the project compounds this: temporary files created there become index entries too.
Rules for testing:
- Add only
C:\Projects\TestImagesas a library folder. - Create throwaway images for a test run in a per-run subfolder, e.g.
C:\Projects\TestImages\tmp-<purpose>\, and delete them when done. - For automated / smoke-test launches, point Electron at a throwaway profile (
--user-data-dir) so the test can never write a folder into the real dev profile in the first place. - Same rule for CLI runs: never pass the project root to
npm run cli:index,npm run comfy:batch, or similar. - If you find the project folder already added while testing, remove it from the app's folders before continuing.
Browser vs Desktop Considerations
All features are desktop-only (Electron):
- File system operations (rename, move, delete, show in folder)
- Command-line arguments
- Auto-updater
- Native file dialogs
Browser version uses File System Access API with limited capabilities.
Performance Tips
- Always test with large image collections (10,000+ images)
- Use React.memo() for expensive components
- Implement proper virtualization for lists
- Cache metadata aggressively in IndexedDB
- Process files in background threads when possible
- Use synchronous I/O (
fs.openSync) for header reads during indexing to prevent disk contention (Phase B optimization)
