Skip to content

[pull] develop from marktext:develop - #49

Merged
pull[bot] merged 9 commits into
code:developfrom
marktext:develop
Jun 8, 2026
Merged

[pull] develop from marktext:develop#49
pull[bot] merged 9 commits into
code:developfrom
marktext:develop

Conversation

@pull

@pull pull Bot commented Jun 8, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

Jocs and others added 9 commits June 9, 2026 01:14
…aps (#4407)

* test(parity): muya engine xfail tests for #4406 gaps PG1,3,5,6,7,8,9,10,11,12,13

Encode 10 of the 15 confirmed muyajs→@muyajs/core functional-parity gaps as
vitest `it.fails` regression tests in the engine. Each asserts the desired
pre-migration behaviour and fails on develop today (counted as a pass by
`it.fails`); when a fix lands the test passes and `it.fails` errors, forcing the
fixer to drop the marker. Every test name is prefixed with its gap id (`PGn:`)
so fix PRs can grep + flip it.

- PG1  selection-change lacks block affiliation / ancestor type
- PG3  autoCheck preference not consumed (task-list cascade lost)
- PG5  binary/bitmap clipboard image paste not persisted via imageAction
- PG6  pasted image FILE bypasses imageAction (insert preference ignored)
- PG7  export links core CSS from CDN instead of inlining it
- PG8  exported headings carry no slug id (dead TOC anchors)
- PG9  copyAsRich writes HTML source not rich text (no copyAsRich path)
- PG10 preview-image not emitted (Space on a selected image)
- PG11 heading-copy-link not emitted (hover-to-copy affordance gone)
- PG12 hideLinkPopup not consumed (link hover popover not gated)
- PG13 insertParagraph anchors to outermost not immediate nested block

Allow the uppercase `PG` test-title prefix through antfu's
`test/prefer-lowercase-title` rule (scoped to spec files) so the greppable
gap-id naming coexists with the lint rule; all other test titles still must
start lowercase. Suite stays GREEN: 512 pass + 20 expected-fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(parity): desktop e2e xfail tests for #4406 gaps PG1,2,14,15

Add Playwright `test.fail()` regression specs for the desktop-side parity gaps.
Each runs headless and currently fails (the gap), which `test.fail()` counts as
a pass; removing `test.fail()` flips the entry green once fixed. Test names are
prefixed with the gap id (`PGn:`) for grep-and-flip.

- PG1  Paragraph menu does not check the current block type (affiliation gap;
       caret placed directly in an H1 content span — the engine-unit half lives
       in paritySelectionChange.spec.ts)
- PG2  WYSIWYG caret not restored after a source-mode edit (handleFileChange
       drops muyaIndexCursor; no index→path cursor conversion in the engine)
- PG14 first undo after exiting source mode does not revert the bulk source-mode
       change as one step
- PG15 undo back to on-disk content leaves the tab marked unsaved (synthetic
       history id never re-matches the saved id)

Undo is driven through the same `mt::editor-edit-action` IPC the Edit › Undo
menu uses. Suite stays GREEN (4 expected-fail).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(parity): scoreboard index + manual-QA checklist for #4406 gaps

Add the visible "how many gaps remain" board (PARITY_SCOREBOARD.md): a table of
all 15 muyajs→@muyajs/core parity gaps → severity → test location(s) → status
(all xfail now), plus the flip-to-green workflow for fix PRs.

Add PARITY_QA.md: precise manual-QA checklists (exact steps + expected vs
current result) for the two gaps that cannot be driven headless —
PG4 drag-drop image insertion and the OS-clipboard/screenshot half of
PG5 binary image paste.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(parity): destroy() muya instances in afterEach to detach DOM listeners

Address Copilot review: the parity engine specs' cleanup only removed the host
DOM node, never calling `muya.destroy()`, so the `document`-level keydown/click
listeners selection registers during init leaked across tests — a flake / order-
dependence risk as the suite grows. Track booted Muya instances and `destroy()`
each in afterEach (detaches all DOM events + removes the node).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (PG13) (#4408)

Restore the legacy immediate-anchor capability for `insertParagraph`. The
engine method always resolved `_outmostBlockAtCursor()`, so inside a nested
list item / blockquote the new paragraph landed adjacent to the OUTERMOST
container instead of as an inner sibling next to the immediate block.

Add a third `outMost` flag (default `false`): when false, anchor to the
IMMEDIATE block-level parent of the active content leaf (mirrors the legacy
`getAnchor`/`getParent` path used by the context-menu "Insert Paragraph
Before/After" action); when true, keep the outermost-container behaviour used
by "Create Paragraph Below" (which passes `outMost=true` explicitly). Top-level
paragraphs are unaffected since the immediate parent and outermost block
coincide.

Flips parity scoreboard PG13 (parityInsertParagraphNested.spec.ts, 2 specs
`it.fails` -> `it`) and adds a blockEditing.spec.ts regression locking the
`outMost=true` path.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(muya): cascade task-list checkbox state when autoCheck is on (PG3)

The autoCheck option was merged into muya.options (muya.ts setOptions) but
no engine code consumed it, so the legacy muyajs task-list cascade was lost
(parity gap PG3 of #4406).

Restore it in the task-list checkbox handler: a user toggle now sets every
descendant task item to the same checked state and re-derives each ancestor
(checked iff all its siblings are checked), gated on muya.options.autoCheck.
Cascaded items mutate state through the TaskListItem.checked setter so each
change still dispatches its OT replace op; their checkboxes sync via syncDom.

Flips the parityAutoCheck scoreboard specs from it.fails to it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(muya): gate link hover popover on hideLinkPopup (PG12)

The hideLinkPopup option was merged into muya.options but the link-hover
emitter (linkMouseEvents.ts#overHandler) emitted muya-link-tools
unconditionally, so the popover always appeared on hover even when the user
set hideLinkPopup: true (parity gap PG12 of #4406).

Read muya.options.hideLinkPopup live in overHandler and return early when
set, mirroring legacy muyajs eventHandler/mouseEvent.js. Optional chaining
keeps the existing options-less test mock working.

Flips the parityHideLinkPopup scoreboard spec from it.fails to it; the
positive control stays green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(muya): expose block affiliation in selection-change (PG1)

The `selection-change` payload only carried flat caret/range info, so the
desktop Paragraph/Format menu state builder (`createApplicationMenuState`)
could not reconstruct block context: no ancestor `affiliation` chain, and
the engine blocks expose `blockName` rather than muyajs's `type` /
`functionType`. As a result the Paragraph-menu check marks, Loose/Task-list
toggles, table / code-fence detection and the in-code Format-menu disable
were all dead.

Re-derive the legacy `selectionChange` block-context from the @muyajs/core
block tree in a focused `selection/affiliation.ts` helper and add three
fields to the `selection-change` payload (existing fields untouched):

- `affiliation` — shared ancestor PARAGRAPH-type chain (outermost-first),
  each entry carrying the markdown `type` (`p`, `h1`…`h6`, `ul`, `ol`, `li`,
  `pre`, `figure`, `blockquote`) plus list context (`listType`,
  `listItemType`, `isLooseListItem`).
- `anchorBlockInfo` / `focusBlockInfo` — per-endpoint content-leaf info:
  `type` (always `span`) + `functionType` (`codeContent`, `cellContent`,
  `languageInput`, `paragraphContent`).

Flips parity scoreboard PG1: the two `it.fails` PG1 tests are now real
passing tests, plus two new tests pin the per-endpoint code-content info
and list affiliation context. Desktop adapter consumption is wave 2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(muya): derive list-item affiliation context from the parent list (PG1)

Address Copilot review on #4410:

- `isLooseListItem` was read from a list-item block's own `meta.loose`, but
  `list-item` / `task-list-item` carry no `meta` — the loose/tight flag lives
  on the parent list (`bullet-list` / `order-list` / `task-list`). It was
  therefore always `false` for `li` entries, so the desktop "Loose list item"
  state could never enable.
- `listItemType` for an `li` was mapped from the item's own `blockName`
  (`list-item` → `bullet`), but both bullet and ordered lists use the same
  `list-item` block, so ordered-list items were misclassified as `bullet`.

Walk from a list-item up to its enclosing list block and read both the
discriminator (`bullet` | `order` | `task`) and `meta.loose` from there. Add
tests pinning ordered-list-item classification and loose-list detection on
both the list and item entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…yAsRich (PG5/PG6/PG9) (#4411)

* feat(muya): add copyAsRich clipboard path for rich-text copy (PG9)

Expose a public Muya.copyAsRich() backed by a new 'copyAsRich' copyType
branch in the clipboard copyHandler. Unlike copyAsHtml (which blanks
text/html and drops markup into text/plain as literal source), copyAsRich
mirrors the 'normal' branch: rendered HTML in the text/html slot so a
rich-text target (Word, email, contenteditable) renders formatting, with
the markdown source in text/plain.

Restores the legacy @muyajs 'Copy as Rich Text' behaviour (PG-COPYRICH).
The desktop COPY_PASTE_METHOD_MAP.copyAsRich remap is wave 2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(muya): route pasted images through imageAction, restore bitmap paste (PG5/PG6)

On paste, both the resolved clipboard FILE path (PG06) and an in-memory
bitmap (PG05, screenshots / browser 'Copy Image') now flow through the
embedder's options.imageAction so the insert preference (copy-to-assets /
upload / keep-path) applies and a portable src is written — instead of
linking the raw on-disk path verbatim (PG06) or inserting nothing (PG05).

- types.ts: add IMuyaOptions.imageAction({ src, alt, title }) => Promise<string>
  and the IImageActionState shape, ported from legacy @muyajs.
- utils/paste.ts: add getClipboardImageFile (reads clipboardData.files /
  items for an image File) and readFileAsDataURL (FileReader.readAsDataURL,
  falling back to Blob.arrayBuffer + btoa for the chrome70 target / Node tests).
- clipboard: snapshot the image File synchronously before the first await,
  add tryPasteImage (file path then binary) + insertImageSrc (imageAction
  routing); split the raw markdown splice out as insertImageText.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(parity): flip PG5/PG6/PG9 to passing, update scoreboard + QA

The clipboard image-paste + copyAsRich fixes land the engine behaviour, so
convert the parity specs from it.fails to it:
- parityImagePaste.spec.ts: PG5 (binary paste -> imageAction) + PG6 (x2,
  resolved path -> imageAction).
- parityCopyAsRich.spec.ts: PG9 (x2, copyAsRich sets text/html=html and
  text/plain=text).

Mark PG5/PG6/PG9 green on PARITY_SCOREBOARD.md (gaps remaining 12/15) and
update PARITY_QA.md § PG5: the engine half is implemented; only the
OS-clipboard delivery (real bitmap, macOS screencapture) stays manual.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(muya): guard readFileAsDataURL fallback; sync scoreboard mechanism (review)

Address Copilot review on PR #4411:
- readFileAsDataURL: guard the non-FileReader fallback for missing
  arrayBuffer/btoa (resolve '' instead of throwing out of Promise<string>)
  and base64-encode the bytes in 0x8000 chunks so a large blob avoids a huge
  per-byte intermediate string.
- PARITY_SCOREBOARD: update the Mechanism column for PG5/PG6/PG9 from
  it.fails to 'passing it' now that those parity specs are flipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nchors (PG7/PG8) (#4412)

* fix(muya): inline export CSS for offline self-contained HTML (PG7)

MarkdownToHtml.generate linked github-markdown-css, katex, and prism from
external CDNs via <link> tags, so a saved standalone .html (and any
offline / CSP-restricted / air-gapped viewer) rendered unstyled. The
legacy muyajs ExportHtml inlined those three core stylesheets as <style>
blocks via ?inline imports, producing fully self-contained output.

Restore that: inline the three base stylesheets by default. Keep the CDN
shell available behind generate({ inlineStyles: false }) for callers that
want a lighter document. Add github-markdown-css to muya deps (katex /
prismjs already present) and enable Vitest CSS processing (css: true) so
the inlined ?inline imports resolve to real content under test — without
it Vitest stubs every CSS import to an empty string and would silently
mask this regression.

Flips parityExportHtml PG7 specs (it.fails -> it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(muya): inject github slug ids onto exported headings (PG8)

@muyajs/core rendered exported headings via stock marked with no id, so
in-document [TOC] / getHtmlToc `<a href="#slug">` anchors pointed at
nonexistent targets — dead TOC links in exported HTML/PDF. The legacy
muyajs export emitted `<hN id="{slug}">` using the same slugger as the
TOC.

Inject a github-compatible slug id (reusing the engine's existing
generateGithubSlug) onto every h1..h6 in the export DOM, deduplicating
collisions with a `-N` suffix. Scoped to MarkdownToHtml's export path
only — renderToStaticHTML (the CommonMark/GFM conformance renderer) is
left untouched, so spec conformance output does not change.

Flips parityExportHtml PG8 specs (it.fails -> it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(muya): dedup exported heading ids by full slug (review)

_injectHeadingIds deduplicated per base slug only, so a heading whose
text already matched an earlier `-N` slug collided (e.g. `heading`,
`heading`, `heading-1` emitted `heading-1` twice), breaking the anchor it
was meant to fix. Switch to a Slugger-style "seen full slug" set,
incrementing the suffix until the whole candidate id is unused, and seed
it with any pre-existing heading ids. Add a chained-collision regression
test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#4414)

* feat(muya): emit preview-image on Space for a selected image (PG10)

Legacy muyajs dispatched `preview-image` { data: src } from keyboard.js
when an image was selected and the user pressed Space; the desktop
renderer opened the full-screen SimpleImageViewer. @muyajs/core never
emitted it — the image-selected keydown handler only acted on
Backspace/Delete/Enter, so Space fell through to native handling
(inserting a literal space) and the desktop subscription was dead.

Restore the emit: when an image is selected and Space is pressed, the
selection keydown handler resolves the image src the same way the
Cmd/Ctrl-click preview path does (token src via getImageSrc, falling
back to the rendered <img>'s src) and emits `preview-image` { data: src },
calling preventDefault so no space is inserted.

Extracts the image keydown handler and the preview resolution into
private methods to keep _listenSelectActions under the max-lines gate.

Flips the PG10 parity scoreboard specs (it.fails -> it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(muya): add heading hover-copy affordance + heading-copy-link (PG11)

Legacy muyajs rendered a hover affordance (i.icon.ag-copy-header-link)
on each heading and dispatched `heading-copy-link` { key } on click; the
desktop renderer copied the heading's GitHub slug/anchor to the
clipboard (copyGithubSlug, which looks the key up against listToc by
slug). @muyajs/core rendered no such affordance and never emitted the
event, leaving copyGithubSlug unreachable.

Restore both:

- New HeadingCopyLink attachment block (mu-copy-header-link), appended to
  every atx/setext heading via appendAttachment (same mechanism as the
  task-list checkbox). It carries no document state, so markdown/HTML
  round-trip is unaffected. On click it emits
  `heading-copy-link` { key } and stops propagation.
- The emitted key is the heading's stable slug — the SAME value getTOC()
  exposes as ITocItem.slug — so the host can resolve it back to a TOC
  entry. stableSlug is exported from state/getTOC for reuse (no new
  circular dep: getTOC only type-imports blocks).
- Engine-level hover-reveal CSS in blockSyntax.css: the affordance sits
  in the left gutter, hidden by default and revealed on heading hover.

Flips the PG11 parity scoreboard specs (it.fails -> it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(muya): make heading copy-link affordance accessible (review)

Addresses the e2e a11y scan failure (critical image-alt) and Copilot
review on the heading copy-link affordance:

- The decorative icon <img> now carries alt="" — resolves the axe-core
  `image-alt` critical violation that failed the muya e2e a11y host scan.
- The affordance is now a proper button: role="button", tabindex="0",
  and an i18n aria-label/title, so it is discoverable and focusable by
  assistive tech.
- Keyboard activation (Enter / Space) mirrors click, so the focusable
  control is operable without a pointer.
- Clarify (no behavior change) that the document-level image keydown
  handler is gated solely by `selectedImage`, which is only ever set by
  an in-editor image click and cleared on any document click — matching
  the legacy muyajs scope. A focus-in-editor gate was rejected because a
  selected image intentionally blurs the contenteditable (activeElement
  becomes <body>), so it would defeat the Space-preview feature.

Adds PG11 spec coverage for the button semantics + Enter/Space activation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(muya): restore drag-and-drop image insertion (PG4)

The @muyajs/core rewrite (#4406) shipped without any DnD handler, so
dropping an image into the document was a no-op — parity gap PG4. Port
the legacy muyajs dragDrop/dragDropCtrl behaviour as an engine-level,
embedder-agnostic handler.

`attachDragDropImageHandlers(muya)` binds dragstart/dragover/drop on the
editor container (and dragleave for the ghost) via
`eventCenter.attachDOMEvent`, so cleanup rides on
`muya.destroy() → detachAllDomEvents()`. It is wired in `Editor.init()`
alongside `attachLinkMouseHandlers`.

Two drop paths mirror the legacy controller:
- web-link image (`text/uri-list`): verify it is an image (extension or
  content-type sniff) then insert `![](url)`.
- local image FILE (`dataTransfer.files`): resolve the file to a path via
  the new embedder `getPathForFile` hook, insert a `![loading-id](path)`
  placeholder, persist it through the new `imageAction` option (the same
  `{ src, alt, title }` contract the imageEditTool plugin consumes), then
  swap in the returned src.

Two optional `IMuyaOptions` hooks are added (mirroring the existing
`clipboardFilePath`): `imageAction` and `getPathForFile`. The engine
stays free of `window.electron`; the desktop wires these in wave 2.

Tested in `dragDropImage.spec.ts`: happy-dom provides a fully working
`DataTransfer` (items.add / getAsString / files) and fires getAsString
synchronously, so a synthetic `drop` event drives the real handler
end-to-end — asserting both drop paths and the no-op-off-target case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(parity): mark PG4 engine half automated, note wave-2 desktop wiring

PG4 (drag-drop image insertion) was a manual-QA-only entry because real
drag gestures are hard headless. The engine handler now has a synthetic-
DataTransfer unit test, so update the scoreboard and QA checklist:

- PARITY_QA.md § PG4: describe what is now automated (both drop paths via
  the live handler), keep the OS-integration steps manual, and document
  the desktop wave-2 wiring needed for the local-file persistence path
  (pass `imageAction` / `getPathForFile` into the Muya constructor).
- PARITY_SCOREBOARD.md: point PG4 at the new spec, flag the engine half
  fixed, and drop the remaining-gaps count 15 → 14.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(muya): address PG4 drag-drop review (ghost CSS, no-imageAction, web-image gating)

Copilot review on #4413:

- Add the `#mu-dragover-ghost` CSS rule (position/height/background) to
  `assets/styles/index.css`. The new engine had no ghost style, so the
  drop indicator was invisible — legacy muyajs shipped this rule.
- When no `imageAction` hook is configured, insert a clean `![name](path)`
  with the raw path verbatim instead of a `![loading-id](path)` placeholder
  that would never be swapped (it persists only when imageAction resolves).
  Matches the documented `imageAction` contract and imageEditTool's
  direct-replacement behaviour.
- Gate the web-link path on the legacy "image dragged from a browser"
  signature — `text/uri-list` + `text/html` and NO `text/plain` — in both
  `dragover` and `drop`. A plain hyperlink drag (uri-list + text/plain) is
  now left to the browser instead of being intercepted and swallowed by
  `preventDefault()`.

Tests updated: web-link drags use the realistic (uri-list + html) payload,
plus new cases for the no-imageAction clean insert and the plain-hyperlink
pass-through.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… heading-link, source cursor, saved indicator) (#4415)

* fix(desktop): map copyAsRich to the engine copyAsRich method (PG9)

The legacy "Copy as Rich Text" command was remapped to copyAsHtml, which
blanks text/html and puts the HTML source into text/plain, so pasting into
Word/email yielded raw HTML markup as literal text. #4411 added a real
Muya.copyAsRich() that writes rendered HTML to text/html and plain text to
text/plain; point COPY_PASTE_METHOD_MAP.copyAsRich at it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(desktop): wire heading-copy-link to copyGithubSlug (PG11)

#4414 made the engine attach a hover-to-copy affordance to every heading and
emit heading-copy-link { key } (key == the heading's stable slug) on click.
Re-subscribe in editor.vue and forward the key to editorStore.copyGithubSlug,
which copies `#<githubSlug>` to the clipboard — restoring the heading-anchor
copy affordance that was a documented gap after the @muyajs/core migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(desktop): match exported-TOC anchors to engine heading ids (PG8)

editor.vue now exports via @muyajs/core (#4406), and #4412 injects
github-compatible heading ids onto exported headings (deduped in document
order with a `-N` suffix). getHtmlToc still slugged via the legacy muyajs
Slugger, so `href="#slug"` targets no longer matched the injected ids and TOC
/ [TOC] links were dead. Swap to @muyajs/core's generateGithubSlug and
replicate the engine's whole-document `-N` dedup so the anchors resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(desktop): consume selection-change affiliation for menu state (PG1)

adaptSelectionChange hardcoded affiliation:[] and copied changes.type
('Caret'/'Range') into start.type/end.type, so createApplicationMenuState's
`start.type === 'span'` and functionType guards never fired — the native
Paragraph-menu check marks, loose/task-list toggles, table/code-fence
detection, and Format-disable-in-code all went dead after the @muyajs/core
migration.

#4410 added an `affiliation` chain (outermost-first) plus per-endpoint
`anchorBlockInfo`/`focusBlockInfo` (`type: 'span'` + `functionType`) to the
selection-change payload. Map them onto the legacy shape:
  - start/end `.type` and `.block.functionType` from the leaf block info,
  - affiliation passed through, with a derived `functionType` surfaced on
    `pre`/`figure` containers so table / code-fence detection lights up.

Also fix the consumer's loose-list read: the engine affiliation entry carries
`isLooseListItem` on the list block directly, not via a `children` chain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(desktop): restore saved indicator on undo-to-disk (PG15)

makeSyntheticHistory minted an ever-incrementing editSeq id on every
json-change (including undo/redo), so after edit-then-undo-to-saved the id
never matched lastSavedHistoryId and the tab stayed marked dirty even when its
content matched disk.

Derive the synthetic id from the engine undo-stack DEPTH instead — a stable
position marker that returns to its saved value when an edit is undone back to
the baseline. Seed lastSavedHistoryId to 0 (the engine's post-setContent
baseline depth, since setContent clears history) so a freshly-loaded,
never-saved document clears its dirty indicator when undone back to disk
content, mirroring the legacy history-index behaviour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(muya): add setCursorByOffset for source-mode cursor restore (PG2)

The source-code -> WYSIWYG handoff carries only a CodeMirror {line, ch} index
cursor; @muyajs/core had no index-offset -> block-path conversion, so the
WYSIWYG caret was lost. Add Muya#setCursorByOffset, reproducing the legacy
muyajs approach: inject sentinel strings into the current markdown at the
line/ch offsets, rebuild the tree (sentinels embed as literal text), find the
content blocks they landed in, then rebuild the clean document and set the
cursor by the resolved block paths + offsets. Both setContent calls run
synchronously so no intermediate paint occurs, and the method is a no-op for
stale/unresolvable cursors.

Engine helper only — desktop wiring lands in a separate commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(desktop): restore WYSIWYG caret after source-mode edit (PG2)

handleFileChange dropped the saved muyaIndexCursor on the source-code ->
WYSIWYG handoff, so the caret was lost. Consume the new engine
Muya#setCursorByOffset: when the tab has no key-based cursor but carries a
CodeMirror {line, ch} index cursor, map it onto a block-key cursor so the
caret lands where the source-mode cursor was. Restore the per-tab engine
history afterwards (setCursorByOffset re-runs setContent internally, which
clears history).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(desktop): flip parity e2e for PG1/PG2/PG15, defer PG14

Remove the `test.fail()` markers on the PG1 (Paragraph-menu check mark), PG2
(source-mode caret restore), and PG15 (saved indicator on undo-to-disk) parity
e2e tests now that the desktop wiring lands — all three genuinely pass.

PG14 (first undo after source mode reverts the bulk edit in one step) stays
`test.fail()`: recording the source-mode change as a single undo boundary needs
a general whole-document json1 diff through Editor.updateContents' pick/drop
walker, which only handles specific op shapes and would risk corrupting the
document. Deferred with an explanatory note here and in handleFileChange.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(test): reconcile parity scoreboard after wave-2 (14/15 fixed)

Update the Status column to the true post-merge state: all seven Wave-1 engine
PRs (#4408-#4414) plus the Wave-2 desktop wiring close PG1-PG13 and PG15. Fix
the "Gaps remaining" count (11 -> 1), add the PG2 setCursorByOffset engine spec,
and document why PG14 is accept-deferred (single-undo-boundary across the
source-mode handoff needs a whole-document json1 diff the op walker can't safely
apply).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: address Copilot review on parity wave-2

- editor.vue adaptSelectionChange: restore start/end block.text from the live
  anchorBlock/focusBlock so SELECTION_CHANGE can still slice the selected text
  (search prefill); the previous {functionType}-only block dropped it.
- editor.vue isIndexCursor: validate both line AND ch are numbers (factored out
  isIndexPosition) so a missing ch no longer silently clamps to column 0.
- muya setCursorByOffset: snapshot getHistory() and restore it after the
  internal setContent rebuild so the public API is caret-only and does not clear
  the undo stack; document the behaviour and cover it with a test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pull pull Bot locked and limited conversation to collaborators Jun 8, 2026
@pull pull Bot added the ⤵️ pull label Jun 8, 2026
@pull
pull Bot merged commit 0736252 into code:develop Jun 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant