Skip to Content
EditorEditor Guide

Editor

The editor is built on Lexical 0.39. A note is stored as a tree of editor nodes. Agent edits appear as pending suggestions first. You accept or reject each suggestion before it changes the note. Template sections can be locked so agents cannot write to them.

Components

AreaWhat it does
EditorCoreMounts Lexical, tracks selection, and dispatches commands.
LexicalNoteEditorThe active editor runtime: sets up the composer, owns the live Lexical editor instance, and bridges debounced snapshots back to app state. NoteEditorV2 remains as a compatibility wrapper around it.
TemplateManagerHolds template anchors and fields. Blocks writes to locked regions.
SuggestionOverlayRenders pending agent diffs in the editor and routes review to the revisions surface.
SyncBridgePersists debounced plain text and Lexical JSON snapshots, with blur and unload flushes.

Data Flow

User input enters EditorCore, updates the Lexical state, and emits change pipeline events. The pipeline updates undo/redo history, the operational change log, and the agent suggestion queue. The log feeds local storage and sync snapshots, while queued suggestions render in the overlay until the user accepts or rejects them.

Core Rules

  1. All document mutations go through editor.update(). Every write is transactional and observable.
  2. Lexical is the live typing source of truth. React note state and persisted JSON are snapshots.
  3. Agents and automation cannot modify locked template anchors. Only a user edit can.
  4. Each suggestion is tagged with the revision it was generated against. If the user edits in between, the suggestion’s range is remapped onto the new revision, or marked stale if remapping can’t resolve it.
  5. Undo/redo only covers changes that actually landed. Rejected and stale suggestions never wrote to the document, so they don’t appear in history.
  6. Every applied change, whether from a user or an agent, emits the same change event. Sync, logging, and other subscribers read from one stream.

Document Model

Documents are serialized as node trees:

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"; }

Node Types

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[]; }

Change Tracking

Rosetta keeps two separate records of changes:

  • Lexical history stack: what undo/redo walks through. Only contains changes that were applied.
  • Operational log: what sync and agent bookkeeping read from. Contains every transaction with its source, revision, and ranges.

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; }; }

Keystroke To Commit Flow

During typing, Rosetta computes one derived editor snapshot per Lexical update. That snapshot carries the plain text, selection offsets, cursor position, slash command context, SmartPhrase context, and context menu selection geometry. JSON serialization waits until the persistence debounce or an explicit flush point such as blur, unload, save, or note switch.

Diffs

Rosetta compares the old note and the new note in three passes. Each pass only runs if the previous one did not produce a reliable result:

  1. Nodes: compare editor subtrees by identity. If a whole paragraph or template block did not change, Rosetta skips it.
  2. Tokens: compare sentence sized chunks or template chunks. Most clinical edits stop here because this is accurate enough for review.
  3. Characters: compare individual characters when token precision drops below 98%. This lets the overlay highlight the exact text that changed.
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

A suggestion points at specific character offsets. If the user types before the suggestion is applied, those offsets can point at the wrong place. Rosetta shifts each offset by the net length change of every user edit that happened at or before it:

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

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; }

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 highlighted with replacement preview
DeleteRedOriginal shown with remove indicator

Template Anchors

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.

Template Fields

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

Expansion rules for common note 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" } ];

Expansion is treated as a user transaction. Pending agent suggestions touching the trigger span are remapped, and if remap fails they are marked stale.

Plugin Order

<LexicalComposer initialConfig={config}> <RichTextPlugin /> <HistoryPlugin /> <RosettaStateSyncPlugin /> <TemplateLockPlugin /> <TemplateReconciliationPlugin /> <EditorCommandPlugin /> <ClinicalHighlightPlugin /> <IndentGuidesPlugin /> </LexicalComposer>

RosettaStateSyncPlugin owns the external load path and the shared derived editor snapshot. EditorCommandPlugin owns keyboard commands, suggestion commands, and template commands. Clinical highlighting and indent guides run after editor mutations, but both are scheduled outside the immediate typing path.

Keyboard Shortcuts

ShortcutAction
Ctrl/Cmd + SSave the current note (or template, in the template editor)
Ctrl/Cmd + WClose the current note tab
Ctrl/Cmd + ZUndo
Ctrl/Cmd + Shift + ZRedo
Tab / Shift + TabIndent / outdent
Enter or TabAccept a smart phrase expansion
EscDismiss a smart phrase expansion