Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893// src/App.tsximport { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'import type { AutomergeUrl } from './storage/document-runtime-contract'import { DocumentManager } from './services/document-manager'import { WELCOME_DOCUMENT_CONTENT as DEMO_CONTENT } from './services/welcome-document'import { SearchProvider, useSearch } from './services/SearchContext'import type { SaveStatus } from './sync/sync-contract'import { LayoutProvider, useLayout, AppShell, type EditorMode } from './layout'import { loadWorkspaceTabs } from './layout/layout-persistence'import { ATPROTO_VAULT_ROOT_URL_KEY } from './services/vault-root-url'import { App as PluginApp, type TFile, type Workspace } from './plugin'import { MarkdownView } from './plugin/views/markdown-view'import { Notice } from './plugin/obsidian-stubs'import type { LoadResult } from './services/url-router'import { registerContentTypeInvariantProvider, registerHostContractInvariantProvider, registerRepositoryInvariantProvider,} from './testing/invariants/reams-invariant-diagnostics'import { registerPerfDiagnostics } from './testing/perf/reams-perf-diagnostics'import { EXERCISE_TYPE_JSON, installPortentProfile } from './types-records/portent-profile'import { setTypesRecordsEnabled } from './types-records/feature-setting'import type { AppRuntime } from './app/app-runtime'import { DAILY_STARTER_TEMPLATE_PATH, STARTER_TEMPLATE_PATH } from './plugins/templates/starter-template-content'import type { BootStage } from './app/boot-stages'import { bindTextFirstPageLifecycle } from './spikes/text-first-page-lifecycle'import { installPageZoomGuard } from './app/page-zoom'import { ensureTextFirstDemoFixtures, isTextFirstDemoName, type TextFirstDemoName, TEXT_FIRST_DEMO_FIXTURE,} from './testing/invariants/text-first-demo-fixture'
declare global { interface Window { __reamsApp?: PluginApp } }
/** * Initial URL hash captured ONCE at module load (#411). * * Captured here, at the top of the file, rather than inside an effect * because the App's mount effect calls `pluginApp.reattach()` → * `urlRouter.attach()`, which subscribes to `workspace.active-leaf-change` * and may overwrite `window.location.hash` via `replaceState` during * `restoreLayout`. Reading the snapshot here freezes the user's * original URL hash so the dedicated `loadInitialUrl` effect (below) * can act on it after init completes. * * SSR-safe via `typeof window !== 'undefined'` check. Module-level so * Vite HMR re-evaluation captures the current hash — which is benign, * since `workspace.openFile` dedups by path. */const INITIAL_URL_HASH = typeof window !== 'undefined' ? window.location.hash : ''const INVARIANT_FIXTURE = import.meta.env.DEV && typeof window !== 'undefined' ? new URLSearchParams(window.location.search).get('invariantFixture') : nullconst TEXT_FIRST_DEMO_NAME: TextFirstDemoName = typeof window !== 'undefined' && isTextFirstDemoName(new URLSearchParams(window.location.search).get('demo')) ? new URLSearchParams(window.location.search).get('demo') as TextFirstDemoName : 'start'export const LIST_FOLDS_INVARIANT_FIXTURE = 'list-folds'export const STRUCTURAL_INVARIANT_FIXTURE = 'structural-simple'export const WELCOME_INVARIANT_FIXTURE = 'welcome-real'export const TYPED_LISTS_INVARIANT_FIXTURE = 'typed-lists'export const BIDIRECTIONAL_RECORDS_INVARIANT_FIXTURE = 'bidirectional-records'export const PORTENT_RECORDS_INVARIANT_FIXTURE = 'portent-records'export const TEXT_FIRST_DEMO_INVARIANT_FIXTURE = TEXT_FIRST_DEMO_FIXTUREexport const DATACORE_INVARIANT_FIXTURE = 'datacore'export const LIST_FOLDS_INVARIANT_CONTENT = [ '- root parent', ' - [ ] child task', ' - grandchild', ' - child sibling', '- outside sibling',].join('\n')const STRUCTURAL_INVARIANT_CONTENT = 'paragraph'export const TYPED_LISTS_INVARIANT_CONTENT = [ '- typed parent #task', ' - nested child with enough text to wrap at narrow widths without changing its list depth or marker alignment', ' - deep child', ' - nested sibling', '- comparison parent', ' - comparison nested child with enough text to wrap at narrow widths without changing its list depth or marker alignment', ' - comparison deep child', '- outside sibling',].join('\n')export const BIDIRECTIONAL_RECORDS_INVARIANT_CONTENT = [ '---', 'types: [task, exercise]', 'task.title: Root title', 'task.done: false', 'task.priority: high', 'task.due: 2026-08-28', 'task.url: https://example.com/offline', 'task.belongs_to: "[[Root]]"', 'exercise.text: Root text', 'exercise.number: 7', 'exercise.boolean: false', 'exercise.date: 2026-08-28', 'exercise.datetime: 2026-08-28T10:00:00Z', 'exercise.select: open', 'exercise.url: https://example.com/exercise', 'exercise.email: test@example.com', 'exercise.phone: +1 555 0100', 'exercise.reference: "[[Root]]"', '---', '- [ ] Alpha task #task', ' - [x] Bravo task #task', '- [ ] Gamma task #task',].join('\n')
let listFoldsFixturePromise: Promise<TFile> | null = nulllet structuralFixturePromise: Promise<TFile> | null = nulllet welcomeFixturePromise: Promise<TFile> | null = nulllet typedListsFixturePromise: Promise<TFile> | null = nulllet bidirectionalRecordsFixturePromise: Promise<TFile> | null = nulllet portentRecordsFixturePromise: Promise<TFile> | null = null
function ensureListFoldsInvariantFixture(pluginApp: PluginApp) { if (!listFoldsFixturePromise) { listFoldsFixturePromise = (async () => { const basePath = '__invariants__/list-folds-v2.md' const existing = pluginApp.vault.getFileByPath(basePath) if (existing) { if (await pluginApp.vault.read(existing) === LIST_FOLDS_INVARIANT_CONTENT) return existing // A user or an earlier campaign owns the fixed path. Never overwrite it. let suffix = 2 while (pluginApp.vault.getFileByPath(`__invariants__/list-folds-v2-${suffix}.md`)) suffix += 1 return pluginApp.vault.create(`__invariants__/list-folds-v2-${suffix}.md`, LIST_FOLDS_INVARIANT_CONTENT) } return pluginApp.vault.create(basePath, LIST_FOLDS_INVARIANT_CONTENT) })() } return listFoldsFixturePromise}
function ensureTypedListsInvariantFixture(pluginApp: PluginApp) { if (!typedListsFixturePromise) { typedListsFixturePromise = (async () => { setTypesRecordsEnabled(true) await installPortentProfile(pluginApp.vault) const fixturePath = '__invariants__/typed-lists.md' const existing = pluginApp.vault.getFileByPath(fixturePath) if (!existing) return pluginApp.vault.create(fixturePath, TYPED_LISTS_INVARIANT_CONTENT) if (await pluginApp.vault.read(existing) === TYPED_LISTS_INVARIANT_CONTENT) return existing return pluginApp.vault.create(`__invariants__/typed-lists-${Date.now()}.md`, TYPED_LISTS_INVARIANT_CONTENT) })() } return typedListsFixturePromise}
function ensureBidirectionalRecordsInvariantFixture(pluginApp: PluginApp) { if (!bidirectionalRecordsFixturePromise) { bidirectionalRecordsFixturePromise = (async () => { setTypesRecordsEnabled(true) await installPortentProfile(pluginApp.vault, { additionalTypes: { exercise: EXERCISE_TYPE_JSON } }) const exerciseType = pluginApp.vault.getFileByPath('types/exercise.json') if (exerciseType && await pluginApp.vault.read(exerciseType) !== EXERCISE_TYPE_JSON) { await pluginApp.vault.modify(exerciseType, EXERCISE_TYPE_JSON) } const fixturePath = '__invariants__/bidirectional-records.md' const existing = pluginApp.vault.getFileByPath(fixturePath) if (!existing) return pluginApp.vault.create(fixturePath, BIDIRECTIONAL_RECORDS_INVARIANT_CONTENT) if (await pluginApp.vault.read(existing) === BIDIRECTIONAL_RECORDS_INVARIANT_CONTENT) return existing return pluginApp.vault.create(`__invariants__/bidirectional-records-${Date.now()}.md`, BIDIRECTIONAL_RECORDS_INVARIANT_CONTENT) })() } return bidirectionalRecordsFixturePromise}
const PORTENT_RECORD_FIXTURES: ReadonlyArray<readonly [string, string]> = [ ['project.md', `---\ntypes: [project, task, operation]\nproject.title: Reams launch\nproject.state: organized\nproject.deadline: 2026-09-30\nproject.success_criteria: [stable sync, fast search]\nproject.budget: 12500\noperation.title: Weekly release review\noperation.cadence: [weekly, monthly]\noperation.status: organized\noperation.estimate: 3\ntags: [local-first, markdown, offline]\naliases: [Launch, Release]\npreferences:\n notifications:\n email: daily\n push: true\n reviewers:\n - Anish\n - Priya\nsummary: |\n Keep ordinary YAML visible.\n Let structured controls edit it recursively.\n---\nA focused product launch project.\n`], ['operation.md', `---\ntypes: [operation]\noperation.title: Weekly release review\noperation.state: captured\noperation.cadence: [weekly, monthly]\noperation.status: organized\noperation.estimate: 3\n---\nKeep the release train healthy.\n`], ['responsibility.md', `---\ntypes: [responsibility]\nresponsibility.title: Editor quality\nresponsibility.state: organized\nresponsibility.indicators: {owner: Anish, escalation: email}\nresponsibility.owner: Anish\n---\nA clearly owned responsibility.\n`], ['task.md', `---\ntypes: [task]\ntask.title: Verify offline edits\ntask.state: captured\ntask.done: false\ntask.due: 2026-08-28\ntask.priority: high\ntask.url: https://example.com/offline\n---\nA task with every common control.\n`], ['event.md', `---\ntypes: [event]\nevent.title: Product review\nevent.state: organized\nevent.date: 2026-09-14T15:30:00Z\nevent.attendees: [Anish, Priya]\nevent.location: Studio\nevent.online: true\n---\nReview the launch readiness.\n`], ['note.md', `---\ntypes: [note]\nnote.title: Launch research\nnote.state: captured\nnote.kind: [fleeting, reference]\nnote.source: https://example.com/research\n---\nA source-backed note.\n`], ['topic.md', `---\ntypes: [topic]\ntopic.title: Local first\ntopic.state: organized\ntopic.url: https://example.com/local-first\n---\nA topic with a canonical URL.\n`], ['person.md', `---\ntypes: [person]\nperson.title: Priya Shah\nperson.state: captured\nperson.role: Design lead\nperson.organization: Reams\nperson.url: https://example.com/priya\nperson.social: {github: priyashah, mastodon: '@priya@example.social'}\n---\nA collaborator record.\n`],]
function ensurePortentRecordsInvariantFixture(pluginApp: PluginApp) { if (!portentRecordsFixturePromise) { portentRecordsFixturePromise = (async () => { setTypesRecordsEnabled(true) await installPortentProfile(pluginApp.vault) let first: TFile | undefined for (const [name, content] of PORTENT_RECORD_FIXTURES) { const path = `__invariants__/portent-records/${name}` const existing = pluginApp.vault.getFileByPath(path) const file = existing ?? await pluginApp.vault.create(path, content) if (existing && await pluginApp.vault.read(existing) !== content) { await pluginApp.vault.modify(existing, content) } first ??= file } if (!first) throw new Error('Portent invariant fixture is empty') return first })() } return portentRecordsFixturePromise}
function ensureStructuralInvariantFixture(pluginApp: PluginApp) { if (!structuralFixturePromise) { structuralFixturePromise = (async () => { const fixturePath = '__invariants__/structural-simple.md' const existing = pluginApp.vault.getFileByPath(fixturePath) if (existing) { if (await pluginApp.vault.read(existing) === STRUCTURAL_INVARIANT_CONTENT) return existing return pluginApp.vault.create( `__invariants__/structural-simple-${Date.now()}.md`, STRUCTURAL_INVARIANT_CONTENT, ) } return pluginApp.vault.create(fixturePath, STRUCTURAL_INVARIANT_CONTENT) })() } return structuralFixturePromise}
/** * Map Reams editor mode to Obsidian plugin API mode string. * Obsidian uses 'source' for both supported Reams editor modes. */export function mapEditorModeToObsidian(_mode: EditorMode): 'source' { return 'source'}
export function useActiveMarkdownView(workspace: Workspace): MarkdownView | null { const subscribe = useCallback((onStoreChange: () => void) => { const activeLeafRef = workspace.on('active-leaf-change', onStoreChange) const activeEditorRef = workspace.on('active-editor-change', onStoreChange) const layoutRef = workspace.on('layout-change', onStoreChange)
return () => { workspace.offref(activeLeafRef) workspace.offref(activeEditorRef) workspace.offref(layoutRef) } }, [workspace])
const getSnapshot = useCallback(() => { const activeEditor = workspace.activeEditor return activeEditor instanceof MarkdownView ? activeEditor : null }, [workspace])
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)}
function ensureWelcomeInvariantFixture(pluginApp: PluginApp) { if (!welcomeFixturePromise) { welcomeFixturePromise = (async () => { const fixturePath = '__invariants__/welcome-real.md' const existing = pluginApp.vault.getFileByPath(fixturePath) if (existing) { if (await pluginApp.vault.read(existing) === DEMO_CONTENT) return existing return pluginApp.vault.create(`__invariants__/welcome-real-${Date.now()}.md`, DEMO_CONTENT) } return pluginApp.vault.create(fixturePath, DEMO_CONTENT) })() } return welcomeFixturePromise}
const DEMO_DOC_KEY = 'reams:demo-doc-url'
// Module-level promise to deduplicate concurrent createDemoDocument calls.// React StrictMode double-fires effects in development, causing two concurrent// async invocations. Both share this promise so only one file is created.// Reset to null on failure so retries can attempt creation again.let demoDocPromise: Promise<AutomergeUrl | null> | null = null
/** * Shared helper to ensure the welcome demo doc exists. Both the init effect's * normal "no saved tabs" path AND the URL load effect's failure fallback call * this — they share `demoDocPromise` so concurrent calls dedupe to a single * `vault.create` (matters under StrictMode double-mount and under the * "URL load fails after init effect skipped demo creation" path). */async function ensureWelcomeDoc(pluginApp: PluginApp, documentManager: DocumentManager): Promise<AutomergeUrl | null> { const demoKey = pluginApp.storageScope ? `reams:${pluginApp.storageScope}:demo-doc-url` : DEMO_DOC_KEY if (!demoDocPromise) { demoDocPromise = (async () => { try { const existingFile = pluginApp.vault.getFileByPath('welcome.md') if (existingFile) { const url = pluginApp.vault.getUrlForPath('welcome.md') if (url) { localStorage.setItem(demoKey, url) return url } } const url = await documentManager.createOrOpenWelcomeFile(pluginApp.vault.rootUrl) localStorage.setItem(demoKey, url) return url } catch (err) { console.error('Failed to create demo document:', err) demoDocPromise = null return null } })() } const url = await demoDocPromise // Reset so a subsequent failure-path retry can try again (the resolved // value is still cached for in-flight callers above). demoDocPromise = null return url}
/** * Inner app component that uses the layout context. */function AppContent({ pluginApp, documentManager, edition, allowDemoFallback, accountUi, syncSettingsSection, onReady, onStage, enabledStateStore, recentVisits, feedbackModal, publishContribution,}: { pluginApp: PluginApp documentManager: DocumentManager edition: AppRuntime['edition'] allowDemoFallback: boolean accountUi: AppRuntime['accountUi'] syncSettingsSection: AppRuntime['syncSettingsSection'] onReady?: () => void onStage?: (stage: BootStage) => void enabledStateStore: AppRuntime['enabledStateStore'] recentVisits: AppRuntime['recentVisits'] feedbackModal: AppRuntime['feedbackModal'] publishContribution: AppRuntime['publishContribution']}) { const { state } = useLayout() // Only need state for editorMode now const { isInitialized: searchInitialized } = useSearch() const [initialized, setInitialized] = useState(false)
const [saveStatus, setSaveStatus] = useState<SaveStatus>({ state: 'idle' }) const [visibleSaveStatus, setVisibleSaveStatus] = useState<SaveStatus>({ state: 'idle' }) const [isConcurrentTabOpen, setIsConcurrentTabOpen] = useState(false)
useEffect(() => { if (saveStatus.state === 'idle' || saveStatus.state === 'error') { setVisibleSaveStatus(saveStatus) return } if (saveStatus.state === 'saving') { setVisibleSaveStatus(saveStatus) return } // Wait for a quiet spell before showing "Saved" so typing doesn't flicker. const show = setTimeout(() => setVisibleSaveStatus({ state: 'saved' }), 450) const hide = setTimeout(() => setVisibleSaveStatus({ state: 'idle' }), 1650) return () => { clearTimeout(show); clearTimeout(hide) } }, [saveStatus])
const handleToggleMode = useCallback(() => { pluginApp.commands.executeCommandById('reams-layout-commands:toggle-editor-mode') }, [pluginApp])
// Bootstrap: restore tabs → load URL → fall back to demo doc, all in ONE // sequenced effect. Critical for #411: the URL load MUST run after // restoreLayout fully resolves, so `_findRootLeafByUrl` sees settled // leaves (with `leaf.file` populated) rather than phantom leaves whose // open is still in flight. // // Why a single effect and not two: // - Previously the URL load lived in a separate effect gated on // `setInitialized(true)`. `setInitialized` would fire as soon as a // leaf existed in `this.leaves` (via `iterateRootLeaves`), but // `getLeaf(true)` synchronously pushes a leaf BEFORE its view+file // are assigned. So URL load could race against an in-flight // restoreLayout and create a second tab for the same file. // - The single-effect design awaits restoreLayout (via the workspace's // coalesced in-flight promise, see `Workspace.restoreLayout`) before // touching the URL hash. // // Wait only for the indexer's event wiring to start, not for a full vault // scan. Canonical Vault/FileIndex hydration already completed above. const urlLoadAttempted = useRef(false) const demoFallbackAttempted = useRef(false) useEffect(() => { if (initialized) return
let cancelled = false
async function bootstrap() { if (!searchInitialized) { onStage?.('starting-search') return } // Dedicated browser-invariant fixtures boot through the real vault and // workspace rather than mutating an already-mounted editor through test // diagnostics. They intentionally bypass restored tabs so IndexedDB from // another campaign cannot substitute unrelated document content. if (INVARIANT_FIXTURE === LIST_FOLDS_INVARIANT_FIXTURE) { onStage?.('opening-note') try { const file = await ensureListFoldsInvariantFixture(pluginApp) if (cancelled) return await pluginApp.workspace.openFile(file) } catch (err) { console.error('Failed to load list-fold invariant fixture:', err) } if (!cancelled) setInitialized(true) return } if (INVARIANT_FIXTURE === TYPED_LISTS_INVARIANT_FIXTURE) { onStage?.('opening-note') try { const file = await ensureTypedListsInvariantFixture(pluginApp) if (cancelled) return await pluginApp.workspace.openFile(file) } catch (err) { console.error('Failed to load typed-list invariant fixture:', err) } if (!cancelled) setInitialized(true) return } if (INVARIANT_FIXTURE === BIDIRECTIONAL_RECORDS_INVARIANT_FIXTURE) { onStage?.('opening-note') try { const file = await ensureBidirectionalRecordsInvariantFixture(pluginApp) if (cancelled) return await pluginApp.workspace.openFile(file) } catch (err) { console.error('Failed to load bidirectional record fixture:', err) } if (!cancelled) setInitialized(true) return } if (INVARIANT_FIXTURE === PORTENT_RECORDS_INVARIANT_FIXTURE) { onStage?.('opening-note') try { const file = await ensurePortentRecordsInvariantFixture(pluginApp) if (cancelled) return await pluginApp.workspace.openFile(file) } catch (err) { console.error('Failed to load Portent records invariant fixture:', err) } if (!cancelled) setInitialized(true) return } if (INVARIANT_FIXTURE === STRUCTURAL_INVARIANT_FIXTURE) { onStage?.('opening-note') try { const file = await ensureStructuralInvariantFixture(pluginApp) if (cancelled) return await pluginApp.workspace.openFile(file) } catch (err) { console.error('Failed to load structural invariant fixture:', err) } if (!cancelled) setInitialized(true) return } if (INVARIANT_FIXTURE === WELCOME_INVARIANT_FIXTURE) { onStage?.('opening-note') try { const file = await ensureWelcomeInvariantFixture(pluginApp) if (cancelled) return await pluginApp.workspace.openFile(file) } catch (err) { console.error('Failed to load Welcome invariant fixture:', err) } if (!cancelled) setInitialized(true) return } if (INVARIANT_FIXTURE === TEXT_FIRST_DEMO_INVARIANT_FIXTURE) { onStage?.('opening-note') try { const file = await ensureTextFirstDemoFixtures( pluginApp.vault, TEXT_FIRST_DEMO_NAME, window.location.origin, ) if (cancelled) return await pluginApp.workspace.openFile(file) } catch (err) { console.error('Failed to load text-first demo fixture:', err) } if (!cancelled) setInitialized(true) return } if (INVARIANT_FIXTURE === DATACORE_INVARIANT_FIXTURE) { onStage?.('opening-note') try { const { ensureDatacoreInvariantFixture } = await import('./testing/invariants/datacore-fixture') if (cancelled) return const file = await ensureDatacoreInvariantFixture(pluginApp.vault) if (cancelled) return await pluginApp.workspace.openFile(file) } catch (err) { console.error('Failed to load Datacore invariant fixture:', err) } if (!cancelled) setInitialized(true) return } // Step 1: restore saved tabs (or no-op if none). Under StrictMode // remount, this awaits the FIRST mount's in-flight restore via the // workspace's coalesced promise — both mounts see the same settled // state when their `await` resolves. onStage?.('restoring-tabs') const savedTabs = loadWorkspaceTabs(pluginApp.storageScope ? `reams:${pluginApp.storageScope}:workspace-tabs` : undefined) if (savedTabs && savedTabs.tabs.length > 0) { try { await pluginApp.workspace.restoreLayout(savedTabs, pluginApp.vault) } catch (err) { console.error('Failed to restore workspace tabs:', err) // Continue — URL load may still succeed, or fallback may create demo. } } else { // No saved tabs to restore, but if some other mount is mid-restore // (extremely unlikely without saved tabs, but defensive), still wait. await pluginApp.workspace.whenLayoutRestored() } if (cancelled) return
// Step 2: hand off the initial URL hash to the router. Guards against // re-running under StrictMode via `urlLoadAttempted`. onStage?.('opening-note') let urlResult: LoadResult | null = null if (!urlLoadAttempted.current) { urlLoadAttempted.current = true try { // INITIAL_URL_HASH is the module-level snapshot — see declaration. urlResult = await pluginApp.urlRouter.loadInitialUrl(INITIAL_URL_HASH) } catch (err) { // loadInitialUrl normally swallows errors and returns a // LoadResult; this catch is defence-in-depth. Roll back the // claim on StrictMode-unmount-during-await so a fresh mount can // retry. if (cancelled) { urlLoadAttempted.current = false return } console.warn('App: urlRouter.loadInitialUrl threw unexpectedly:', err) } if (cancelled) return }
// Step 3: surface URL load failures. if (urlResult?.status === 'unreachable') { new Notice( 'Could not load shared document — it may not be reachable from this device. ' + 'Check the URL and your sync server.', 7000, ) } else if (urlResult?.status === 'invalid') { new Notice('Invalid document URL in address bar — ignoring.', 5000) }
// Step 4: if NO leaves exist at this point (no saved tabs, no // successful URL load), open an existing note or create the demo doc. Guarded by // `demoFallbackAttempted` so StrictMode remount doesn't double-fire // demo creation (independent of the ensureWelcomeDoc dedup, which // protects against concurrent invocation rather than sequential). let hasLeaves = false pluginApp.workspace.iterateRootLeaves(() => { hasLeaves = true }) if (!hasLeaves && !demoFallbackAttempted.current) { const existingFile = pluginApp.vault.getFileByPath('welcome.md') ?? pluginApp.vault.getMarkdownFiles().find(file => file.path !== STARTER_TEMPLATE_PATH && file.path !== DAILY_STARTER_TEMPLATE_PATH) if (existingFile) { await pluginApp.workspace.openFile(existingFile) demoFallbackAttempted.current = true } else if (allowDemoFallback) { try { const welcomeUrl = await ensureWelcomeDoc(pluginApp, documentManager) if (cancelled) return if (welcomeUrl) { const file = pluginApp.vault.getFileByUrl(welcomeUrl) if (file) { pluginApp.workspace.openFile(file) // Claim the fallback only after this effect instance requests // the open. StrictMode can cancel while Welcome is being created. demoFallbackAttempted.current = true } } } catch (err) { console.error('Failed to create fallback demo document:', err) } if (cancelled) return } else { // Account onboarding is installed once through synced .reams state; // an intentionally empty account vault must not regenerate demos. demoFallbackAttempted.current = true } }
if (!cancelled) { setInitialized(true) } }
void bootstrap().catch(err => { console.error('Failed to restore workspace:', err) if (!cancelled) setInitialized(true) })
return () => { cancelled = true } }, [initialized, pluginApp, searchInitialized, onStage])
if (!initialized) return null
return ( <AppShell documentManager={documentManager} app={pluginApp} editorMode={state.editorMode} onToggleMode={handleToggleMode} saveStatus={visibleSaveStatus} isConcurrentTabOpen={isConcurrentTabOpen} onSaveStatusChange={setSaveStatus} onConcurrentTabChange={setIsConcurrentTabOpen} edition={edition} accountUi={accountUi} syncSettingsSection={syncSettingsSection} onReady={onReady} onStage={onStage} enabledStateStore={enabledStateStore} recentVisits={recentVisits} feedbackModal={feedbackModal} publishContribution={publishContribution} /> )}
function App({ runtime, onReady, onStage }: { runtime: AppRuntime; onReady?: () => void; onStage?: (stage: BootStage) => void }) { useEffect(() => installPageZoomGuard(), []) const pluginApp = runtime.pluginApp const documentManager = runtime.documentManager const backgroundFrames = useRef<number[]>([]) const activationGeneration = useRef(0) const markReady = useCallback(() => { onReady?.() const generation = activationGeneration.current for (const frame of backgroundFrames.current) cancelAnimationFrame(frame) backgroundFrames.current = [] // RAF runs before paint. A second frame permits the initial visible frame // to paint; generation checks also fence StrictMode replay and teardown. const first = requestAnimationFrame(() => { if (generation !== activationGeneration.current) return const second = requestAnimationFrame(() => { if (generation === activationGeneration.current) { if (new URLSearchParams(location.search).has('benchBreakdown')) performance.mark('reams:background:after-frame') runtime.startBackgroundWork() } }) backgroundFrames.current.push(second) }) backgroundFrames.current.push(first) }, [onReady, runtime])
const [vaultReady, setVaultReady] = useState(false) const [vaultError, setVaultError] = useState<string | null>(null) const [vaultRetry, setVaultRetry] = useState(0) useEffect(() => { window.__reamsApp = pluginApp return () => { if (window.__reamsApp === pluginApp) delete window.__reamsApp } }, [pluginApp]) useEffect(() => { return bindTextFirstPageLifecycle(documentManager, () => pluginApp.flushAllDocumentSync()) }, [pluginApp])
// Reattach before opening in the same effect: StrictMode cleanup closes the // runtime, so the replay must reopen only after reattaching it. useEffect(() => { activationGeneration.current++ runtime.reattach() const controller = new AbortController() async function initVault() { const traceBoot = new URLSearchParams(window.location.search).has('benchBreakdown') const mark = (step: string) => { if (traceBoot) performance.mark(`reams:boot:${step}`) } return runtime.openVault({ signal: controller.signal, onStage, mark }) } initVault().then(completed => { if (completed && !controller.signal.aborted) { setVaultError(null) setVaultReady(true) } }).catch(err => { if (controller.signal.aborted) return console.error('Failed to initialize vault:', err) if (!controller.signal.aborted) { setVaultError(err instanceof Error ? err.message : String(err)) } }) return () => { controller.abort() activationGeneration.current++ for (const frame of backgroundFrames.current) cancelAnimationFrame(frame) backgroundFrames.current = [] runtime.close() } }, [documentManager, pluginApp, runtime, vaultRetry, onStage])
useEffect(() => runtime.preferences.subscribe(key => { if (key === 'reams:ribbon-config') pluginApp.reamsRibbon.reloadConfig() if (key === 'reams:status-bar-config') pluginApp.reamsStatusBar.reloadConfig() if (key === 'reams:formatting-toolbar-config') pluginApp.reamsFormattingActions.reloadConfig() }), [runtime, pluginApp])
useEffect(() => { if (!runtime.repositoryDiagnostics) return return registerRepositoryInvariantProvider(runtime.repositoryDiagnostics) }, [runtime.repositoryDiagnostics])
// #489: register perf diagnostics bridge for agent-operable performance testing useEffect(() => { return registerPerfDiagnostics() }, [])
// #406: register a content-type invariant provider so browser tests can // provision binary files + read the active view type without dynamic-importing // Reams internals from a page.evaluate. useEffect(() => { return registerContentTypeInvariantProvider({ provisionBinaryFile: async (name, mimeType, bytes) => { try { const file = await pluginApp.vault.createBinary(name, bytes, mimeType) const url = pluginApp.vault.getUrlForPath(file.path) return url ?? null } catch (err) { console.error('provisionBinaryFile failed:', err) return null } }, getActiveViewType: () => { const el = document.querySelector('.main-area-leaf') return el?.getAttribute('data-reams-view') ?? null }, provisionMarkdownFile: async (name, content) => { try { const existing = pluginApp.vault.getFileByPath(name) if (existing) { await pluginApp.vault.modify(existing, content) return pluginApp.vault.getUrlForPath(existing.path) ?? null } const file = await pluginApp.vault.create(name, content) const url = pluginApp.vault.getUrlForPath(file.path) return url ?? null } catch (err) { console.error('provisionMarkdownFile failed:', err) return null } }, // #411 invariant support — see ContentTypeInvariantProvider. getUrlForFilePath: (path) => pluginApp.vault.getUrlForPath(path) ?? null, provisionForeignFileDoc: async (name, content) => { try { // Create the file doc directly via DocumentManager but do NOT // call addFileToFolder — so from the vault's FileIndex // perspective the URL is "foreign" until the router adopts it. const fileUrl = documentManager.createFile(name) // Write content synchronously via the repo handle. if (content) { const repo = pluginApp.reamsDocumentManager await repo.updateContent(fileUrl, content) } return fileUrl } catch (err) { console.error('provisionForeignFileDoc failed:', err) return null } }, }) }, [pluginApp])
// #408: register a host-contract invariant provider so browser tests can // assert that workspace.activeEditor is null for non-markdown leaves, // workspace.getActiveFile() still returns the TFile, and editor-context // commands are unavailable. Production code paths are unchanged — this // adapter only reads the existing state. // // Probe-command lifecycle: any `editorCallback` commands planted by // `registerEditorContextProbeCommand` are tracked in `plantedProbeIds` // (closed over by this effect) and torn down when the provider is // unregistered (StrictMode unmount, HMR, app teardown). This prevents // synthetic probes from accumulating in the live `CommandRegistry` — // which would otherwise leak into the command palette across sessions // because `addCommand` mutates the same registry the production UI // reads from. useEffect(() => { const probePluginId = 'reams-host-contract-probe' const plantedProbeIds = new Set<string>()
const unregister = registerHostContractInvariantProvider({ getWorkspaceActiveEditor: () => { const editor = pluginApp.workspace.activeEditor if (!editor) return null return { file: editor.file ? { path: editor.file.path } : null } }, getWorkspaceActiveFile: () => { const file = pluginApp.workspace.getActiveFile() return file ? { path: file.path } : null }, isCommandAvailable: (commandId: string) => { const cmd = pluginApp.commands.getCommand(commandId) if (!cmd) return false // Mirror the hotkey dispatcher's editorContext construction in // `App` (see the HotkeyManager wiring in `src/plugin/app.ts`) so // this diagnostic reports the same availability the runtime would // resolve when firing a hotkey. const activeEditor = pluginApp.workspace.activeEditor const editorContext = activeEditor ? { editor: activeEditor.editor, view: activeEditor } : null return pluginApp.commands.canExecuteCommand(cmd, editorContext, { notifyUser: false }) }, registerEditorContextProbeCommand: (commandId: string) => { // Plant a synthetic editorCallback command under a fixed pseudo- // plugin id so the browser suite can assert editor-context gating. // Reams' default plugin set ships no editorCallback commands, so // the suite needs one to test against. // // True idempotency: if a probe with this full id is already // planted, return the existing id without re-registering. We // track plantings in `plantedProbeIds` so the effect-cleanup // teardown is complete and accurate; `addCommand` mutates the // shared registry and would otherwise leave probes behind. const fullId = `${probePluginId}:${commandId}` if (plantedProbeIds.has(fullId)) return fullId pluginApp.commands.addCommand(probePluginId, { id: commandId, name: `[#408 probe] ${commandId}`, editorCallback: () => { // No-op. The probe exists purely to drive availability assertions. }, }) plantedProbeIds.add(fullId) return fullId }, })
return () => { unregister() // Tear down every probe planted while this provider was live. // `removeCommand` is idempotent in the registry and notifies // command-removal listeners, so the command palette / UI snapshot // stays consistent. for (const fullId of plantedProbeIds) { pluginApp.commands.removeCommand(fullId) } plantedProbeIds.clear() } }, [pluginApp])
if (!vaultReady) { // mount() keeps the app element hidden until onReady. Send an async vault // failure through its boot boundary instead of rendering an unreachable // Retry button behind the loading screen. if (vaultError && onReady) throw new Error(`Could not open vault state: ${vaultError}`) if (!vaultError) return null return ( <div style={{ color: 'var(--text-secondary)', padding: '2rem', textAlign: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', }}> {vaultError ? ( <div role="alert"> <p>Could not open this vault’s Reams state: {vaultError}</p> <button type="button" onClick={() => { setVaultError(null); setVaultRetry(value => value + 1) }}>Retry</button> </div> ) : 'Opening Reams…'} </div> ) }
return ( <LayoutProvider preferences={runtime.preferences}> <SearchProvider documentManager={documentManager} vault={pluginApp.vault} app={pluginApp} searchContribution={runtime.searchContribution} stateRoot={runtime.stateRoot} backgroundReady={runtime.backgroundReady}> <AppContent pluginApp={pluginApp} documentManager={documentManager} edition={runtime.edition} allowDemoFallback={runtime.edition === 'classic' || runtime.vaultRootStorageKey !== ATPROTO_VAULT_ROOT_URL_KEY} accountUi={runtime.accountUi} syncSettingsSection={runtime.syncSettingsSection} enabledStateStore={runtime.enabledStateStore} recentVisits={runtime.recentVisits} onReady={markReady} onStage={onStage} feedbackModal={runtime.feedbackModal} publishContribution={runtime.publishContribution} /> </SearchProvider> </LayoutProvider> )}
export default App