dsh-math-input

September 1, 2026 · View on GitHub

English | 中文

A zero-token, fully offline math input plugin for DeepSeek Harness (DSH). It adds three input methods — handwriting recognition, screenshot OCR, and a LaTeX editor — plus inline LaTeX rendering inside the DSH composer. All recognition runs in the browser; no API key, no server round-trip, no token consumption.

Verification status (2026-08-31): every platform claim in this document was checked against first-party sources — deepseek-harness official docs (docs/architecture.md, docs/subsystems/slots.md, docs/subsystems/typert.md, docs/subsystems/web-client.md, docs/subsystems/client-modules.md, docs/user/develop/basic/*) and the complete source of DIAG5/dsh-better-input v0.1.8. Items still to resolve during implementation are collected in Open Questions.

Design Goals

GoalHow it is met
Zero token costRecognition runs entirely client-side via ONNX Runtime Web (WASM/WebGPU). The plugin never calls ctx.llm.
Fully offline after first model downloadThe CoMER model (7.2 MB total) is fetched once from GitHub Releases, then cached in IndexedDB. Subsequent loads are instant and require no network.
Math-focused, not Chinese textThe recognition engine (CoMER, trained on CROHME handwritten math) targets LaTeX symbols and expressions. Latin letters and math operators, not Chinese characters.
Consistent with dsh-better-inputMirrors the verified Client/Host/Typert/Remote structure, slot conventions, locale pattern, and build pipeline of dsh-better-input v0.1.8, with a thin Host because no LLM calls are needed.

Platform Integration (verified)

Bundles, profiles, and layers

A running dsh is a Cordis plugin tree composed from ordered layers: each bundle in the profile, then the profile's cordis.patch.yml, then the home-level one, then any --patch overlay. dsh plugin --profile web add dsh-math-input installs this package into the profile home; the browser half is discovered through the package's dsh.client manifest, not through the Host entry.

Our cordis.patch.yml is therefore minimal (identical shape to dsh-better-input and dsh-skills-nexus):

- insert:
    - id: dsh-math-input
      name: dsh-math-input

package.json declares dsh.bundle.patch: "./cordis.patch.yml" plus dsh.client (see Client runtime).

Host runtime

  • Plugin entry exports name and apply(ctx) (function form). Dependencies declared in inject are ready before apply runs; everything registered through ctx is disposed automatically; explicit cleanup uses ctx.effect(() => disposer).

  • The Host registers one service: MathInputSettingsService extends TypertRemoteService (from @deepseek-ai/dsh-typert-protocol), mounted via await ctx.plugin(MathInputSettingsService) and constructed as super(ctx, 'MathInput', { namespace: 'mathInput' }). Each public method of the service becomes a Remote invocation.

  • Settings persist through @deepseek-ai/dsh-settings: ctx.settings.register(settingsNamespace('dsh-math-input'), MathInputSettingsSchema, { validate }), where the schema is a Schemastery object with per-field defaults. The Host owns validation (validateSettings) and the flat stored shape.

  • Typert gateway boundary rules (from dsh-better-input's hard-won comments): never assign explicit undefined in a returned object — omit optional keys instead; cancellation arrives as a trailing signal: AbortSignal parameter declared in the descriptor.

  • No ctx.llm anywhere — zero token cost is architectural, not behavioral.

Typert / Remote contract

Third-party plugins hand-write the three contract files (the harness's Typert codegen is internal to its own build):

FileContent
src/remote-contract.tsZod wire schemas + z.infer wire types for every parameter/result
src/typert.tsTYPERT manifest: { package, face: 'host', schemas: [], invocations: [...], model: { services, events, objects } } — one descriptor per method
src/remote.tsTYPERT_REMOTE: TypertRemoteContribution (client-facing descriptors) + declare module '@deepseek-ai/dsh-typert-protocol' augmentations

Descriptor shape per method: id: 'dsh-math-input#mathInput/<method>', service: 'MathInput', namespace: 'mathInput', invocation: { kind: 'direct' }, ordered parameters ({ name, wire, source: 'json', codec: { mode: 'strict', typeSymbol, schema } }), optional cancellation: { parameter: 'signal' }, and a strict result codec.

For this thin plugin the contract exposes only settings I/O:

  • mathInput/getSettings() -> MathInputSettingsView

  • mathInput/updateSettings(patch, signal?) -> MathInputSettingsView

The Client mounts the contribution once with await ctx.remote.$mount(TYPERT_REMOTE) and calls remote.getSettings() etc., which resolve to RemoteResult<T> ({ ok: true, value } | { ok: false, error }).

Client runtime

  • package.json declares dsh.client: { platform: 'web', inject: [...] } listing the framework packages whose factories must arrive before ours materialize (mirrors dsh-better-input): @deepseek-ai/dsh-client-runtime, @deepseek-ai/dsh-client-ui-conversation, @deepseek-ai/dsh-client-ui-slots. The client bundle is exported at exports['./client'].

  • The client entry (src/client.tssrc/client/index.ts) exports inject: ['slots', 'remote', 'locale'] and async apply(ctx: ClientContext): Promise<() => Promise<void>>:

    1. const disposeRemote = await ctx.remote.$mount(TYPERT_REMOTE) — mounts remote.mathInput.
    2. ctx.locale.register(MATH_INPUT_NS, { zh, en }) — bilingual dictionary registered before any slot renders.
    3. await ctx.inject(['slots', 'remote', 'remote.mathInput', 'locale'], async (remoteCtx) => { ... slot registrations ... }) — the inner inject requests remote.mathInput only after the mount (requesting it in the outer inject deadlocks, since it gates our own activation).
    4. Returns a dispose function that disposes the locale dictionaries and the remote mount.
  • Components are React 18 function components. They never receive ctx; everything arrives as props.

  • CSS: inline styles keyed off DSH design tokens (var(--dsw-alias-*)); plugin keyframes injected via a document.head style tag carrying dataset.plugin = 'dsh-math-input', removed on dispose.

Slot map (verified against docs/subsystems/slots.md hierarchy)

Our componentSlotCardinality/scopePlacement notes
Launcher "+" buttonconversation.input.leftlist / sessionLeft of the composer input row. Fresh list id, low order.
Handwriting windowReact portal → document.bodyn/aNo modal slot exists; modal windows are portals (dsh-better-input uses portals for floating UI).
Screenshot windowReact portal → document.bodyn/aSame portal pattern.
LaTeX editor panelconversation.input.docklist / sessionToggleable dock line above the composer (dsh-better-input occupies orders 15/20 here; we take a fresh id).
Settings pagesettings.sectionlist / rootlabel thunk via ctx.locale.bind(NS)('settingsTitle') so the sidebar row follows locale switches.
Inline LaTeX rendercomposer draft (see below)n/aThe composer draft is a plain string; see Component 6 and Open Questions.

Slot registration pattern (exactly dsh-better-input's):

remoteCtx.slots.inject('conversation.input.dock', () =>
  remoteCtx.slots.register(
    { name: 'conversation.input.dock', id: 'math-input-latex-dock', order: 30, locale: MATH_INPUT_NS,
      inject: (sessionId) => ({ /* private face: controllers, callbacks */ }) },
    LatexEditorDock
  )
)

Composer access: components registered on session-scoped conversation slots receive framework props input: { draft: string } and inputActions: { setDraft(text: string): void } (plus session, t, and our injected face). All insertion paths funnel through setDraft: appending \[latex\] to the current draft is the entire write surface we need.

Project Structure

dsh-math-input/
├── src/
│   ├── index.ts                  # Host entry: name + apply(ctx) -> ctx.plugin(MathInputSettingsService)
│   ├── config.ts                 # MathInputSettings interface, DEFAULT_SETTINGS, validators
│   ├── config-schema.ts          # Schemastery settings schema (Host-only, kept out of browser bundle)
│   ├── remote-contract.ts        # Zod wire schemas + wire types (shared)
│   ├── typert.ts                 # TYPERT manifest (host face)
│   ├── remote.ts                 # TYPERT_REMOTE contribution + module augmentations
│   ├── about.ts                  # installed package.json identity + update check (optional, pattern: dsh-better-input)
│   ├── client.ts                 # Client entry: re-exports src/client/index.ts
│   ├── client/
│   │   ├── index.ts              # client apply(): remote.$mount, locale.register, slots.inject registrations
│   │   ├── strings.ts            # typed zh/en dictionary for the 'math-input' locale namespace
│   │   ├── settings-controller.ts# external store over remote.getSettings/updateSettings + useSyncExternalStore hooks
│   │   ├── settings.tsx          # settings.section component (draft-on-change, save-on-blur)
│   │   ├── launcher.tsx          # "+" button in conversation.input.left + popup menu
│   │   ├── handwriting-pad.tsx   # portal modal: canvas, toolbar, recognition flow
│   │   ├── screenshot-ocr.tsx    # portal modal: paste/upload/capture -> recognition
│   │   ├── latex-editor.tsx      # conversation.input.dock panel: editor + palette + KaTeX preview
│   │   ├── inline-renderer.ts    # \[...\] detection in draft + rendered-block strategy (see Component 6)
│   │   └── ui/                   # shared modal frame, KaTeX preview pane, button styles
│   ├── recognition/
│   │   ├── engine.ts             # ink-on InferenceEngine singleton (lazy load + IndexedDB cache + Web Worker)
│   │   ├── preprocess.ts         # stroke preprocessing (reuses ink-on preprocessStrokes)
│   │   └── image-preprocess.ts   # screenshot image -> encoder tensor (grayscale / invert / scale to 256h)
│   └── latex/
│       ├── render.ts             # KaTeX rendering + \[...\] / $$...$$ detection
│       └── repair.ts             # brace balancing / argument fixing (reuses ink-on repairLatex)
├── test/                         # unit tests (node:test + tsx), pure-logic modules only
│   ├── latex-detect.test.ts
│   ├── latex-repair.test.ts
│   ├── preprocess.test.ts
│   ├── image-preprocess.test.ts
│   └── engine-mock.test.ts
├── docs/                         # ARCHITECTURE (en/zh), recognition-engine, local-testing
├── .github/workflows/ci.yml      # Node 20/22/24 matrix
├── lib/                          # build output (committed; CI checks it matches a fresh build)
├── cordis.patch.yml              # `- insert: [{ id, name }]`
├── tsdown.config.ts              # Host ESM/node entries: index, typert, remote (+ client bundle config)
├── tsdown.client.ts              # Client CJS/browser bundle wrapped in window.__ModuleLoader__.load(...)
├── tsconfig.json                 # strict, jsx react-jsx, noEmit (typecheck)
├── tsconfig.build.json           # declaration-only emit to lib/
├── eslint.config.js              # ESLint 9 flat config
├── package.json                  # dsh.bundle.patch + dsh.client + exports '.'/'./client'/'./typert'/'./remote'
├── README.md / README_CN.md
├── CONTRIBUTING.md / CONTRIBUTING.zh-CN.md
└── CHANGELOG.md

Client / Host Architecture

Host side (src/index.ts + settings service): registers MathInputSettingsService (a TypertRemoteService), which owns the dsh-math-input settings namespace and exposes getSettings / updateSettings. No LLM calls, no inject beyond settings. This is the entire Host surface — recognition, rendering, and UI are all Client-resident.

Client side (src/client/): mounts the remote contribution, registers the locale dictionary, then registers slot occupants (launcher, dock panel, settings section) plus portal-rendered modal windows and the inline renderer. Runs the ONNX engine in a Web Worker.

Contract (typert.ts / remote.ts / remote-contract.ts): the minimal settings-only Typert surface described above. Compared with dsh-better-input (nine invocations routing LLM calls through the Host), ours has two.

Recognition Engine

The plugin uses ink-on, a framework-agnostic browser library for handwritten math expression recognition.

PropertyValue
ModelCoMER (Coverage-guided Multi-scale Encoder-decoder Transformer, ECCV 2022)
RuntimeONNX Runtime Web (WASM, optional WebGPU)
Model sizeEncoder 3.4 MB + Decoder 4.0 MB = 7.2 MB total (INT8 quantized)
Inference threadWeb Worker (off main thread, UI stays responsive)
Latency1–2 seconds per recognition
CachingIndexedDB — first download fetches 7.2 MB, subsequent loads are instant
Dependenciesonnxruntime-web (bundled into the client bundle), no Vue required

The engine exposes:

  • InferenceEngine — loads ONNX sessions, runs encoder + decoder with beam search.

  • preprocessStrokes(strokes) — resamples points at 3px intervals, renders Bezier curves on a white-on-black canvas, scales to height 256, converts to a grayscale Float32 tensor.

  • repairLatex(tokens) — fixes unbalanced braces and broken \frac/\sqrt arguments, validated by KaTeX.

  • isStrokeMeaningful(strokes) — filters accidental taps and dots.

  • loadVocab(url) — loads the 245-symbol token vocabulary.

Screenshot OCR reuse: the same CoMER encoder accepts a preprocessed image tensor. A separate image-preprocess.ts converts a screenshot/pasted image to the same format (grayscale, inverted to white-on-black, scaled to height 256, 64px-aligned padding). This avoids shipping a second model. CoMER is trained on handwritten data (CROHME), so accuracy on printed formulas may be lower; if real-world testing shows insufficient results, a pix2tex ONNX export can be slotted in behind the same recognize(input) interface.

Six Components

1. Inline LaTeX Renderer (inline-renderer.ts)

Watches the composer draft for \[ ... \] closure pairs.

  • State machine: source (editable text) <-> rendered (KaTeX block).

  • Unclosed \[ stays as plain text (no premature rendering).

  • On send: rendered blocks expand back to \[latex\] plain text so the model reads the LaTeX source.

  • The delimiter is \[ ... \] (pure LaTeX display math, no $ ambiguity); $$ ... $$ is detected too for pasted content, but plugin-produced output uses \[...\].

Framework constraint (verified): the composer draft is a plain string exposed through input.draft / inputActions.setDraft; the shipped slot system provides no hook to replace a text range with a React node inside the composer. v1 therefore renders a KaTeX preview strip (our own conversation.input.dock occupant showing live-rendered blocks for every closed pair in the draft; click a block to edit its source) instead of mutating composer DOM. True in-place rendering requires probing the composer DOM structure — tracked in Open Questions, to be investigated during implementation.

2. Input Method Launcher (launcher.tsx)

A "+" button registered on conversation.input.left (verified: this slot exists in the shipped hierarchy, left of the input row). Clicking opens a popup menu (absolutely-positioned panel anchored to the button):

Menu itemOpens
Handwriting inputComponent 3 (portal modal)
Screenshot and recognizeComponent 4 (portal modal)
LaTeX syntax editorComponent 5 (dock panel, toggled)

3. Handwriting Window (handwriting-pad.tsx)

A React portal modal (fixed overlay on document.body), modeled on the AxMath writing pad.

Toolbar (left to right): settings, mode switch (auto / number / expression), eraser, clear, undo, submit-recognize, confirm, cancel.

Canvas: Pointer Events capture strokes (mouse, touch, and stylus unified). Strokes are stored as Stroke[] with Bezier-smoothed curves.

Recognition flow: a configurable debounce (default 1.5 s) after the last stroke -> isStrokeMeaningful filters noise -> preprocessStrokes normalizes -> Web Worker runs CoMER encoder + decoder -> repairLatex fixes common errors + KaTeX validation -> first valid candidate selected.

Result area: KaTeX live preview + editable LaTeX source. Confirm composes \[latex\] into the draft via inputActions.setDraft and closes the modal; the inline renderer then picks up the closed pair.

4. Screenshot OCR Window (screenshot-ocr.tsx)

A portal modal supporting three image sources: paste (Ctrl+V), file upload, and browser capture. After acquisition:

Image -> image-preprocess (grayscale, invert to white-on-black, scale to 256h, 64px-aligned padding) -> same CoMER encoder + decoder -> LaTeX -> KaTeX preview + editable -> confirm inserts \[latex\] via setDraft.

5. LaTeX Editor (latex-editor.tsx)

A conversation.input.dock occupant toggled by the launcher (collapsed by default; the dock slot keeps it co-present with the composer, same placement strategy as dsh-better-input's docks). Left: code editor area. Right: live KaTeX preview. Bottom: symbol palette modeled on the AxMath bottom toolbar.

  • Greek letter row: alpha, beta, gamma, delta, theta, lambda, mu, pi, sigma, phi, omega, etc.

  • Structure template row: \frac{}{}, \sqrt{}, \sum_{}^{}, \int_{}^{}, x^{}, x_{}, matrix templates.

  • Clicking a palette item inserts the corresponding LaTeX fragment at the cursor.

Confirm inserts \[latex\] into the draft via setDraft.

6. Settings Page (settings.tsx)

A settings.section occupant (list cardinality, root scope) with a locale-bound sidebar label. Props: { close, t, settingsController }. Configurable options:

SettingOptions
Recognition modeauto / number / expression (vocabulary masking)
Beam width1 (greedy, fastest) / 2 / 3 (default, best quality)
Execution providerwasm (default) / webgpu (auto-detected, 2–5x faster)
Stroke debounce delayseconds (default 1.5)
Model cacheshow IndexedDB status / clear cache / re-download (7.2 MB)
Interface languageChinese / English (follows DSH via ctx.locale)

Pattern: SettingsController external store over remote.getSettings/updateSettings, observed with useSyncExternalStore; fields edit a local draft and save on blur/change; the Host validates via validateSettings and rejects invalid patches.

Data Flow

Handwriting path

Pointer Events -> Canvas Stroke[]
  -> ink-on preprocessStrokes (resample, Bezier, scale to 256h)
  -> Web Worker: CoMER encoder -> decoder (ONNX WASM, ~1-2s)
  -> repairLatex (brace balancing, KaTeX validation)
  -> LaTeX string + KaTeX preview
  -> user confirms
  -> inputActions.setDraft(draft + \[latex\])
  -> inline-renderer detects closure -> preview strip renders

Screenshot path

paste / upload / capture -> ImageBitmap
  -> image-preprocess (grayscale, invert, scale to 256h, padding)
  -> same CoMER encoder -> decoder
  -> LaTeX + KaTeX preview -> confirm -> setDraft -> inline render

LaTeX editor path

keyboard input / palette click -> LaTeX string
  -> KaTeX live preview (per keystroke)
  -> confirm -> setDraft -> inline render

Build

Two-phase build, mirroring dsh-better-input exactly:

  1. tsdown — two configs:

    • Host: entries { index, typert, remote } from src/, format: 'esm', platform: 'node', target: 'es2022', sourcemaps.

    • Client: entry { client: 'src/client.ts' }, format: 'cjs', platform: 'browser', with outputOptions banner/footer wrapping the bundle in window.__ModuleLoader__.load({ id: 'dsh-math-input', factory: (require) => { ... return module.exports } }). Externals (react, react/jsx-runtime, react-dom, @deepseek-ai/cordis, and the @deepseek-ai/dsh-client-* framework packages) are resolved by the DSH module loader's require; everything else is bundled — including katex, onnxruntime-web, and ink-on.

  2. tsc -p tsconfig.build.json — declaration-only emit into lib/.

lib/ is committed (same convention as dsh-skills-nexus / dsh-better-input); CI re-builds and fails on drift. package.json exports maps ., ./client, ./typert, ./remote; files ships lib/ + cordis.patch.yml + README/LICENSE.

Peer dependencies (provisioned by the DSH web app at runtime): @deepseek-ai/cordis ^4.x, @deepseek-ai/dsh-client-runtime, dsh-client-ui-conversation, dsh-client-ui-slots, dsh-client-locale, dsh-settings, dsh-typert-protocol, dsh-api-remotes, @deepseek-ai/schemastery ^3.18, zod ^4. Dev/runtime: react 18, tsdown, typescript, katex, ink-on, onnxruntime-web.

Testing and CI

Quality gates (local)

npm run typecheck   # tsc --noEmit (strict)
npm run lint        # ESLint 9 flat config
npm test            # node:test + tsx, no extra framework
npm run build       # tsdown && tsc -p tsconfig.build.json -> lib/

Unit tests

Tests live in test/ and target pure-logic modules. No real ONNX model is loaded — the engine is mocked.

Test fileWhat it verifies
latex-detect.test.ts\[...\] closure detection, unclosed delimiter stays plain, nesting
latex-repair.test.tsbrace balancing, \frac/\sqrt argument fixing, KaTeX validation
preprocess.test.tsstroke resampling interval, tensor shape (256 x 64-aligned), empty stroke filter
image-preprocess.test.tsimage -> grayscale / invert / scale / padding shape correctness
engine-mock.test.tsmock InferenceEngine returns -> candidate selection logic

CI (.github/workflows/ci.yml)

Triggers on push and pull request. Matrix across Node 20 / 22 / 24. Runs: typecheck -> lint -> unit tests -> build. Also checks that the committed lib/ still matches a fresh build (prevents drift).

Local testing (two install modes)

Overlay (fast iteration) — against any dsh web installation:

  1. npm install && npm run build (generates lib/).

  2. Create overlay.yml inserting the built Host entry by absolute path:

    - insert:
        - id: dsh-math-input
          name: '/abs/path/to/dsh-math-input/lib/index.js'
    
  3. npx @deepseek-ai/dsh web --patch overlay.yml — the patch layer mounts the plugin; the client half is discovered via the package's dsh.client manifest (a patch file does not change the profile directory from which the loader resolves module paths, hence the absolute path).

  4. Verify the "+" button, the three input surfaces, and the settings section in the DSH web UI (default http://127.0.0.1:3080).

Installed plugin:

  1. Push to GitHub.
  2. dsh plugin --profile web add github:<owner>/dsh-math-input (or the npm name once published).
  3. Restart the profile; verify the same functionality.

Key Technical Decisions

DecisionChoiceRationale
Recognition engineink-on (CoMER, browser ONNX)Zero token, tiny model (7.2 MB), framework-agnostic core, LaTeX auto-repair built in. Trained on handwritten math, matching the use case.
Screenshot OCRReuse CoMER (one model)Avoids shipping a second model. If printed-formula accuracy is insufficient, swap in pix2tex ONNX behind the same interface.
Model hostingDownload from ink-on GitHub Releases + IndexedDB cacheNo self-hosted CDN needed. First load fetches 7.2 MB; subsequent loads are instant.
LaTeX delimiter\[ ... \] (primary), $$ ... $$ (compatible detection)Pure LaTeX, no $ ambiguity, model-native. Renderer also detects $$ for pasted content.
Launcher placementconversation.input.left slotVerified in the shipped slot hierarchy; no CSS hacks needed (the earlier draft's conversation.input.right fallback is obsolete).
Window form factorHandwriting/screenshot: portal modals. LaTeX editor: conversation.input.dock occupant.No modal slot exists in DSH; portals to document.body are the established pattern for floating plugin UI. The dock keeps the editor co-present with the composer.
Composer writesinputActions.setDraft onlyVerified write surface on session-scoped input slots; no composer DOM mutation in v1.
Inline renderingv1: dock preview strip over closed pairs; in-place rendering deferredThe composer draft is a plain string; replacing ranges with React nodes is not supported by the slot system. See Open Questions.
Host thicknessThin: settings-only TypertRemoteService (2 invocations)All recognition is browser-side. No ctx.llm, no inject beyond settings.
Contract styleHand-written typert/remote/remote-contract trio + zodHarness Typert codegen is internal; dsh-better-input proves the hand-written pattern ships fine.
Build toolchaintsdown (Host ESM + wrapped Client CJS) + tsc declarationsRequired by the window.__ModuleLoader__ client packaging; plain tsc cannot produce the wrapped client bundle.
Test frameworknode:test + tsxNo extra dependency, matches dsh-skills-nexus convention.

Open Questions

To resolve during implementation (edge-work, per plan agreement):

  1. Composer DOM for true inline rendering — v1 ships the dock preview strip. During implementation, inspect the live composer DOM (via browser devtools against a running dsh web) to judge whether a safe, version-tolerant in-place render (e.g., an overlay layer keyed to draft offsets) is feasible without fighting the framework.
  2. ink-on release asset URLs — exact encoder/decoder/vocab artifact URLs and checksums to pin in engine.ts; verify the 7.2 MB size claim and WebGPU provider availability on current onnxruntime-web.

Compatibility

  • DeepSeek Harness >= 0.1.1-rc.2 (Web profile); client packages developed against @deepseek-ai/dsh-client-* 0.1.0-rc.8 peer ranges (>=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0).

  • Node.js >= 20.0.0 for building (harness itself targets 22.19+/24+).

  • Chromium-based browser (Chrome / Edge) for ONNX WASM + WebGPU.

  • SharedArrayBuffer requires COOP/COEP headers for multi-threaded WASM (falls back to single-threaded without them).

License

The plugin code is licensed under MIT.

Third-party dependencies and their licenses are documented in THIRD_PARTY_NOTICES.md. Summary:

  • ink-on (recognition engine): Apache-2.0 — permissive, compatible with MIT. Conditions: preserve copyright and license notices, state changes.

  • KaTeX (LaTeX rendering): MIT.

  • onnxruntime-web (inference runtime): MIT.

  • DSH / Cordis (plugin framework): MIT.

  • CoMER model weights (downloaded at runtime, not bundled): no explicit license in the source repository. See THIRD_PARTY_NOTICES.md for provenance and risk assessment.

  • pix2tex (optional screenshot OCR fallback): MIT.

Avoid: lia-canvas-ocr (AGPL-3.0) — strong copyleft would force the entire plugin to GPL.

Research Sources

This document was verified against (2026-08-31):

  • deepseek-ai/deepseek-harness @ master: docs/architecture.md, docs/development.md, docs/subsystems/slots.md, docs/subsystems/typert.md, docs/subsystems/web-client.md, docs/subsystems/client-modules.md, docs/user/develop/basic/{index,config}.md.

  • DIAG5/dsh-better-input @ v0.1.8 (complete source): package.json, cordis.patch.yml, src/index.ts, src/config.ts, src/config-schema.ts, src/remote-contract.ts, src/typert.ts, src/remote.ts, src/about.ts, src/polish/service.ts, src/client.ts, src/client/{index,settings,settings-controller,strings}.ts(x), src/client/{MicrophoneButton,OptimizeButton,VoiceRecognitionBar,conversion-controller}.ts(x), tsdown.config.ts, tsdown.client.ts, tsconfig{,.build}.json.