# Markdown file editor Users read and edit Markdown files in the workspace viewer, search their text, switch between Edit, Source, and Preview, and attach comments to selections. This map defines the repeatable critical user journey (CUJ) for editor changes and the recording protocol for comparing revisions. ## Sub-features - `read`: headings, lists, tasks, tables, links, quotes, and fenced code render. - `edit-save`: typing preserves the caret and autosave survives reopening. - `find`: search finds edited content without typing into the document. - `modes`: Edit, Source, and Preview retain the same saved content. - `review`: selected text anchors a comment; an unsent draft remains editable. - `responsive`: desktop and mobile show the file and retain edits and drafts. - `load`: fixed mixed-content documents expose parsing and rendering costs. - `offline`: unsaved edits remain guarded when the workspace cannot save. - `live-refresh`: an open file shows saves from other sessions and processes in place, keeping the caret; unsaved edits raise a conflict instead. - `large-files`: files above 256 KiB open in Preview, and Edit opens editable source; files above 1 MiB open directly in source. Truncated files remain read-only, with full-file download available. ## How to get to it (user POV) - `files`: open a session's Workspace panel, select Files, then a Markdown file. - `changes`: select an existing changed Markdown file, then exit diff to edit it. Deleted files stay in diff; select another file to continue editing. - `direct-link`: open a session URL with a selected Markdown file. - `chat-link`: follow a Markdown file link in a message; a line citation opens Source at that line, rather than the rich editor, and a range (`:12-18`, `#L12-L18`) highlights every cited line. Right-clicking or long-pressing the link offers Copy path, which copies its path (absolute when the workspace root is known). - `tool-row`: click the path on an agent's Read, Write, or Edit row for a Markdown file, including one the agent just created; absolute paths open too. - `agent-api`: ask the agent to use `ui_open_file` with a workspace-relative Markdown path, or call the [file-opening endpoint](../docs/agent-ui-api.md). Only current session viewers (the selected pane in grids) open the file; background requests are not replayed. - `document-link`: click a web or relative file link directly in the editor; Alt-click or move the caret with the keyboard to edit it and use its link popup. - `mobile`: use the session Files surface or a file link at a phone width. ## Driving it with the repro environment Preconditions: follow the [Verify Omnigent skill](skills/verify-omnigent/SKILL.md). Build the exact checkout and use its frozen dependency lock. The browser-contract journeys below need no server: they run the production SPA with a sealed mock backend, a fresh browser context, and a fixed Markdown fixture. Server-backed journeys require an isolated verification instance. ### Standard comparison journey `tests/browser_ui/files/test_markdown_editor_journey.py::test_markdown_editor_review_journey` starts at `direct-link` and performs these actions in order: 1. Read the heading, ordered list, table, and original code block. 2. Create another Python code block and type two lines; verify the caret stays inside it. Change the original block's language with the pointer and the new block's language with the keyboard, then verify autosave submits the changes. 3. Reload and verify the edit and fence language are returned by the mock filesystem. 4. Find the edited text and verify exactly one match. 5. Switch through Source, Preview, and Edit, retaining the changed text. 6. Resize from desktop to phone and back, retaining the saved content. 7. Double-click "Review" in the first paragraph, verify the comment anchors to that word, and start an unsubmitted comment; end with its text focused. The fixture is [markdown_editor_journey.md](../tests/browser_ui/files/markdown_editor_journey.md). The test fixes Chromium's viewport and recording size at 1600 x 1000, scale 1, light color scheme, en-US, and UTC. Phone width is 390. Use the same test and fixture revision on both builds. These actions exercise saved-content resizing; they do not claim unsaved-draft preservation across a resize. ### Record before and after Use separate worktrees and evidence directories for each revision. Apply only the same CUJ test/fixture changes to older revisions that lack them. Rebuild each SPA, use the same browser binary and machine, and run comparisons sequentially. Run from each worktree root, changing only `CUJ_EVIDENCE` between runs: ```sh CUJ_EVIDENCE=/tmp/markdown-editor-cuj/before mkdir -p "$CUJ_EVIDENCE" git rev-parse HEAD > "$CUJ_EVIDENCE/revision.txt" git diff --binary > "$CUJ_EVIDENCE/working-tree.patch" sha256sum pnpm-lock.yaml tests/browser_ui/files/markdown_editor_journey.md \ tests/browser_ui/files/test_markdown_editor_journey.py \ > "$CUJ_EVIDENCE/inputs.sha256" pnpm --filter web run build uv run --no-sync pytest \ tests/browser_ui/files/test_markdown_editor_journey.py::test_markdown_editor_review_journey \ --browser-ui-skip-build --browser=chromium --video=on --slowmo=250 \ --screenshot=on --tracing=on --reruns=0 --output="$CUJ_EVIDENCE/review" uv run --no-sync pytest \ tests/browser_ui/files/test_markdown_editor_journey.py::test_markdown_editor_load_journey \ --browser-ui-skip-build --browser=chromium --video=on \ --screenshot=on --reruns=0 --output="$CUJ_EVIDENCE/load" ``` Use `after` for the candidate. Compare corresponding clips at normal playback speed; preserve the waiting period when the UI stalls. Do not splice out waits or use the deliberately slowed review clip as a performance measurement. Attach the videos to the PR with `gh pr edit --attach`, identifying the revision, entry point, fixture, and whether APIs were mocked. Keep artifacts out of Git. `test_markdown_editor_load_journey` repeats the same fixture to roughly 64 and 256 KiB. Each test writes `markdown-load.json` beside its recording: fixture hash, actual bytes, browser version, viewport, editor count, selection-read count, longest main-thread task, and navigation-to-ready time through two animation frames. Readiness waits directly for the visible editor's terminal marker; locator assertions can build a full accessibility snapshot and inflate timings. Results carry `readiness_probe: terminal-marker`; older results without this field include assertion overhead and must be remeasured for comparisons. The load runs use real time and no slow-motion or simulated clock. Report repeated samples for performance decisions; a single video is illustrative. These runs do not cover every Markdown shape or establish the limits for every machine. ### Complementary correctness journeys - `files`, `read`, and `modes`: `tests/e2e_ui/files/test_markdown_rich_rendering.py::test_markdown_renders_rich_constructs_and_source_toggle` opens from Files and checks rich constructs, reflow, copying, and Source. - `files`, oversized read-only content: `tests/e2e_ui/files/test_right_panel.py::test_file_viewer_download_saves_the_complete_file` includes a single-line Markdown file beyond the server read cap. Its loaded prefix must render read-only, and downloading must preserve the entire file. - `direct-link`, `large-files`, `edit-save`: `tests/e2e_ui/files/test_file_autosave.py::test_large_markdown_source_autosaves` opens a 512 KiB file in Preview, chooses Edit, edits it in source, verifies every saved byte through the real server, finds its final line, and shrinks it below the limit. Source stays mounted until reload, which then restores the rich editor. - `chat-link`, `large-files`, `mobile`: `tests/browser_ui/files/test_file_line_navigation.py::test_large_markdown_citation_and_unsaved_resize` opens in Preview, follows a final-line citation into source, rejects saves, and preserves the unsaved source editor through phone/desktop resizing and the mobile close guard. - `direct-link`, `edit-save`: `tests/e2e_ui/files/test_file_autosave.py::test_markdown_edit_autosaves` confirms the edit, undo, and redo through fresh filesystem API reads from the real server, then reloads the saved edit. - `direct-link`, `live-refresh`: `tests/e2e_ui/files/test_file_autosave.py::test_open_markdown_follows_other_writers` leaves an idle session's editor open while another process replaces the file and another session saves it. Both edits appear without a reload, and typing at the parked caret saves beside them. With saves failing, a later external change raises the conflict instead of replacing unsaved text, and Keep mine saves it. Edits typed during a save, the pre-save conflict check, undo, and caret mapping are covered by component tests (`MarkdownRichTextViewer.integration.test.tsx`, `MonacoCodeEditor.autosave.test.tsx`, `TipTapEditorHelpers.test.ts`). - `files`, `review`: `tests/e2e_ui/comments/test_markdown_editor_comments.py::test_markdown_rich_text_editor_add_comment` submits a comment and checks its stored anchor. The heading variant is `tests/e2e_ui/comments/test_markdown_editor_comments.py::test_heading_text_anchor_content_excludes_prefix`. `tests/e2e_ui/comments/test_markdown_editor_comments.py::test_repeated_word_keeps_selected_occurrence_after_reopen` checks exact raw endpoints and the highlighted occurrence after save/reopen, including a word split across formatting beside a hidden URL and a plain copy, a bold word in an ordered-list parent followed by a nested ordered list, and a repeated bold word outside a table containing an escaped pipe. `tests/e2e_ui/comments/test_markdown_editor_comments.py::test_mixed_structures_accumulated_comments_after_autosave` edits the mixed fixture, waits for real autosave, then comments on every duplicate through the UI, selecting the nested-list parent last. It checks persisted raw endpoints and every highlight again after a full page reload. This covers accumulated decorations and serialization, unlike single-comment tests or parser-only input matrices. `tests/browser_ui/files/test_markdown_editor_journey.py::test_markdown_comment_boundary_save_reload` checks raw endpoints and highlights after saving/reloading selections ending on a CRLF soft break, hard break, or image next to duplicate text. `test_markdown_existing_comment_after_autosave` checks an existing highlight before clicking it or adding another comment. `test_markdown_source_comment_after_external_edit` reloads externally changed text and checks Source-mode ranges with formatting retained or removed. These browser-contract cases reuse the file journey's mock backend; they do not establish server persistence. Parser input matrices remain in unit tests. - `agent-api`, desktop and phone-sized browser: `tests/e2e_ui/files/test_right_panel.py::test_agent_opens_workspace_file` drives a scripted agent tool call through the runner, server and stream into the real file viewer. Grid targeting and dirty-navigation guards are covered by `web/src/grid/GridSessionPane.test.tsx`; native app/device runs remain manual. - `document-link`: `tests/e2e_ui/files/test_markdown_editor_link_popup.py::test_editor_links_open_with_one_click` opens a table's web link and a relative file link with one desktop click or phone-width tap, preserves drag selection, and checks the source is unchanged. `tests/e2e_ui/files/test_markdown_editor_link_popup.py::test_editor_link_popup_opens_relative_link_from_keyboard` uses Alt-click to place the caret, then opens a relative file from the keyboard-accessible link popup. - `chat-link`, `offline`: `tests/browser_ui/files/test_file_line_navigation.py::test_citation_preserves_offline_markdown_draft_with_diff_preference` exercises a citation, offline editing, and the dirty-navigation guard. Its health polling uses simulated time; it is not a performance sample. - `chat-link` Copy path: the "copying a file link's path" component tests in `web/src/components/blocks/ChatMarkdown.fileLinks.test.tsx` cover openable, missing, and unverified links. Manually long-press a link in the mobile apps and right-click one in the desktop app; neither should open the file. - `changes`: manually open Markdown from Changes, exit diff, edit and save, then re-enter diff and verify the saved edit. The standard CUJ starts from a direct link; it does not establish this entry point's correctness. - `mobile`, unsaved `responsive`: record starting at both desktop and phone widths. Create an unsent comment, resize in both directions, and verify its text, focus, and selection. Repeat with an offline edit and the mobile Back dirty guard. These are separate from the saved-content resize above; the automated regressions are in https://github.com/omnigent-ai/omnigent/pull/10715. Record the server-backed tests through `verify-env run`, using `--ui-skip-build`, `--video=on`, `--slowmo=250`, `--screenshot=on`, and `--reruns=0`. Keep each test's fixture and viewport unchanged between revisions. List any entry point not run instead of treating one comparison clip as complete feature verification. ## Gotchas - Comment offsets remain raw-source ranges; the quote is rendered text. A selection such as `wo**rd**` can span markup, and its minimal source range ends at the last selected character, before the closing `**`. Comments whose raw ranges span markup are remapped through the same parser in Edit, Source, and Diff views, preferring an exact stored range, then the closest verified source occurrence. Unsupported or oversized documents retain stored ranges in Source mode. If the remapping module cannot load, Source and Diff use the legacy text search. Equal-length source ranges still use legacy search after external edits, including selections entirely inside bold or italic delimiters. This does not repair previously mis-saved offsets or guarantee tracking through arbitrary subsequent edits. - Older clients can display a formatted comment on a plain literal copy. Refresh those tabs to load the source-map behavior; this display difference does not overwrite the comment's saved offsets. - The large-file limits use UTF-8 bytes, not JavaScript string length. Above 256 KiB the rich editor is unavailable regardless of the saved view preference: the file opens in Preview (or source when Source is the saved preference, or a comment link or line citation needs source), and Edit opens source for that visit only, without changing the saved preference. Above 1 MiB, or when the file is truncated, it opens in source with no Preview. The routing stays fixed for that file visit so an autosave cannot replace an editor with newer unsaved edits. Reopen another file and return, or reload, to reassess. This is an initial-load guard, not a live cap on edits or external updates. - The large-file Preview reuses its rendered tree across re-renders, skips raw-HTML parsing when the document has none, and skips layout of off-screen blocks; a single paragraph spanning tens of thousands of lines still parses slowly in GFM. - File editing uses TipTap. Chat Markdown, read-only Preview, and Source use different rendering paths; speeding up one does not validate the others. - Task-list tokenization and table-start detection bound the source passed to TipTap 3.31.4. `web/src/shell/tiptapBoundedTokenizers.test.ts` checks upstream token compatibility and line-splitting volume using the CUJ fixture. Remove these bounds when upstream avoids the repeated whole-remainder scans. - Footnote references (`[^1]`) render as superscripts when the file defines them, and definition lines as read-only source text; edit definitions in Source. Typing, pasting or Enter over a selected footnote keeps it; Backspace or Delete removes it. Saves keep footnotes and the final newline, except a reference inside bold or italic moves just outside those marks; reference syntax in link text stays literal, so the link is kept. - Reference-style links (`[text][label]`, `[text][]`, `[text]`) save in that form while their URL, title and label still match a definition; otherwise they save inline. Link definition lines show read-only, like footnotes. - Saves write top-level blocks the user did not change from the file's source, byte-for-byte; only edited blocks are re-serialized, so an edit inside a list or table rewrites that whole list or table. Changed definitions, or a kept block that would read differently beside its new neighbour (indented code after a list, an unclosed fence), re-serialize the whole file, so an unrelated block's syntax can still change after a deletion. An edited block is separated from its neighbours by exactly one blank line. `web/src/shell/markdownSourceBlocks.ts`. - Load measurements do not establish typing latency. Dirty detection compares immutable editor documents; autosave writes edited blocks only. Changes that serialize identically become clean when autosave checks them. - Setext-heading detection uses a line scanner with marked 17 interruption rules. Compatibility tests compare it with upstream; remove the workaround when upstream handles long lines without exhausting the regex stack. - Browser-contract saves update an in-memory mock filesystem. A successful reload there does not prove server/runner persistence or external-file edits. - Live refresh polls only an on-screen UTF-8 file up to 256 KiB, every two seconds; hidden browser tabs pause. Larger and binary files, and the diff view, refresh when shown again or when the session finishes a turn. - Every autosave re-reads the file first and raises the conflict if another writer changed it. Saves are still last-writer-wins: a write landing between that read and the save can be overwritten. - A failed refresh keeps the last loaded content, shows a notice, and re-checks every 30 seconds; saving a file that was deleted meanwhile recreates it. - An external update replaces the document as `setContent` would, maps the caret through the changed range, and clears undo history, as Monaco does. A caret inside that range, or between separately changed regions, moves to its end. Monaco keeps the caret's line and column instead of mapping it. - Desktop and mobile can have hidden viewer instances on older revisions. Interact with the visible viewer and report total editor count separately. - Resizing saved content is not evidence that unsaved edits or comments survive. - A phone-sized Chromium viewport is not native iOS keyboard, Safari, touch, or Android verification. Record those gaps explicitly when relevant. - The 64/256 KiB probes are report-only. Readiness checks can fail, but there is no machine-dependent latency assertion and no silent plain-text fallback.