Flashcards
Spaced-repetition flashcards for learning and retention, shipped as a plugin with its own sidebar tile — create packs, review cards on an FSRS schedule, import from Anki, and watch streaks and stats build up, all stored locally.
What it is
Spaced-repetition flashcards for learning and retention. Flashcards is now a first-party builtin-source plugin in Omniscio: the UI ships as a self-contained webview under src/plugins/flashcards/ and reaches core services through a typed bridge (window.AgentMC.flashcards). Backend (FSRS scheduler, Anki .apkg importer, media store, task-system integration, migrations, tables, CLI routes) STAYS in core so the data model + queries are shared with any future in-core consumer (e.g. an LMS meeting-intel handshake).
The user opens the plugin from its sidebar tile ("Flashcards"), which shows the pack list, a stats dashboard, pack detail with per-card review, and an Anki import wizard. Each pack holds cards; each card has a front + back (+ optional pasted images), a scheduling blob, and provenance (manual, anki-imported, or ai-generated).
Where to find it
Flashcards has its own tile in the sidebar, Flashcards. It is a first-party plugin, so it installs and updates the way any other plugin does, and the panel itself is a self-contained view with a pack list, a stats dashboard, pack detail for reviewing individual cards, and an Anki import wizard.
Creating a pack in the UI
"New pack" (pack list) and "Create your first pack" (empty state) open the card editor with no pack selected; the editor then shows a Pack name field and creates the pack together with its first card on Save, landing on the new pack's detail view. This is the only place the UI creates a pack (the bridge's packCreate is otherwise reached only by tests and the CLI).
The stats dashboard
The pack list header carries a Stats button that sets subView: 'dashboard-tile'; DashboardTile renders retention %, a 30-day review heatmap, a 7-day forgetting-curve forecast, the streak and the due-today count, and its own header renders the way back to the pack list on every branch (loading / error / empty / full). Reachable since 2026-09-18 — until then App.tsx routed the subView and nothing ever set it, so the whole surface was dead UI. The same sweep removed 'onboarding-stage' from the union: it was routed to ReviewMode, which returns null for any subView other than 'review', so it could only ever have rendered a blank screen (ReviewMode already delegates to OnboardingStage on its own, from settings.sessionsCompleted < 3). Guard: tests/unit/lint/flashcards-subview-reachable.test.ts fails whenever a SubView member has no navigation target.
How it behaves
Reviews are scheduled by an FSRS spaced-repetition scheduler that runs inside the app, and everything lives in your local database — no account, no cloud, no cost. A pack's counts are worked out when the page is read rather than stored, so they are never stale; media attached to a card is held in a local store and survives a re-import.
For agents
Where the code lives
src/plugins/flashcards/manifest.json- plugin idflashcards, categoryproductivity, sidebar title "Flashcards", entry pointui/index.html.src/plugins/flashcards/bridge/flashcards-bridge.ts- 28-method switch dispatching to services (single-object args pattern; emitsIPC.FLASHCARDS_PUSHafter each mutation).src/plugins/flashcards/web/src/- the React webview:App.tsx(subView router),components/(PackListView, PackDetailView, CardEditor, ReviewMode, GradeButtons, OnboardingStage, MediaRefStrip, PackStatsPanel, DashboardTile, ImportWizard, FlashcardsErrorBoundary, ReviewComplete, FlashcardsEmptyState),store/(Zustand pack + review slices),lib/(flashcards-api bridge SDK, i18n shim, toast bus, telemetry stub, parse-cloze, is-fresh helpers),ui/(self-contained primitives: Button, AsyncButton, Modal, DialogShell, Toaster, Pill, EmptyState, Spinner, CenteredSpinner, ListItem, IconButton, ImageCanvas, Kbd, FormField, SegmentedControl, SearchInput, LoadingPanel, MenuShell, ConfirmDialog).src/main/services/flashcards/- core services:flashcards-service.ts(CRUD + review + spawn context builder + CLI-server-mediated spawn),fsrs-scheduler.ts(ts-fsrs 5.4.1 wrapping),media-store.ts(content-addressed image blob store),importer/(Anki .apkg parse + commit via yauzl + fzstd),pack-group-task.ts(task-system integration),pick-apkg-file.ts(OS-native file picker).src/main/db/queries-flashcards.ts+queries-flashcards-session.ts- SQL surface for pack + card + review + trickle-queue + settings + pack-session tables.src/main/db/migrations/- four additive migrations:20260824-flashcards-tables.ts,20260825-flashcards-trickle-queue.ts,20260826-flashcards-media-table.ts,20260826-flashcards-retrievability.ts.src/main/services/cli/cli-server-flashcards-routes.ts- 28 CLI routes under/flashcards/*(route-list identical to the bridge methods).src/shared/flashcards/- types + Zod schemas + parse-cloze + telemetry-event catalog shared by bridge, service, tests.src/shared/push-event-schemas/flashcards.ts- 9-kind discriminated union forIPC.FLASHCARDS_PUSH(pack.list.changed, pack.detail.changed, card.list.changed, review.session.updated, card.review.recorded, import.started, import.progress, import.completed, import.failed).src/shared/ipc-channels/flashcards.ts- reduced to the ONE push channel; the 28 request/response channels are retired (bridge is the transport).tests/unit/plugins/flashcards/bridge/- bridge dispatch, media round-trip, session-spawn cost preservation tests.
Bridge shape (window.AgentMC.flashcards)
Every method resolves with UNWRAPPED data and rejects with an Error when the main side reports a failure. Arguments are single-object payloads (matching the Wave 1 Zod schemas). Full list (28 methods):
- Pack CRUD -
packList(),packGet({packId}),packCreate({name, source?}),packRename({packId, name}),packDelete({packId}). - Card CRUD -
cardList({packId?, limit?, offset?}),cardCreate({packId, front, back, cardType, frontMediaRefs?, backMediaRefs?}),cardUpdate({cardId, ...}),cardDelete({cardId}). - Review -
reviewStart({packId: string | null}),gradeCard({sessionId, cardId, grade, latencyMs, reviewMode, interleaved}),reviewEnd({sessionId}). - Settings + stats -
settingsGet(),settingsUpdate({...}),stats(). - Import (Anki .apkg) -
pickApkgFile(),importParse({apkgFilePath}),importCommit({importId, packName}),importCancel({importId}). - Sessions (spawn) -
sessionStartOnCard({cardId, reason?, ...}),sessionStartOnPack({packId}),packSessionAttribution({packId}),trickleEnqueue({...}),trickleDismiss({...}). - Media -
mediaUpload({bytes: Uint8Array, originalName?}),mediaGet({mediaId}),mediaDelete({mediaId}),mediaList().
CLI parity
CLI routes at /flashcards/* are preserved; the plugin bridge and CLI parity share the same 28-method surface. Read routes gate through cliReadPreamble; mutations gate through cliMutationPreamble (bearer auth + rate limit). The flashcards-panel unreleased-feature flag was retired with the plugin conversion - plugin install + enable is now the user-facing gate, and CLI routes are always registered so plugin-external agents can drive Flashcards data. pickApkgFile retains its CLI-parity exemption (OS-native dialog is not headless-callable).
Cost preservation invariant
Session-spawn methods (sessionStartOnCard, sessionStartOnPack) MUST route through the local CLI server's /project/:name/new with bearer auth + the X-AMC-Source-Session-Id provenance header. The bridge dispatches these to spawnClaudeSessionForCard / startSessionOnPack in src/main/services/flashcards/; it does NOT reimplement the spawn logic. A AMC_TEST_MOCK_CLI_POST=1 escape hatch in spawnClaudeSessionForCard returns a canned response so integration tests never fire a real (billable) session-spawn. Automated tests MUST use that escape hatch - invoking these methods live burns real Claude tokens.
Media Buffer round-trip
mediaUpload({bytes, originalName}) stores content-addressed image blobs under <userData>/flashcards-media/, returns {mediaId, contentHash, sizeBytes, mime}. mediaGet({mediaId}) returns {bytes, mime, sizeBytes} - the bytes may arrive as a Node Buffer or a {type:'Buffer', data:[...]}-shaped structured-clone; the plugin store's normalizeToUint8Array() helper accepts either shape so a new Blob([bytes]) render path stays safe. Verified by tests/unit/plugins/flashcards/bridge/flashcards-bridge.media.test.ts.
Push events
Server-side emits emitPush(IPC.FLASHCARDS_PUSH, {kind: '...', ...}) from the bridge (after each mutation) and from import-service.ts (import.started / progress / completed / failed). The host forwards that channel onto the plugin's OWN event bus as flashcards:push (src/main/app/startup/register-push-routing.ts → dispatchPluginEvent('flashcards', …), plugin-scoped), because a sandboxed plugin webview only hears window.AgentMC.events.on, never the renderer push channels. Island side, lib/plugin-events.ts is the one subscription chokepoint: subscribeToFlashcardsPush (store) drops the pack-list TTL guard and refetches on any list-changing kind (pack/card changes, a recorded review, a completed import), and usePushListener (used by ImportWizard for progress) subscribes a component with optional debounce. Wired 2026-09-15 — before that both were no-op stubs, so a pack created through the bridge or the CLI never appeared without a reload.
Pack counts are computed on read (not stored)
flashcard_packs.card_count / due_count are legacy columns: every writer (createPack and the Anki importer) inserts 0 and nothing maintains them — due_count cannot be maintained at all, since it changes as the clock passes with no write. PACK_READ_COLUMNS (queries-flashcards.ts) computes both from the card rows with the same predicate the review queue uses (dueAt <= now, not soft-deleted), so pack.dueCount > 0 means exactly "Start review can start a session now". A lint guard (tests/unit/lint/flashcards-pack-counts-computed.test.ts) bans any raw SELECT of the stored columns. Fixed 2026-09-15: until then the raw zeros were served and "Start review" was disabled forever on every pack (imports only worked because the wizard calls startReview directly).
Deps
ts-fsrs@5.4.1 (FSRS scheduling), yauzl (.apkg zip reading), fzstd (Zstd decompression), @types/yauzl (typings) live in the ROOT package.json and are used by main-process code only. The plugin webview never imports these - its own package.json under src/plugins/flashcards/web/ pulls react, zustand, lucide-react, zod, tailwindcss.
Related
Flashcards keeps its own data and does not read your sessions; for the notes-and-memory side of Omniscio see Global memory.
Last verified 2026-09-23