Skip to Content
EditorEditor Guide

Editor

Rosetta uses a custom Lexical 0.39 stack with tokenized document state, template anchors, and an agent review queue. This page covers the editor internals, including change tracking and edge case behavior.

At A Glance

AreaWhat It Handles
EditorCoreHost, selection, command routing
NoteEditorV2Composer, plugins, transactions
TemplateManagerAnchors, fields, lock enforcement
SuggestionOverlayPending diffs and review actions
SyncBridgeLocal store and cloud sync

Figure 1: Runtime Topology

Core Invariants

  1. All mutations happen inside editor.update().
  2. Locked anchors are immutable for agent edits and template automation.
  3. Character ranges used by suggestions are revision-scoped and remapped before apply.
  4. Undo/redo affects only applied document changes, never discarded suggestions.
  5. Every accepted agent edit emits a structured change event for auditability.

Document Model

Documents are serialized as node trees, not HTML strings:

interface SerializedEditorState { root: { type: "root"; version: number; children: SerializedNode[]; direction: "ltr" | "rtl" | null; }; } interface SerializedNode { type: string; version: number; children?: SerializedNode[]; text?: string; format?: number; detail?: number; mode?: "normal" | "segmented" | "token"; }

Why This Model

  • Portable between browser/runtime boundaries
  • Safer than HTML transport for clinical text
  • Deterministic diffing against prior snapshots
  • Enables anchored range mapping for suggestions

Node Types

Built-In + Rosetta Nodes

class RosettaTextNode extends TextNode { __format: number; } class RosettaParagraphNode extends ParagraphNode { __indent: number; } class AnchorNode extends ElementNode { __label: string; __isLocked: boolean; } class FieldNode extends DecoratorNode<JSX.Element> { __fieldType: "SELECT" | "MULTISELECT" | "DYNAMIC_SELECT"; __options: string[]; }

Custom Node Example

export class SmartPhraseNode extends TextNode { static getType(): string { return "smart-phrase"; } static clone(node: SmartPhraseNode): SmartPhraseNode { return new SmartPhraseNode(node.__text, node.__key); } createDOM(): HTMLElement { const span = document.createElement("span"); span.className = "smart-phrase"; return span; } }

Change Tracking

Rosetta tracks changes in two layers:

  • Lexical history stack for user-facing undo/redo
  • Operational log for sync, agent traceability, and conflict logic

Transaction Shape

type ChangeSource = "user" | "ai" | "template" | "sync"; type ChangeKind = "insert" | "replace" | "delete" | "format"; interface ChangeTransaction { id: string; revision: number; parentRevision: number; source: ChangeSource; kind: ChangeKind; createdAt: number; ranges: Array<{ start: number; end: number; beforeText: string; afterText: string; anchorId?: string; }>; metadata?: { suggestionId?: string; agent?: "chat" | "shorthander" | "reasoner" | "reformatter" | "citations" | "ingester"; batched?: boolean; }; }

Figure 2: Keystroke-To-Commit Flow

Diff Strategy

For reliable clinical text diffing, Rosetta uses staged comparison:

  1. Node-level quick check (skip unchanged subtrees)
  2. Token-level diff (sentence or field chunks)
  3. Character-level fallback for precise selection overlays
function computeRanges(prev: string, next: string): RangeDelta[] { const tokenPass = diffByTokens(prev, next); if (tokenPass.precision >= 0.98) return tokenPass.ranges; return diffByCharacters(prev, next).ranges; }

Range Remapping

Pending agent edits are revision-bound. If user edits arrive first, ranges are remapped before apply:

function remapRange(range: { start: number; end: number }, deltas: RangeDelta[]) { let { start, end } = range; for (const delta of deltas) { if (delta.pos <= start) start += delta.netLength; if (delta.pos < end) end += delta.netLength; } return { start, end }; }

Overlap and Conflict Rules

When two pending edits overlap, Rosetta resolves deterministically:

  1. Locked anchor guard always wins.
  2. User edits override pending agent edits.
  3. Newer agent suggestion supersedes older suggestion from the same agent scope.
  4. Cross-agent overlap enters manual review state.

Agent Suggestion Lifecycle

Suggestion Object

type SuggestionStatus = | "pending" | "focused" | "accepted" | "rejected" | "stale" | "superseded"; interface AISuggestion { id: string; revision: number; type: "replace" | "insert" | "delete"; start: number; end: number; replacementText: string; reasoning: string; confidence: number; status: SuggestionStatus; }

Figure 3: Suggestion State Machine

Safe Apply Logic

function applySuggestion(suggestion: AISuggestion) { editor.update(() => { if (isLockedRange(suggestion.start, suggestion.end)) return; const remapped = remapSuggestionAgainstLatestRevision(suggestion); if (!remapped) { markSuggestion(suggestion.id, "stale"); return; } replaceText(remapped.start, remapped.end, remapped.replacementText); appendChangeTransaction({ source: "ai", kind: suggestion.type === "insert" ? "insert" : "replace", metadata: { suggestionId: suggestion.id } }); markSuggestion(suggestion.id, "accepted"); }); }

Visual Indicators

TypeColorBehavior
InsertGreenPreview as additive text
ReplaceYellowOriginal text highlighted with replacement preview
DeleteRedOriginal text shown with remove indicator

Template Anchors

Anchors define protected structure:

interface TemplateAnchor { id: string; label: string; isLocked: boolean; nodeKey: string; start: number; end: number; }

Guard Rails

  • Agents cannot modify locked anchors.
  • Bulk accept skips suggestions touching locked anchors.
  • Template updates can move anchors but cannot silently unlock them.
  • Anchor deletion requires explicit user confirmation.

Anchor Navigation

ShortcutAction
Ctrl + ]Jump to next anchor
Ctrl + [Jump to previous anchor
Ctrl + EnterFill current anchor and advance

Template Fields

Field Types

SELECT single choice:

{{SELECT: medication | aspirin, clopidogrel, warfarin}}

MULTISELECT multiple values:

{{MULTISELECT: symptoms | chest pain, dyspnea, diaphoresis}}

DYNAMIC_SELECT options resolved from query:

{{DYNAMIC_SELECT: drug | query: medications for hypertension}}

Field Node Rendering

class FieldNode extends DecoratorNode<JSX.Element> { decorate(): JSX.Element { return ( <FieldComponent type={this.__fieldType} options={this.__options} onSelect={this.handleSelect} /> ); } }

SmartPhrases

SmartPhrases are expansion rules for high-frequency sections:

const smartPhrases: SmartPhrase[] = [ { trigger: ".cc", expansion: "Chief Complaint:\n" }, { trigger: ".hpi", expansion: "History of Present Illness:\n" }, { trigger: ".pe", expansion: "Physical Examination:\n" }, { trigger: ".ap", expansion: "Assessment & Plan:\n" } ];

Nuance: Expansion + Suggestions

  • Expansion is treated as a user transaction (source: "user").
  • Pending agent suggestions touching the trigger span are remapped.
  • If remap fails, those suggestions are marked stale.

Selection And Positioning

Character-Level Selection

const selection = $getSelection(); if ($isRangeSelection(selection)) { const anchor = selection.anchor; const focus = selection.focus; console.log({ anchorNode: anchor.key, anchorOffset: anchor.offset, focusNode: focus.key, focusOffset: focus.offset }); }

Programmatic Range Selection

editor.update(() => { const node = $getNodeByKey(nodeKey); if ($isTextNode(node)) { node.select(startOffset, endOffset); } });

Plugin And Command Order

Recommended plugin sequence:

<LexicalComposer initialConfig={config}> <RichTextPlugin /> <HistoryPlugin /> <OnChangePlugin onChange={handleChange} /> <TemplatePlugin /> <SmartPhrasePlugin /> <SuggestionPlugin /> <AutocompletePlugin /> </LexicalComposer>

Why Ordering Matters

  1. TemplatePlugin should run before SuggestionPlugin so locks are enforceable at preview time.
  2. OnChangePlugin should run before network sync side effects.
  3. SmartPhrasePlugin should execute before agent preview generation to avoid stale ranges.

Command Priority Pattern

function MyPlugin(): null { const [editor] = useLexicalComposerContext(); useEffect(() => { return editor.registerCommand( MY_COMMAND, (payload) => { if (!payload) return false; editor.update(() => { // Apply a single atomic transaction. }); return true; }, COMMAND_PRIORITY_NORMAL ); }, [editor]); return null; }

Keyboard Shortcuts Reference

ShortcutAction
Ctrl/Cmd + BBold
Ctrl/Cmd + IItalic
Ctrl/Cmd + UUnderline
Ctrl/Cmd + ZUndo
Ctrl/Cmd + Shift + ZRedo
TabAccept focused agent suggestion
EscReject focused agent suggestion
Ctrl/Cmd + Shift + AAccept all pending suggestions
Ctrl/Cmd + Shift + XReject all pending suggestions
Ctrl + ]Next anchor
Ctrl + [Previous anchor
Ctrl + EnterFill anchor and advance

Performance Notes

Do

  • Batch related edits in one editor.update().
  • Memoize expensive decorators (field components, inline preview components).
  • Keep suggestion overlay calculations incremental by revision.

Avoid

  • Calling $getRoot().getTextContent() repeatedly in hot paths.
  • Rebuilding full range maps on every cursor move.
  • Triggering sync writes per keystroke without buffering.

Batching Example

// Preferred: one atomic transaction. editor.update(() => { applyTemplateDefaults(); insertSmartPhraseExpansion(); queueAutosaveFlag(); });

Related:

Last updated on