Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

LMS (Courses)

Omniscio's learning-management surface, shipped as an in-repo plugin: a dashboard for whoever runs a course, editors for courses and lessons including an AI outline generator, a learner home and lesson viewer with quizzes, progress tracking with a certificate, and an importer for an existing Docebo catalogue.

What it is

The LMS surface — Screen 1 (HR-manager dashboard), Screen 2 (course editor + outline generator), Screen 3 (lesson editor: doc + quiz), Screen 4 (learner home), Screen 5 (learner lesson viewer: doc + quiz), Screen 6 (learner progress + certificate), and Screen 8 (Docebo import wizard) — ships as an in-repo plugin webview. Wave 1.

Where to find it

It ships as an in-repo plugin, so it appears inside the app as its own surface rather than as a separate website. Its screens run from the manager dashboard and the course and lesson editors through to the learner home, lesson viewer and progress view. It is gated behind a feature switch, so it stays hidden until it is turned on.

How it behaves

Feature gate

isLmsFeatureVisible() in src/main/services/lms/lms-feature-gate.ts is the sole gate. It returns true when the LMS plugin is installed AND enabled — no separate feature flag. Retired: the pre-plugin lms-panel UnreleasedFeatureId (Decision 4, split-map.md).

Deep links

omniscio://lms/* is parsed in main and forwarded to the plugin webview via the lms:route push kind. The plugin's lms-route-resolver.ts maps the parsed path to a useLmsCoursesStore.view kind (list, course-editor, lesson-editor, learner-home, learner-lesson, learner-progress, manager-dashboard, manager-per-course-table, import-wizard).

Known gaps (Wave 2)

  • Active-account credential provider: the plugin's getCreds returns null in Wave 1 so generateOutline + doceboAiReview degrade to the "no api key" empty state. Wave 2 wires a real credential provider.
  • Docebo OAuth: opens the consent page via window.open (Wave 1 fallback). The wizard prompts the user to paste the redirect code back. Wave 2 wires a bridge-level openExternalUrl + auto-capture.
  • LinkedIn share button: uses window.open for the LinkedIn Add-to-Profile URL. Wave 2 wires the guarded external-URL bridge.
  • Push subscribers: not wired; wizard polls instead. Wave 2.
  • i18n keys: the plugin ships English-only via the identity-passthrough rendererT shim (src/plugins/lms/web/src/lib/i18n.ts). Every user-facing string is authored with a key + English fallback so Wave 2 can drop in a real catalog without touching call sites.
  • Certificate URL: the PDF lives on-disk in the user's userData folder (not a public URL LinkedIn can crawl); the LinkedIn share URL therefore omits certUrl. Wave 2 with hosted cert URLs adds it.
  • Alert-actions barrel: LMS_DOCEBO_ALERT_ACTIONS is exported but not registered in ALL_ALERT_ACTION_MODULES (matches ref bug). Wave 2 or a follow-up wires it.

Notable adaptations from the ref (do NOT re-open without splitting the design)

Locked at GATE 1 (Session 1); details in _kb/kickoffs/lms/split-map.md:

  1. Reading A — author the plugin webview fresh from the ref, not port-then-delete. src/renderer/src/features/lms/ never existed on this branch.
  2. Push wiring approach 1 — plugin-scoped IPC channels via webContents.send (matches plugin-session-status-change), not IPC channel additions.
  3. UI primitives — plugin owns its own primitives under src/plugins/lms/web/src/ui/** (Writer's 8 base + 4 LMS-specific + Session 2's 4 additions + Session 3's Modal/DialogShell extras). The plugin webview cannot import from the renderer kit.
  4. DirectionalIcon dropped across every ported component — the plugin ships English-only, so raw lucide icons work.
  5. data-ui-anchor props dropped across every ported component — the plugin webview is an isolated React tree the anchor registry cannot see across.

For agents

Where the code lives

  • Plugin webview (renderer): src/plugins/lms/web/src/ (React 19, Zustand, Tailwind, dnd-kit for drag-reorder, react-markdown for lesson bodies, @tanstack/react-virtual for large lists).
  • Plugin bundle: src/plugins/lms/manifest.json + src/plugins/lms/ui/* (built with pnpm build in web/; tracked committed artifacts).
  • Plugin bridge (main): src/plugins/lms/bridge/lms-bridge.ts — one dispatcher exposing 33 methods (course CRUD, module/lesson CRUD, enrollment + completion, manager rollup, Docebo import, AI outline, certificate).
  • Backend services (main): src/main/services/lms/** — course service, learner rollup, certificate PDF generation, Docebo mapper + import orchestrator, AI outline via the generateOutline service.
  • CLI routes (agent-facing): src/main/services/cli/cli-server-lms-routes.ts — 33 routes under /lms/*, each gated on isLmsFeatureVisible() + X-AMC-Source-Session-Id.
  • Shared schemas (Zod): src/shared/lms/** (13 row shapes) + src/main/ipc/bridge-method-schemas.ts LMS_SCHEMAS (bridge input validation) + src/shared/plugin-bridge-response-schemas.ts lms.* entries (response markers).
  • Shared IPC push kinds: src/shared/ipc-channels/lms.ts — only 5 push kinds (4 import lifecycle + 1 deep-link route). Every request/response goes through the plugin bridge, NOT the shared channel barrel.

Data invariants

  • A lesson is completed ONCE per enrollment. lesson_completions carries a UNIQUE index on (enrollment_id, lesson_id) (uniq_lesson_completions_enrollment_lesson, added by 20260918203040-lms-lesson-completion-uniqueness.ts), and LmsService.createLessonCompletion returns the completion already on record instead of inserting a second row.
  • Progress counts are per LESSON, never per row. Both sides must collapse completions to a set of lesson ids before counting — the learner surfaces do it in lms-learner-utils.ts, the manager math in lms-manager-aggregation.ts (latestCompletionPerLesson). Counting raw completion rows let one lesson completed twice report 100% on a two-lesson course.

How the plugin talks to the backend

The plugin webview cannot import from src/main/** or src/renderer/** — it's an isolated React tree. It reaches the backend through the plugin bridge:

plugin webview  →  window.plugin.lms.<method>(input)
                    │
                    │  (preload validates response with PLUGIN_BRIDGE_RESPONSE_SCHEMAS['lms.<method>'])
                    ▼
                  handleLmsBridgeCall  (src/plugins/lms/bridge/lms-bridge.ts)
                    │
                    │  (input validated by LMS_SCHEMAS in bridge-method-schemas.ts)
                    ▼
                  LmsService / DoceboImportOrchestrator / etc.

The plugin's lmsApi (src/plugins/lms/web/src/lib/lms-api.ts) wraps the 33 bridge calls into typed methods. Consumer-only Zustand stores under src/plugins/lms/web/src/store/ hold state; every mutation goes through lmsApi (UFC state-lifecycle-integrity).

Push events (backend-broadcast)

Five push kinds survive on the shared IPC barrel (src/shared/ipc-channels/lms.ts LMS_CHANNELS), broadcast via emitPush() on the main side:

  • lms:import-started / -progress / -complete / -failed — Docebo import lifecycle
  • lms:route — deep-link forwarding (omniscio://lms/* parsed in main → pushed to the plugin webview)

Wave 1 note: the plugin webview does not yet subscribe to these push kinds. The Docebo import wizard polls getImportStatus(jobId) every 2 seconds while a job is running instead. Wave 2 wires the plugin's push subscribers through the preload bridge.

Related

Plugins in general — how they are installed, reviewed and enabled — are described on the Plugin marketplace page.

Last verified 2026-09-23